Skip to content

feat(write): gate a knowledge write on duplication and citation resolution - #164

Merged
drewstone merged 1 commit into
mainfrom
feat/write-intake
Aug 21, 2026
Merged

feat(write): gate a knowledge write on duplication and citation resolution#164
drewstone merged 1 commit into
mainfrom
feat/write-intake

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Why

discovery-lab measured two store defects that no later report reverses (#157, capabilities 1 and 2a):

  • 40 near-duplicate clusters holding 113 pages, with 0 intra-cluster citations. One sweep scored 6 of 6 claims "novel" while their bodies formed a single cluster. Duplication is not the failure; unrelated duplication is. Every one of those pages could have been one citation away from structure.
  • 64 of 867 citation edges dangled. A cite whose target exists nowhere is cheapest to refuse at the moment of the write and most expensive to repair afterwards.

Both were patched locally in the lab, which deepens the recorded fork. The gate belongs in the store.

What

assertKnowledgeWriteIntake(candidates, { visiblePages, nearDuplicates?, citations? }) in src/write-intake.ts composes primitives that already exist — it adds no second detector and no second resolver:

  • the duplicate half runs the existing detectNearDuplicatePages over visible ∪ candidates, keeps only pairs touching a candidate, and refuses through KnowledgeDuplicateIntakeError carrying the offending pairs;
  • the citation half runs the existing assertKnowledgeCitationsResolved over the same corpus and raises the existing KnowledgeCitationResolutionError.

A duplicate pair is cleared by one of three authoring actions, each of which converts the duplication into structure rather than blocking it: cite the matched page, name it in contradicts, or give the candidate that page's id so the write is an update of it. Either side of a pair may declare the relation.

The candidates are part of the corpus both checks see, so a batch may cite a page it writes in the same call. Candidates replace the here entries at the same paths; an inherited or shared page at one of those paths stays visible, so a citation into it still reports its true ambiguity instead of silently resolving.

applyKnowledgeWriteBlocks(root, text, { pagesDirectory, intake }) runs the gate inside the existing mutation lock, before commitKnowledgeFileMutations, so a refused proposal leaves nothing on disk. CLI: apply-write-blocks --intake [--intake-threshold <n>].

The lab's 0.55 title+topic Jaccard is a different similarity from the detector's 0.82 title+text shingles, so threshold is exposed and the detector's own default stands. No untested constant is hardcoded here.

Simplification

Simplification: page construction from Markdown had one inline copy inside loadKnowledgePagesUnlocked; it is now knowledgePageFromMarkdown(path, content, pagesDirectory) and the reader and the gate share it, so a gate judges exactly what the store loads back. The scaffold/.md filter is likewise one predicate, isKnowledgePagePath. No second construction path was created.
Net: +477 / -38 lines, 11 files, 1 duplicated construction path avoided (2 would have existed), 0 new detectors or resolvers.
Not done here: the intake gate does not check contradicts targets or citation cycles; auditKnowledgeCitations and the broken-contradiction lint own those, and moving them to the write path is its own PR.
Tests: +8 (the duplicate refusal and each of the three outs, which is the whole contract of the gate; a path rewrite that replaces rather than duplicates; a batch citing itself; a dangling cite refused at the write path with the pages directory proven untouched; a duplicate refused and then accepted once it cites), -0 deleted. No shape assertions.

Proof

pnpm run typecheck   pass (src + contracts)
pnpm run lint        pass, 233 files
pnpm run build       pass, 35 files
pnpm api:surface     956 exports across 6 entry points, regenerated and committed
check:version-bump   8 export changes needing a minor bump, paid for by 10.1.0 -> 10.2.0
vitest src/write-intake.test.ts    8 passed
pnpm test (full)     23 failed files | 54 passed | 3 skipped (80)

The 23 failing files are identical on this branch and on clean origin/main on this macOS host, and every failure carries one error — knowledge transaction directory escaped its root, the /var -> /private/var symlink under tmpdir(). Linux CI has no such symlink. The new test realpaths its temp root, so it is green locally as well.

Refs #157

@tangletools tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Auto-approved drewstone PR — 028acfc4

This PR was opened by the trusted drewstone account.

This approval is provisional and was applied by the local stand-in because the pr-reviewer webhook host is unreachable (2026-08-21). CI on this head is fully green. The full PR reviewer audit re-runs via the resweep when the service returns and will publish findings if it detects issues.

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.

2 participants