Skip to content

Commit c4cdf5e

Browse files
authored
Merge pull request #146 from JiusiServe/fix/knowledge-section-repeated-ids
sdk(knowledge): reject a section that heads one rule ID twice
2 parents df003c5 + fd878b2 commit c4cdf5e

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

‎src/infermatrix_copilot/sdk/v1/knowledge.py‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,24 @@ def build_prompt(self, batch: KnowledgeEvidenceBatch) -> str:
478478

479479
@staticmethod
480480
def _section_rule_ids(section: str) -> tuple[str, ...]:
481-
"""The rule IDs every heading in a section carries, the declared
482-
one first: a nested ``###`` heading is a rule heading too."""
483-
ids: list[str] = []
484-
for match in _RULE_HEADING_ID.finditer(section):
485-
if match.group("rule") not in ids:
486-
ids.append(match.group("rule"))
487-
return tuple(ids)
481+
"""The rule IDs every heading in a section carries, in order, the
482+
declared one first: a nested ``###`` heading is a rule heading too.
483+
Repeats are kept so callers can reject a section that heads one ID
484+
twice (``## X-2`` over ``### X-2``, or two ``### X-3``)."""
485+
return tuple(
486+
match.group("rule") for match in _RULE_HEADING_ID.finditer(section)
487+
)
488+
489+
@classmethod
490+
def _repeated_section_rule_id(cls, section: str) -> str:
491+
"""The first rule ID a section heads more than once, else ''."""
492+
seen: set[str] = set()
493+
for heading_id in cls._section_rule_ids(section):
494+
if heading_id in seen:
495+
return heading_id
496+
seen.add(heading_id)
497+
return ""
498+
488499

489500
@staticmethod
490501
def _rule_exists(page_text: str, rule_id: str) -> bool:
@@ -643,11 +654,18 @@ def validate_proposals(
643654
"rule_id duplicates an earlier proposal on "
644655
f"{proposed_ids[rule_id]}"
645656
)
657+
if not reason and self._repeated_section_rule_id(section):
658+
reason = (
659+
"section heads rule_id "
660+
f"{self._repeated_section_rule_id(section)} more "
661+
"than once"
662+
)
646663
if not reason:
647664
# Every rule heading the section introduces, not
648665
# only the declared one: a nested heading carrying
649666
# another page's ID would otherwise land unchecked.
650667
for heading_id in self._section_rule_ids(section):
668+
651669
owner = existing_ids.get(heading_id)
652670
nested = heading_id != rule_id
653671
if owner is None and nested and heading_id in proposed_ids:
@@ -948,6 +966,8 @@ def apply(
948966
r"^##\s+", proposal.section_markdown, re.MULTILINE
949967
)) != 1
950968
or not 80 <= len(proposal.section_markdown) <= _MAX_SECTION_CHARS
969+
or self._repeated_section_rule_id(proposal.section_markdown)
970+
951971
or not proposal.sources
952972
or len(proposal.sources) > 10
953973
or any(

‎test/test_sdk_knowledge_v1.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,3 +788,40 @@ def test_nested_headings_inside_a_section_are_checked_too(workspace):
788788
assert result.rejected[0].reason == (
789789
"nested rule heading X-2 already exists on knowledge/repos/x/rules.md"
790790
)
791+
792+
793+
@pytest.mark.parametrize(
794+
"extra",
795+
[
796+
"\n\n### X-2 — the declared ID repeated in a nested heading\n\n- Required: reject. ^[PR #7]",
797+
"\n\n### X-4 — first nested\n\n- Required: a. ^[PR #7]\n\n### X-4 — second nested\n\n- Required: b. ^[PR #7]",
798+
],
799+
ids=["parent-and-nested", "two-nested"],
800+
)
801+
def test_section_heading_one_rule_id_twice_is_rejected(workspace, extra):
802+
"""A section may not head one ID twice, whether it repeats its own
803+
declared ID under ``###`` or two nested headings share an ID: the
804+
validators downstream do not enforce ID uniqueness."""
805+
curator = KnowledgeCurator(workspace)
806+
result = curator.validate_proposals(
807+
_document(_rule(section=SECTION + extra)), _batch()
808+
)
809+
810+
assert result.accepted == ()
811+
repeated = "X-2" if "X-2 — the declared" in extra else "X-4"
812+
assert result.rejected[0].reason == (
813+
f"section heads rule_id {repeated} more than once"
814+
)
815+
816+
817+
def test_apply_refuses_a_section_that_repeats_a_rule_id(workspace):
818+
curator = KnowledgeCurator(workspace)
819+
validation = curator.validate_proposals(_document(_rule()), _batch())
820+
smuggled = replace(
821+
validation.accepted[0],
822+
section_markdown=SECTION + "\n\n### X-2 — repeated\n\n- Required: reject. ^[PR #7]",
823+
)
824+
tampered = replace(validation, accepted=(smuggled,))
825+
826+
with pytest.raises(KnowledgeCurationError, match="apply-time integrity"):
827+
curator.apply(tampered, updated_on="2026-08-29")

0 commit comments

Comments
 (0)