Skip to content

Add typed knowledge operations with a rule lifecycle and the L1 gate - #204

Merged
tzhouam merged 2 commits into
mainfrom
kb/knowledge-ops-l1
Sep 28, 2026
Merged

tzhouam merged 2 commits into
mainfrom
kb/knowledge-ops-l1

Conversation

@tzhouam

@tzhouam tzhouam commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Second PR of the knowledge-base redesign. It adds Knowledge Ops API 2.0, typed changes to rule pages with a rule lifecycle, plus L1, the deterministic half of the knowledge quality gate. The v1 KnowledgeCurator (API 1.1.0) is unchanged.

What changes

  • knowledge_service/lifecycle.py: rule pages as data.

    • Pages parse into frontmatter, head and sections, and render back byte for byte (checked over all 581 pages).
    • A section is a ## heading, or a ### rule heading under a non-rule topic heading. That picks up 102 existing ### rules such as SERV-5r.
    • Code fences are respected, and nested ### headings inside a ## rule stay with it.
    • A rule may end with a <!-- kb:rule ... --> footer (status, since, retired_at, reason, evidence, supersedes, superseded_by, protected). No footer means active.
  • knowledge_service/ops.py: pure apply_operations() covering:

    • add, including to a new rules-<topic>.md page, which also gets its _index.md line;
    • edit_same_meaning;
    • replace: retires the old rule as superseded and adds the successor atomically, keeping the chain intact;
    • retire;
    • purge: only after one full release; the IDs, nested ones included, go into _tombstones.yaml and are reserved forever.

    It writes updated: and append-only sources:, refuses protected rules unless on the human path, and refuses any page past the 32 KiB / 500-line split line.

  • knowledge_service/l1.py:

    • check_tree(): footers, supersede links, tombstones, _routes.yaml.
    • check_changeset() enforces:
      • path whitelist and mode 100644;
      • no new or reused IDs, no dangling references, no new tree issues;
      • capacity, page type, protection taken from the old footer, nested IDs, a link for every new page, and purge timing.
    • Mechanical changes are recomputed, and anything else becomes rule or prose blocks with stable block_ids for per-block L2 verdicts.
    • References outside knowledge/ are reported, for companion PRs.
  • Retired rules stop being served immediately. Direct document reads, doc_read, doc_search and briefings drop them. Pages without retired rules are served unchanged.

  • Docs: doc/knowledge/SCHEMA.md covers lifecycle footers and _routes.yaml. SPEC pages added for the three modules.

Test Plan

  • test/test_knowledge_ops_l1.py (29 tests):
    • each operation on a synthetic tree;
    • forged mechanical changes;
    • whitelist and mode rules;
    • prose blocks;
    • external references;
    • withholding retired rules;
    • ### rules on the real rules-engine-lifecycle.md;
    • the review-found regressions: page-type bypass, dropped protection, supersede chain, nested IDs, index links, purge timing;
    • one real-tree integration test: add a rule to a new page, retire one and replace another on the real scheduler page, then run both existing validators on the result.
  • Full suite, check_spec_freshness, check_doc_links, both knowledge validators.

Test Result

  • Full suite passed. test_testing_substrate.py::test_final_scan_kill_works_after_leader_reaped failed once in a full run (process-timing flake) and passed 3/3 when rerun on its own.
  • check_spec_freshness: 86/86. check_doc_links: OK. check_knowledge_tree: 0 errors.
  • Local review gate (gpt-6-astra-high): approved after 8 rounds of fixes, all covered by regression tests.

🤖 Generated with Claude Code

The v1 curation path can only append a rule, so nothing could ever retire,
supersede or delete one, and the SDK never wrote `sources:` or index lines.
This adds the Knowledge Ops API 2.0 the knowledge service and its quality gate
build on. The v1 KnowledgeCurator (API 1.1.0) is unchanged.

- lifecycle: rule pages parse into frontmatter, head and level-two sections
  (fence-aware) and render back byte for byte (checked over the whole tree).
  A rule may end with a `<!-- kb:rule ... -->` footer: status, since,
  retired_at, reason, evidence, supersedes, superseded_by, protected.
- ops: apply_operations() is pure. add (also to a new rules-<topic>.md page
  plus its index line), edit_same_meaning, replace (retire as superseded and
  add the successor atomically), retire, purge (retired for a full release;
  the ID goes to _tombstones.yaml and is reserved forever). It writes
  updated:, append-only sources:, index lines and tombstones, refuses
  protected rules without the human path, and refuses a page past the split
  line.
- l1: check_tree() and check_changeset(). The latter enforces the path
  whitelist and 100644 mode, no new duplicate or reused IDs, no dangling
  references to retired or purged rules, and no new tree issues. It
  recomputes every mechanical change and splits the rest into rule and prose
  blocks with stable block IDs for per-block L2 verdicts. It also reports
  references outside knowledge/ for companion PRs.
- Retired rules stop being served at once: Direct document reads, doc_read,
  doc_search and briefings drop them; pages without retired rules are served
  byte for byte as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T10:46:29.458832Z 1d94d75 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The package page was not re-verified when lifecycle, ops and l1 were added,
so the strict SPEC freshness gate failed in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d94d75f62

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +194 to +196
models = data.get("models")
if models is not None and (not isinstance(models, dict) or not models.get("dir") or not models.get("page")):
issues.append(Issue("routes_invalid", path, "models needs dir and page"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate the model route directory before accepting routes

When _routes.yaml contains a missing or mistyped models.dir, this check accepts it because it validates only that dir and page are nonempty. The activated route later reaches direct_routing.py:490 and calls model_root.iterdir(), raising FileNotFoundError for every review with nonempty intent and breaking Direct routing for that repository. Verify that the directory exists and is a directory before accepting the route.

Useful? React with 👍 / 👎.

Comment on lines +32 to +33
ANY_RULE_HEADING = re.compile(
r"^#{2,6}\s+(?P<rule>[A-Za-z0-9][A-Za-z0-9-]{1,40})(?=\s|$)", re.MULTILINE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require rule syntax when collecting nested IDs

The pattern treats the first word of every level-2–6 heading as a rule ID without requiring the rule heading's dash-and-title syntax. In the shipped qwen3-tts/rules.md, the ordinary subheading ### PR #6001 性能证据边界 therefore makes PR a nested ID of Q3TTS-3e; retiring that rule reports PR as retired and produces hundreds of dangling_reference issues across the tree, so the valid lifecycle operation cannot pass L1. Restrict this matcher to actual rule headings.

Useful? React with 👍 / 👎.

Comment on lines +155 to +159
if footer.supersedes:
source = footers.get(footer.supersedes)
if source is not None and source[1].superseded_by != rule_id:
issues.append(Issue("supersede_link_broken", path,
f"{rule_id} supersedes {footer.supersedes}, which does not name it"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject successors whose superseded rule is absent

When an active rule footer declares supersedes=MISSING-ID, source is None and this condition silently accepts the broken link. A hand-authored change can consequently pass L1 with a successor that has no corresponding retired rule, despite the lifecycle contract requiring bidirectional supersession links; emit supersede_link_broken when the source is absent as well as when its backlink is wrong.

Useful? React with 👍 / 👎.

@tzhouam
tzhouam merged commit 19a1dcc into main Sep 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant