Skip to content

feat(evidence): prove retrieval-to-outcome knowledge use - #153

Merged
drewstone merged 18 commits into
mainfrom
feat/knowledge-use-receipts-v1
Aug 17, 2026
Merged

feat(evidence): prove retrieval-to-outcome knowledge use#153
drewstone merged 18 commits into
mainfrom
feat/knowledge-use-receipts-v1

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Mission

Add the missing proof layer between a page was visible, a retriever returned it, and an agent used it in a decision or artifact.

This is required before Discovery can claim that accumulated knowledge improves later research. Final-prose resemblance, a citation field, and a search call are not equivalent evidence.

New canonical receipts

KnowledgeVisibilitySnapshot

Binds the exact ordered current/ancestor/shared page view presented to retrieval:

  • stable page id;
  • origin;
  • path;
  • canonical page digest;
  • source ids;
  • invalidation state;
  • position.

Page text, frontmatter, source joins, citations, contradictions, and invalidation state are identity-bearing.

KnowledgeRetrievalReceipt

Binds:

  • run, actor, profile, and execution identities;
  • query;
  • retriever id/version/config digest;
  • complete visibility snapshot;
  • ranked page ids, origins, paths, digests, scores, snippets, and reasons;
  • Eval evidence refs;
  • bounded scalar attributes;
  • canonical receipt digest.

A result is refused unless the exact page bytes/path/id/origin appear in the visibility snapshot. Ranks are unique and contiguous; scores are finite.

KnowledgeUseReceipt

Selects one exact rank from a verified retrieval and binds it to a downstream:

  • decision;
  • artifact;
  • experiment;
  • candidate;
  • message;
  • other consumer.

The relation is explicit: supports, contradicts, extends, rederives, or background.

The use receipt cannot verify against a different retrieval, a rank that was not returned, a changed page digest, or a mutated consumer/relation.

Public API

  • knowledgePageDigest()
  • createKnowledgeVisibilitySnapshot()
  • createKnowledgeRetrievalReceipt()
  • verifyKnowledgeRetrievalReceipt()
  • assertKnowledgeRetrievalMatchesVisibility()
  • createKnowledgeUseReceipt()
  • verifyKnowledgeUseReceipt()
  • all receipt and identity contracts

Evidence ownership

  • Knowledge owns page visibility, retrieval, and use provenance.
  • Runtime owns execution and trace emission.
  • Eval owns semantic adjudication, novelty/reuse classification, downstream correctness, and causal comparison.

These receipts do not claim that the page is true, the declared relation is correct, the artifact passed, or knowledge improved the outcome. They provide the immutable joins those later claims require.

Tests

The suite covers:

  • ordered visibility and page-byte identity;
  • origin, order, text, source, and invalidation sensitivity;
  • repeated-path refusal;
  • deterministic receipt identity;
  • exact optional-field omission;
  • result-not-visible and post-snapshot mutation refusal;
  • duplicate/gapped ranks;
  • non-finite and out-of-range scores;
  • unsupported attributes;
  • receipt and live-snapshot mutation detection;
  • valid retrieval-to-artifact use;
  • missing selected rank;
  • mismatched retrieval;
  • selected page, relation, and consumer mutation.

Documentation

docs/knowledge-use-receipts.md defines the proof chain, non-claims, trace integration, and experiment use.

Related: tangle-network/discovery#54, G4 in meta/autonomous-discovery-v1.json.

tangletools
tangletools previously approved these changes Aug 17, 2026

@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 — 2042ddcb

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

This approval is provisional. It rests on the audit running. If the audit cannot run — for example the CLI bridge rejects it — this approval is dismissed rather than left standing, so an unrun check never reads as a passing one.

tangletools · auto-approval · reason: drewstone_author · 2026-08-17T13:25:14Z

@tangletools

Copy link
Copy Markdown
Contributor

❌ Needs Work — 7b7bf249

Review health 100/100 · Reviewer score 0/100 · Confidence 80/100 · 29 findings (6 high, 7 medium, 16 low)

opencode GLM 5.2 opencode DeepSeek v4 Pro opencode DeepSeek v4 Flash aggregate
Readiness 0 26 0 0
Confidence 80 80 80 80
Correctness 0 26 0 0
Security 0 26 0 0
Testing 0 26 0 0
Architecture 0 26 0 0

Reviewer score is advisory once the run is complete and the verdict has no blockers.

Full multi-shot audit completed 4/4 planned shots over 4 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 4/4 planned shots over 4 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 4/4 planned shots over 4 changed files. Global verifier still owns final merge decision.

Blocking

🔴 HIGH Barrel re-export publishes a module that fails repo typecheck — src/index.ts

Line 29 export * from './knowledge-use-receipts' adds the new module to the public package entrypoint. At head, pnpm typecheck:src fails with 3 errors in that module: knowledge-use-receipts.ts:168 TS2339 (Property 'cites' does not exist on type 'KnowledgePage' — src/types.ts:89 has no cites field, so the digest's cites input is always [] at runtime) and knowledge-use-receipts.ts:540 TS7006 implicit-any on reason/reasonIndex. Base commit typechecks clean. Impact: CI typecheck gate goes red and tsdown dts generation (publish gate) blocks. Fix belongs in the module (add cites?: KnowledgeId[] to KnowledgePage or drop the field from the digest input; type the map

🔴 HIGH Barrel re-exports a module that fails tsc --noEmit — src/index.ts

export * from './knowledge-use-receipts' adds a module to the public API whose source does not typecheck. pnpm typecheck:src fails with 3 errors, all in the newly re-exported file: (1) src/knowledge-use-receipts.ts:168 TS2339 — knowledgePageDigest calls canonicalCandidateDigest({ ..., cites: [...(page.cites ?? [])] }) but KnowledgePage (src/types.ts:89-102) declares contradicts?: KnowledgeId[] and no cites field, so the digest's cites term is dead (?? []) and the file won't compile; (2) src/knowledge-use-receipts.ts:540 TS7006 (reason, reasonIndex implicit any) — the if (!Array.isArray(results)) throw guard at :487 narrows the readonly OriginatedKnowledgeSearchResult[] parameter to any[], so the .map callback loses contextual typing under noImplicitAny. Impact: CI

🔴 HIGH Exported module fails pnpm typecheck — blocks CI and package build — src/index.ts

The single change in this shot, export * from './knowledge-use-receipts' (src/index.ts:29), adds a module to the package surface that does not compile. pnpm run typecheck:src fails with 3 errors on the repo-pinned TS 7.0.2: src/knowledge-use-receipts.ts(168,22) TS2339: Property 'cites' does not exist on type 'KnowledgePage' and (540,29)/(540,37) TS7006: Parameter 'reason'/'reasonIndex' implicitly has an 'any' type. CI runs pnpm typecheck and pnpm build (ci.yml:29,37; publish.yml:34,40), so this PR fails CI and cannot be published. Verified locally: pnpm run typecheck exits 1 at typecheck:src (typecheck:contracts never runs); tsdown also failed to emit d.ts for src/index.ts. Fix in the module (not the export line): (1) either add cites?: KnowledgeId[] to KnowledgePage in s

🔴 HIGH TS 7.0.2 silently degrades map element to any — reasons.map params have no contextual type — src/index.ts

In the exported module, normalizeRetrievalResults (src/knowledge-use-receipts.ts:483-555) loses type safety and fails typecheck at the inner map: result.reasons.map((reason, reasonIndex) => nonEmpty(reason, ...)) (lines 540-542). Isolated by bisection under TS 7.0.2 with the repo flags: the error is caused specifically by the COMBINATION of if (!Array.isArray(results)) (line 487) + the type-predicate ternary snippet: typeof result.snippet === 'string' ? result.snippet : '' ([line 538](https://github.com/tangle-network/agent-knowledge/blob/7b7bf249

🔴 HIGH Head fails pnpm typecheck: page.cites does not exist on KnowledgePage — src/knowledge-use-receipts.ts

knowledgePageDigest builds its digest material with cites: [...(page.cites ?? [])], but KnowledgePage (src/types.ts:89-102) has no cites field — only contadicts? and invalidation?. Verified: pnpm typecheck at head 7b7bf24 emits TS2339: Property 'cites' does not exist on type 'KnowledgePage' at 168:22; base a31bca5 typechecks clean (isolated checkout, exit 0). CI runs pnpm typecheck on every PR (.github/workflows/ci.yml:29), so this branch is red. The field is also dead weight: real pages never carry cites, and no test exercises it. Fix: drop the cites line (digest already covers contadicts and invalidation), or first add cites to KnowledgePage with a test proving it changes the digest.

🔴 HIGH page.cites does not exist on KnowledgePage; typecheck fails and citations are never digested — src/knowledge-use-receipts.ts

cites: [...(page.cites ?? [])] reads a property absent from KnowledgePage (src/types.ts:89-102 has no cites field and no index signature). tsc --noEmit errors TS2339 here, failing pnpm typecheck, which ci.yml runs as a required step. It is not just a type error: no code in the repo produces a cites field on pages (grep for cites: returns only this line), so at runtime page.cites is always undefined and the digest always hashes cites: []. This means the page digest does not cover citations even though docs/knowledge-use-receipts.md:33 claims 'citations ... are included in each page digest'. Fix: either add a real cites field to KnowledgePage and populate it in the loader, or delete the cites line and correct the docs claim.

Other

🟠 MEDIUM Doc example imports canonicalCandidateDigest from the wrong package — docs/knowledge-use-receipts.md

Lines 60-64 instruct import { canonicalCandidateDigest, createKnowledgeRetrievalReceipt } from '@tangle-network/agent-knowledge'. Verified empirically: importing src/index.ts yields typeof canonicalCandidateDigest === 'undefined'; the symbol is exported from '@tangle-network/agent-interface' (agent-candidate-schema-common) and no module in this package re-exports it (rg over src/ shows only imports, no re-export). The example at line 75 then uses it to compute the required retriever.configDigest Sha256Digest, so the

🟠 MEDIUM Page digest hashes a cites field the type cannot express — src/index.ts

knowledgePageDigest (src/knowledge-use-receipts.ts:157-172) includes cites: [...(page.cites ?? [])] in the canonical digest, but KnowledgePage (src/types.ts:89-102) defines contradicts?: KnowledgeId[] and no cites. This is the root cause of the TS2339. It also creates a silent identity divergence: the receipt API claims pageDigest binds 'one exact knowledge page', but any page object that happens to carry an undeclared cites property (the test fixtures do — src/knowledge-use-receipts.test.ts:34) hashes differently from an otherwise identical page without it, while the type says identity is fully captured by the declared fields. The digest is the tamper-evidence anchor for both receipt kinds, so its input should be exactly the typed page surface. Fix: add `cites?: KnowledgeId[

🟠 MEDIUM Invalidation and sourceIds snapshot state claimed by test title but never asserted or exercised — src/knowledge-use-receipts.test.ts

The test title promises binding of 'sources, and invalidation state', but no fixture page in the file sets invalidation (so entry.invalidated is always false), and no assertion reads entry.sourceIds or entry.invalidated. Every digest assertion is relative (same-constructor equality or not.toBe on mutation), so a regression that drops sourceIds from KnowledgeVisibilitySnapshotEntry or hardcodes invalidated: false (impl knowledge-use-receipts.ts:202) passes the whole suite. invalidated is the audit-critical state for stale-page receipts. Fix: add a fixture page with invalidation: { verdict: 'contradicted', observedAt, reason }, assert entries[i].invalidated === true and sourceIds equality, and assert that flipping invalidation changes snapshotDigest.

🟠 MEDIUM Implicit-any .map callback params fail tsc --noEmit under typescript@7.0.2 — src/knowledge-use-receipts.ts

result.reasons.map((reason, reasonIndex) => ...) produces TS7006 (implicit any) for both params, so pnpm typecheck fails. The code is semantically correct (KnowledgeSearchResult.reasons is string[] at src/types.ts:155); explicitly annotating (reason: string, reasonIndex: number) clears both errors, and a faithful isolated repro of the same interface-extension + .map pattern compiles clean — indicating a TypeScript 7.0.2 native-compiler inference gap in this specific context rather than a type-safety bug. Still a merge blocker against the pinned toolchain. Fix: add explicit param types (or bump/repin the compiler once the inference gap is resolved).

🟠 MEDIUM Silent fallback converts non-string snippet to empty string — src/knowledge-use-receipts.ts

snippet: typeof result.snippet === 'string' ? result.snippet : '' silently defaults a malformed retriever snippet to ''. Every other field fails loud with a TypeError (rank, scores, reasons, origin, page shape), and the repo doctrine is 'No fallbacks. Fail loud.' Recording an empty snippet in an immutable evidence receipt erases the discrepancy instead of surfacing it; the receipt then verifies as valid despite the source retriever returning non-string data. Fix: throw a TypeError on non-string snippet, matching the sibling validations.

🟠 MEDIUM TS7006 implicit-any errors in normalizeRetrievalResults block CI typecheck — src/knowledge-use-receipts.ts

pnpm typecheck at head also emits TS7006: Parameter 'reason'/'reasonIndex' implicitly has an 'any' type at 540:29 and 540:37 in the result.reasons.map((reason, reasonIndex) => ...) callback inside the frozen result object. Combined with the 168:22 error this makes typecheck exit non-zero independently of the cites issue. Fix: annotate the callback params ((reason: string, reasonIndex: number)) or type the intermediate result; re-run pnpm typecheck to prove clean.

🟠 MEDIUM Whitespace-only mutation to normalized receipt fields is undetected by verification — src/knowledge-use-receipts.ts

verifyKnowledgeRetrievalReceipt rebuilds material with re-normalized values (retriever via normalizeRetriever L280, evidenceRefs L283, attributes L284, runId/query/actorId via nonEmpty L275-279) then compares canonicalCandidateDigest against receipt.receiptDigest, but returns the raw stored receipt. nonEmpty (L729-734) trims, so any stored-field mutation that reduces to the same trimmed value survives verification. Reproduced with real sha256: mutating a stored receipt's query 'q'->'q ' and retriever.id 'tok'->'tok ' makes verify pass and returns the padded values (likewise for runId, actorId, evidenceRefs uri/excerpt, and attribute keys). For an 'immutable proof' module the stored fields no longer equal what receiptDigest covers; consumers reading receipt.query/retriever.id see padded, un

🟡 LOW Doc example imports canonicalCandidateDigest from the wrong package — docs/knowledge-use-receipts.md

The retrieval-receipt example does import { canonicalCandidateDigest, createKnowledgeRetrievalReceipt } from '@tangle-network/agent-knowledge'. createKnowledgeRetrievalReceipt is a public export of agent-knowledge, but canonicalCandidateDigest is not: the package index (src/index.ts) does not re-export it, and the source imports it from '@tangle-network/agent-interface' (src/knowledge-use-receipts.ts:3), as does the test (src/knowledge-use-receipts.test.ts:1). A consumer copying the example gets TS2305 'no exported member'. Fix: change the import to canonicalCandidateDigest from '@tangle-network/agent-interface' (keep createKnowledgeRetrievalReceipt from agent-knowledge), or re-export canonicalCandidateDigest from the package.

🟡 LOW Doc example imports canonicalCandidateDigest from wrong package — docs/knowledge-use-receipts.md

The retrieval-receipt example (lines 61-64) does import { canonicalCandidateDigest, createKnowledgeRetrievalReceipt } from '@tangle-network/agent-knowledge'. canonicalCandidateDigest is not exported by @tangle-network/agent-knowledge: it is imported from @tangle-network/agent-interface in the implementation (src/knowledge-use-receipts.ts:3) and in the test (src/knowledge-use-receipts.test.ts:1), and src/index.ts re-exports only the local module's own exports via export * from './knowledge-use-receipts' (which re-exports nothing from agent-interface). Grep for canonicalCandidateDigest across src/ shows no export of it. A user copying the e

🟡 LOW Page digest coverage list omits title, tags, and outLinks — docs/knowledge-use-receipts.md

The doc states the page digest includes 'page text, frontmatter, citations, contradictions, invalidation state, path, and source joins'. knowledgePageDigest (src/knowledge-use-receipts.ts:157-172) additionally includes id, title, tags, and outLinks. The doc's claim is not wrong (all listed items are covered) but understates identity-bearing inputs; a reader auditing digest collisions would miss three fields. Fix: add title, tags, and outLinks to the list.

🟡 LOW Dead cites fixture field mirrors a property absent from KnowledgePage — src/knowledge-use-receipts.test.ts

The page() helper declares cites?: string[] (line 23) and spreads it into the returned page (line 34), but no test in the file ever passes cites (grep confirms only lines 23 and 34 reference it). KnowledgePage in src/types.ts:89-102 has no cites property, and the implementation's knowledgePageDigest (knowledge-use-receipts.ts:168) reads page.cites ?? [], which is a TS2339

🟡 LOW Dead cites fixture option models a field that does not exist on KnowledgePage — src/knowledge-use-receipts.test.ts

page(input: { ... cites?: string[] }) stamps cites onto the page (line 34) via spread (which bypasses excess-property checks), but zero tests pass cites, and KnowledgePage (src/types.ts) has no cites field — it has contradicts. The sibling impl reads page.cites at knowledge-use-receipts.ts:168 and pnpm typecheck fails at head with TS2339 'Property cites does not exist on type KnowledgePage' (plus two TS7006 implicit-any at :540), while these tests still pass because vitest does not typecheck. The intended digest binding for cites is therefore never pinned by any test. Fix: either rename the fixture field to contradicts to match the re

🟡 LOW Validation and verify-time guard paths left untested — src/knowledge-use-receipts.test.ts

Untested throw paths in the module under test: schemaVersion/kind/digestAlgorithm rejection in both verify functions (impl:257-264, 358-366), verify-time visibility snapshotDigest mismatch (impl:269-271), repeated page at one origin in results (impl:507-512), negative/non-integer selectedRank (impl:313-315), invalid relation (impl:677) and invalid consumer kind (impl:615), invalid evidenceRef kind (impl:632), and the happy path of assertKnowledgeRetrievalMatchesVisibility. The security-critical paths are covered, so this is breadth, not correctness — add a table-driven rejection test for the remaining guards.

🟡 LOW Verify-path integrity branches untested: result-not-in-visibility and structural guards — src/knowledge-use-receipts.test.ts

Mutation tests cover query/relation/consumer/used.pageDigest only (all caught by digest recompute). Untested are the distinct guard branches in verify: a forged-but-self-consistent receipt whose result row is not in its visibility entries (impl validateReceiptResults lines 576-579 and 573 repeat check), a mutated schemaVersion/kind/digestAlgorithm (impl lines 257-265), and a visibility object whose snapshotDigest disagrees with its entries (impl [lines 266-271](https://github.com/tangle-network/agent-know

🟡 LOW assertKnowledgeRetrievalMatchesVisibility has no positive-path test — src/knowledge-use-receipts.test.ts

Only the failure case (mutated visible pages) is tested. A regression that made the function throw unconditionally would pass the suite. Add: call it with pages byte-equal to the receipt's original visibility and assert it does not throw. Additionally, the first visibility test is named 'binds ... and invalidation state' (line 95) but never asserts the entry's invalidated or sourceIds values directly, only the aggregate snapshotDigest.

🟡 LOW page() helper declares cites but no test exercises it; digest-bound fields unproven — src/knowledge-use-receipts.test.ts

The page() fixture accepts and spreads a cites field (lines 23, 34), but no call site passes it, and knowledgePageDigest (impl line 168) folds cites, contradicts (169), and invalidation (170) into the digest identity. A regression that dropped any of these fields from the digest would change no test result. Fix: add one test asserting the snapshot/result digest changes when cites, contradicts, or invalidation differ between otherwise-identical pages.

🟡 LOW Attribute keys are trimmed and distinct keys silently collapse — src/knowledge-use-receipts.ts

nonEmpty(key) trims attribute keys, so input { ' a': 1, 'a': 2 } collapses to a single a: 2 with no error — silent data loss in the receipt. A __proto__ key is also dropped (assignment hits the plain object's prototype setter rather than creating an own property), so it vanishes from the receipt without being rejected. Impact is limited to attributes metadata, but it violates fail-loud on user-supplied evidence. Fix: reject keys that differ from their trimmed form, and reject prototype-setter keys.

🟡 LOW NUL character can collide (origin, path) join keys — src/knowledge-use-receipts.ts

Visibility identity and result-join keys are `${origin}�${path}` (also lines 489, 506, 562), while validateOrigin accepts any inherited:<non-blank> string and validateKnowledgePage never rejects control characters in path/id. Probe confirmed: origin inherited:a\u0000b with path c.md collides with origin inherited:a and path b\u0000c.md, so two legitimate entries are rejected as repeats path. Direction is fail-closed (spurious rejection, never wrongful acceptance — the pageId/pageDigest join still gates), and the RFC-8785-style serializer escapes NUL so digests stay unambiguous. Fix: reject \u0000 in validateOrigin/`validateKnowl

🟡 LOW Origin/path key joins can alias on the NUL separator — src/knowledge-use-receipts.ts

Membership and duplicate-detection keys are ${origin}\u0000${path} (L188, L489, L506, L562). validateOrigin (L707-717) accepts any inherited:<suffix> including control characters, and page paths are not rejected for NUL, so distinct pairs can alias — e.g. (origin 'inherited:a', path 'b\u0000c') and (origin 'inherited:a\u0000b', path 'c') both key to 'inherited:a\u0000b\u0000c'. Consequence is a false 'repeats path' error or a wrong visibility membership match, i.e. availability/correctness risk. Practically unreachable (paths come from store rel, origins from lineage runIds, neither carries NUL today) but cheap to close by rejecting control characters in validateOrigin and page paths.

🟡 LOW Test factory masks the type break by injecting cites through a spread — src/knowledge-use-receipts.ts

Evidence that the suite stayed green over a non-compiling source: src/knowledge-use-receipts.test.ts:34 builds fixtures with ...(input.cites ? { cites: input.cites } : {}), which bypasses excess-property checking against the KnowledgePage return type. Result: pnpm vitest run passes 15/15 while tsc --noEmit fails on the same value shape the tests construct. No test ever passes cites, so the digest's cites contribution is also untested. Fix alongside finding 1; going forward the fixture should not add fields absent from KnowledgePage.

🟡 LOW nonEmpty silently trims identity-bearing strings, mutating the recorded query/path/pageId — src/knowledge-use-receipts.ts

nonEmpty returns value.trim(), and it is applied to query, runId, actorId, page ids/paths, sourceIds, and reasons. Verified at runtime: createKnowledgeRetrievalReceipt({ query: ' padded ' }) stores query: 'padded' — the receipt records a different query than the caller passed, silently mutating evidence in a module whose contract is 'exact query' (docs line 51) and whose repo doctrine is fail-loud, not normalize. No digest collision results (knowledgePageDigest binds the untrimmed path via its own digest), so this is a fidelity gap rather than a verifiability hole, but the exactness claim is weakened. Fix: preserve the raw string, or reject wh

🟡 LOW rfc8785-sha256 label overstates the underlying canonicalizer — src/knowledge-use-receipts.ts

The constant claims RFC 8785, but canonicalCandidateDigest (agent-interface 1.0.0, agent-candidate-schema-common.js:60-73) serializes with Object.keys().sort() (UTF-16 order, not RFC 8785's UTF-32 code-point order) and JSON.stringify numbers, with no I-JSON/lone-surrogate normalization. In-repo create/verify are self-consistent (probed: key-order insensitivity and deterministic round-trips hold), so integrity is unaffected; but an external verifier implementing true JCS would compute different digests for non-BMP keys or lone surrogates. Fix: either rename the algorithm label to match the implementation or note the deviation where the constant is defined.

🟡 LOW knowledgePageDigest hashes a phantom 'cites' field absent from the KnowledgePage type; cites/contradicts shapes are not validated — src/knowledge-use-receipts.ts

KnowledgePage (src/types.ts) declares no cites field, yet the digest includes cites: [...(page.cites ?? [])] — so a structurally-typed caller that attaches a cites property silently changes the page digest relative to typed callers, while validateKnowledgePage (L683-705) never validates cites/contradicts/invalidation shapes. A non-array contradicts (e.g. a string) is spread [...'abc'] into chars without error, changing the digest over garbage. The real store path is safe (store.ts:124,136 idListField and zod-validated invalidation), so this is a robustness gap for structurally-constructed pages. Fix: validate cites/contradicts as string arrays (and invalidation shape) in validateKnowledgePage, or drop the phantom field.


tangletools · 2026-08-17T14:02:44Z

@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.

❌ 6 Blocking Findings — 7b7bf249

Full multi-shot audit completed 4/4 planned shots over 4 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 4/4 planned shots over 4 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 4/4 planned shots over 4 changed files. Global verifier still owns final merge decision.

Summary comment for this run: full summary


tangletools · 2026-08-17T14:02:44Z

@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 — a9028df0

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

This approval is provisional. It rests on the audit running. If the audit cannot run — for example the CLI bridge rejects it — this approval is dismissed rather than left standing, so an unrun check never reads as a passing one.

tangletools · auto-approval · reason: drewstone_author · 2026-08-17T16:22:02Z

@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.

🟡 Value Audit — sound-with-nits

Verdict sound-with-nits
Coverage 2 of 2 lenses (value, usefulness)
Concerns 3 (3 weak-concern)
Heuristic 0.0s
Duplication 0.1s
Interrogation 398.6s (2 bridge agents)
Total 398.7s

💰 Value — sound-with-nits

Adds a canonical, digest-verified provenance chain (visible pages → retrieval receipt → downstream use receipt) that the package completely lacked; coherent, in-grain, verified working end-to-end, with only minor internal-duplication and no-producer nits.

  • What it does: Adds src/knowledge-use-receipts.ts (749 lines, exported from src/index.ts:29): (1) createKnowledgeVisibilitySnapshot() binds the ordered current/ancestor/shared page view (from run-scoped.ts:25 OriginatedPage[]) into per-entry page digests plus a snapshot digest; (2) createKnowledgeRetrievalReceipt() binds run/actor/profile/execution identity, query, retriever id+version+config digest, the full vi
  • Goals it achieves: The package's stated mission (AGENTS.md, verified-research-loop, claim ledgers) is that accumulated knowledge improves later research — but before this PR nothing in the repo binds 'a retriever returned this page' or 'this page was used in this decision' to exact page bytes. I searched: search.ts:44 KnowledgeSearchHit carries only a citationId handle; types.ts:134 KnowledgeSearchResult has no prov
  • Assessment: Good change on its merits. It follows the codebase's established grain exactly: the same canonicalCandidateDigest self-verification pattern as kb-improvement/activation.ts:120-130, the same frozen-immutable-object discipline, reuse of agent-eval's EvidenceRef and agent-interface's sha256DigestSchema instead of new serialization, and it correctly builds on run-scoped.ts's PageOrigin vocabulary rath
  • Better / existing approach: none — this is the right approach. Searched for existing equivalents: rg for receipt/retrieval/visibility/provenance across src/ (this repo) and the installed @tangle-network/agent-eval and @tangle-network/agent-interface .d.ts files — the only receipt machinery is mutation/cost/usage receipts (kb-improvement/contracts.ts:130, agent-eval CostReceipt/AnalystUsageReceipt), which cover different conc
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error

🎯 Usefulness — sound-with-nits

A genuinely missing tamper-evident proof layer (visible pages → retrieval → downstream use) built exactly in the repo's established digest-receipt grain, exported and documented; only nit is that callers must hand-stitch origin onto search results because no run-scoped search helper composes the pie

  • Integration: Reachable as public package API: exported at src/index.ts:29 (export * from './knowledge-use-receipts'), documented in README.md:24,91-119,341-355 and a full adoption doc at docs/knowledge-use-receipts.md. No production caller inside this repo yet — only the co-located test — but this is a library package whose consumers are downstream packages (per AGENTS.md layering, agent-runtime composes age
  • Fit with existing patterns: Follows the codebase's established digest-receipt pattern rather than inventing one: kb-improvement/activation.ts:116-124 verifies records by recomputing canonicalCandidateDigest over content minus the digest field — the new module does exactly this at two levels (snapshot digest + receipt digest, src/knowledge-use-receipts.ts:207,245,285). It also uses the same shared primitives (`canonicalCa
  • Real-world viability: Holds up beyond the happy path: deterministic digests for identical evidence (test at src/knowledge-use-receipts.test.ts:161-170), fabrication refused when a result was never visible (receipts.ts:512-517), mutation between snapshot and result refused via page-digest join (receipts.ts:518-523), post-hoc page mutation caught by assertKnowledgeRetrievalMatchesVisibility (receipts.ts:293-302), forge
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

🎯 Usefulness Audit

🟡 Adoption requires callers to hand-attach origin to search results; a run-scoped search helper would remove the glue [ergonomics] ``

searchKnowledge() (src/search.ts:48-100) does not return origin, and loadChain() (src/run-scoped.ts:110) does not search. So every future caller of createKnowledgeRetrievalReceipt must load the chain, build an index, search, then join each hit back to its chain entry by origin\0path to produce OriginatedKnowledgeSearchResult (src/knowledge-use-receipts.ts:40-42,487-489). That join is exactly the key the module itself uses internally, so a small `searchRunScopedKnowledge(stores, runId

💰 Value Audit

🟡 Create and verify paths duplicate the result-row join validation [maintenance] ``

normalizeRetrievalResults (src/knowledge-use-receipts.ts:482-554) and validateReceiptResults (src/knowledge-use-receipts.ts:556-589) both implement origin+path keying, visibility-map joins, rank contiguity, and score finiteness/range checks with slightly different shapes (page objects vs receipt rows). Extract the shared row-level checks (origin validation, key construction, rank/score rules, visibility-entry match) into one helper both paths call, so a future rule change cannot fix one path and

🟡 No code path in this package emits the receipts yet [proportion] ``

Grepped all call sites: createKnowledgeRetrievalReceipt/createKnowledgeUseReceipt are referenced only by the test file and docs; the existing retrieval machinery (run-scoped.ts:110 loadChain, search.ts:48 searchKnowledge) never produces them, so today a caller must assemble visiblePages and results by hand. Contracts-first is acceptable under the package layering (agent-runtime adapters are the intended emitters, per docs/knowledge-use-receipts.md:153), but the proof layer's value stays zero unt


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260817T162902Z

@tangletools

Copy link
Copy Markdown
Contributor

✅ No Blockers — a9028df0

Review health 100/100 · Reviewer score 36/100 · Confidence 90/100 · 38 findings (5 medium, 33 low)

opencode GLM 5.2 opencode DeepSeek v4 Pro opencode DeepSeek v4 Flash aggregate
Readiness 36 58 42 36
Confidence 90 90 90 90
Correctness 36 58 42 36
Security 36 58 42 36
Testing 36 58 42 36
Architecture 36 58 42 36

Reviewer score is advisory once the run is complete and the verdict has no blockers.

Full multi-shot audit completed 6/6 planned shots over 6 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 6/6 planned shots over 6 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 6/6 planned shots over 6 changed files. Global verifier still owns final merge decision.

🟠 MEDIUM 373 lines of release history (7.2.6 through 4.0.0) silently deleted — CHANGELOG.md

Commit 588b474 (the only commit touching CHANGELOG.md in this PR) removes the changelog entries for every release from 7.2.6 down to 4.0.0 (diff: +5/-374). Evidence: git show 588b474 -- CHANGELOG.md shows the deletions with no stated reason; the PR body documents only the new receipts feature; no changelog-retention/truncation policy exists in the repo (rg for keep-a-changelog/truncate/changelog policy finds nothing outside this file); and CHANGELOG.md is in the npm files array (package.json:56), so the history is also lost from the distributed artifact, not just the repo. The new file ends abruptly at the 7.2.7 entry (line 84). This is unrelated to the PR mission and is

🟠 MEDIUM Release history (4.0.0–7.2.6) silently deleted from changelog — CHANGELOG.md

git diff a31bca5..a9028df -- CHANGELOG.md removes 374 lines: the 7.2.7 '### Added' gradeFor entry and every section from '## 7.2.6' through '## 4.0.0'. The result (read from disk, 84 lines) ends at 7.2.7 with no history below it. No other file in the PR (diff --stat: README +49, docs/knowledge-use-receipts.md +184, src/* new) receives the removed text, so this is a deletion, not a move. The lost entries include upgrade-critical migration/breaking-change notes (4.0.0 run-directory non-migration, 7.0.0 ClaimLedgerMigrationRequiredError, 6.x bare-side-effect-import/packed-verification fixes). Impact: consumers on older versions lose the only record of breaking changes and migration steps. Fix: restore the 4.0.0–7.2.6 sections (the new feature entry belongs under 'Unreleased', not as a replac

🟠 MEDIUM Doc example imports canonicalCandidateDigest from the wrong package and does not compile — docs/knowledge-use-receipts.md

Lines 61-64 show import { canonicalCandidateDigest, createKnowledgeRetrievalReceipt } from '@tangle-network/agent-knowledge'. The package root does not export canonicalCandidateDigest: src/index.ts contains no agent-interface re-export, grep across src/ finds no re-export of the symbol, and src/knowledge-use-receipts.test.ts:1 imports it from '@tangle-network/agent-interface'. A reader copying the doc's primary retrieval-receipt example gets a compile-time import error, undermining the file's stated purpose (receipt onboarding DX). Fix: change the import source for canonicalCandidateDigest to '@tangle-network/agent-interface' (or split the import state

🟠 MEDIUM Digest sensitivity to provenance fields is untested — src/knowledge-use-receipts.test.ts

The identity-change test mutates only page.text. knowledgePageDigest (knowledge-use-receipts.ts:157-171) binds id, path, title, frontmatter, sourceIds, tags, outLinks, contradicts, and invalidation. If a regression dropped sourceIds (the provenance binding these receipts exist to prove) or invalidation from the digest material, this suite would still pass. Fix: extend the test to mutate sourceIds, tags, outLinks, frontmatter, and invalidation and assert each changes snapshotDigest.

🟠 MEDIUM verify accepts receipts create can never emit: snippet and reasons not re-validated — src/knowledge-use-receipts.ts

createKnowledgeRetrievalReceipt coerces non-string snippet to '' (line 537: snippet: typeof result.snippet === 'string' ? result.snippet : '') and requires every reason to be a non-empty string (nonEmpty). verifyKnowledgeRetrievalReceipt -> validateReceiptResults (line 585) only checks Array.isArray(result.reasons) and never checks snippet at all. Runtime-proven with a recomputed digest: a receipt whose snippet is {nested:{deep:true}} or whose reasons are [123, {}] passes verify, while the create path can never produce

🟡 LOW Changelog truncation cuts the 7.2.7 release mid-entry — CHANGELOG.md

The deletion removed releases 7.2.6 through 4.0.0 and also the 7.2.7 '### Added' subsection ('gradeFor(evidence, execution) returns { verdict, note }...') while keeping 7.2.7's '### Changed' section. The oldest retained release is now half-documented: it announces verdictFor refusing uncheckable shapes but no longer documents the paired gradeFor API added in the same release. Fix: move the cut to a whole-release boundary — either restore the 7.2.7 '### Added' block or delete the entire 7.2.7 section. Content is recoverable from git (commit 588b474, +4/-373) so nothing is lost permanently.

🟡 LOW Retained 7.2.7 entry truncated mid-section (Added lost, Changed kept) — CHANGELOG.md

The same deletion cuts inside the still-retained 7.2.7 entry: it keeps the '### Changed' bullets but drops the entry's '### Added' bullet documenting gradeFor(evidence, execution). The base file had '### Added - gradeFor(evidence, execution) returns { verdict, note }...' after the 7.2.7 Changed block; the head file ends at line 84 with the 7.2.7 Changed bullets only. A current, otherwise-present release entry is now internally inconsistent (Changed without its Added), which indicates the cut was a by-product of an oversized removal rather than a clean version-boundary trim. Fix: restore the 7.2.7 '### Added' bullet together with the historical entries.

🟡 LOW Unreleased entry is a single ~90-word run-on paragraph — CHANGELOG.md

The new Unreleased bullet packs retrieval-receipt contents, use-receipt contents, the five relations, and the verification semantics into one sentence chain. Every other entry in this file (e.g. 8.0.0, 7.2.7) uses one fact per bullet with wrapped lines. Splitting into one bullet for retrieval receipts and one for use receipts would match the file's own conventions and stay within the repo's STE sentence limits (25 words descriptive). Cosmetic only; content is accurate.

🟡 LOW API table omits createKnowledgeVisibilitySnapshot — README.md

The 'Prove what knowledge was visible, retrieved, and selected for use' table row lists only createKnowledgeRetrievalReceipt and createKnowledgeUseReceipt, but the visibility step in the workflow is created by createKnowledgeVisibilitySnapshot, which is imported and used in the example at line 93 but never listed in the API table. Minor inconsistency for discoverability. Fix: add createKnowledgeVisibilitySnapshot to the row.

🟡 LOW Code example uses undefined variables — README.md

The snippet at lines 96-112 references runStores, runId, configDigest, results, and digest without showing where they come from (they are not imported or defined), unlike the surrounding examples which are self-contained. The APIs resolve correctly, so this is illustrative pseudo-code, but a reader cannot run it as written. Fix: add a brief note or show runStores = createRunScopedStores(...) and the configDigest/digest definitions to match the other runnable examples.

🟡 LOW Duplicate receipt section after License — README.md

Lines 349-355 ('## Proving knowledge retrieval and use') restate the same three facts and the same three API names already covered in the '## Prove what the agent saw and used' section (lines 85-119). It adds no code example and sits after '## License / MIT', which is structurally odd. Evidence: both sections describe visibility/retrieval/use receipts and both link the identical docs/knowledge-use-receipts.md. Fix: delete lines 349-355 and

🟡 LOW Duplicate receipts section placed after License — README.md

The new '## Proving knowledge retrieval and use' section (lines 349-355) sits after '## License' (line 345) and restates the mid-file '## Prove what the agent saw and used' section (lines 85-119): same three functions, same proof-boundary caveat, same doc link. Impact: two sections drift independently on future edits and License is conventionally the final section. Fix: fold the verifier-failure sentence into the mid-file section (or into the ex

🟡 LOW Quickstart uses runStores without showing construction — README.md

The receipt quickstart calls runStores.loadChain(runId) but runStores is never constructed in the README; createRunScopedStores appears only as a table row (line 23). Unlike trainScenarios or updateCandidate (application callbacks), runStores is a package API, so the sample cannot be adapted without reading src/run-scoped.ts. configDigest, results, and digest are also undefined in the snippet, though that elision matches existing README style. Fix: add one line above the sample, e.g. const runStores = createRunScopedStores({ root: './support-kb' }), and import it in the snippet's import list.

🟡 LOW Trailing section duplicates the new receipts section and sits after the License heading — README.md

The PR adds two near-identical sections: '## Prove what the agent saw and used' (README.md:85-119) and '## Proving knowledge retrieval and use' (README.md:349-355), which repeats the same three-facts framing, the same function list, and the same docs link, and is placed after '## License' (README.md:345-347), breaking the conventional last-section structure. Readers see the receipts feature introduced twice with slightly different wording. Fix: delete the trailing section (lines 349-355) or merge its canonical-serialization/trace-attribute pointers into the earlier section before License.

🟡 LOW Doc example imports canonicalCandidateDigest from the wrong package — docs/knowledge-use-receipts.md

The retrieval-receipt example does import { canonicalCandidateDigest, createKnowledgeRetrievalReceipt } from '@tangle-network/agent-knowledge', but canonicalCandidateDigest is not exported by @tangle-network/agent-knowledge. Evidence: src/index.ts re-exports no module that re-exports it (grep across src/ finds it only in src/knowledge-use-receipts.ts as an import and in the test file); the module's own test imports it from '@tangle-network/agent-interface' (src/knowledge-use-receipts.test.ts line 1); agent-interface's dist/index.d.ts [line 20](https://github.com/tangle-network/agent-knowledge/blob/a9028df0ef66b6765fb42c315949144e81b0091c/docs/knowledge-use-

🟡 LOW Example imports canonicalCandidateDigest from the wrong package — docs/knowledge-use-receipts.md

Lines 61-64 import canonicalCandidateDigest from '@tangle-network/agent-knowledge', but that symbol is only exported by '@tangle-network/agent-interface'. src/index.ts does export * from './knowledge-use-receipts' but knowledge-use-receipts.ts imports canonicalCandidateDigest as a value and does not re-export it, and no src file re-exports agent-interface. Confirmed: node_modules/@tangle-network/agent-interface/dist/index.d.ts line 20 exports canonicalCandidateDigest; the package's own test (src/knowledge-use-receipts.te

🟡 LOW Visibility snippet references undeclared stores and runId — docs/knowledge-use-receipts.md

The snippet const visibility = createKnowledgeVisibilitySnapshot(await stores.loadChain(runId)) uses stores and runId without introducing them; the loadChain API exists on RunScopedStores (src/run-scoped.ts:63), and the README's parallel example (README.md:96) declares runStores via createRunScopedStores. Minor consistency gap: readers cannot tell where stores comes from. Fix: mirror the README and show const runStores = createRunScopedStores(...) or state that stores is a RunScopedStores instance.

🟡 LOW Package build and full-suite verification not runnable in this sandbox — src/index.ts

The tsdown build panics inside Rolldown (ThreadPoolBuildError ... IOError code 11 EAGAIN) and vitest aborts (core dump, pthread_create: Resource temporarily unavailable) under this environment's thread/process limits, so dist generation and the whole pnpm test run could not be executed here. The targeted receipts test file (15/15) and tsc --noEmit both pass. Confirm on CI that pnpm run verify:package (tsdown + publint + attw) is green, since the export adds this module to the package's public .d.ts surface.

🟡 LOW Dead cites fixture field misleads about what the digest binds — src/knowledge-use-receipts.test.ts

cites is not a field of KnowledgePage (src/types.ts:89-102) and no test passes it; knowledgePageDigest never hashes it. A reader could conclude citation links are receipt-bound when they are not. Remove the cites parameter and its spread, or test contradicts/invalidation instead since those are the fields actually bound.

🟡 LOW Dead cites parameter in page() fixture helper references a non-existent field — src/knowledge-use-receipts.test.ts

The page() helper accepts cites?: string[] and spreads { cites: input.cites } into the returned object, but KnowledgePage (src/types.ts:89-102) has no cites field, knowledgePageDigest (src/knowledge-use-receipts.ts:157) does not hash cites, and no test in this file ever passes cites. This is dead code that can mislead a reader into thinking cites is a hashed page field (and, if it were later added to the page type, it would silently change no digest). Remove the parameter and the conditional spread.

🟡 LOW Fail-closed validation branches untested — src/knowledge-use-receipts.test.ts

Several negative branches in the implementation have no assertion here: invalid origin format (e.g. inherited: empty suffix, impl line 706-716), empty/whitespace runId or query (impl line 728-733), invalid relation, invalid consumer.kind, invalid evidenceRefs[].kind (impl 626-643), out-of-order ranks [2,1] (exercises the sort at impl 545), rank 0 / non-integer rank, and non-array results/visiblePages. These are cheap one-liners against the existing retrieval() helper and would lock in the fa

🟡 LOW No serialization roundtrip test for persisted receipts — src/knowledge-use-receipts.test.ts

The receipts exist to be durable audit evidence, but every test mutates in-memory objects only. structuredClone is used once (line 113) on the visibility fixture, never on a constructed receipt, and no test runs JSON.parse(JSON.stringify(receipt)) before verifyKnowledgeRetrievalReceipt/verifyKnowledgeUseReceipt. Serialization is the realistic corruption vector for a persisted proof chain (e.g. it would surface key-omission bugs like the optional-field spread at implementation lines 439-441/466-472 leakin

🟡 LOW Uncovered positive/optional branches in verification paths — src/knowledge-use-receipts.test.ts

Two branches lack any assertion: (1) assertKnowledgeRetrievalMatchesVisibility is only exercised via its mismatch-throw path (line ~282); there is no test proving it returns silently for a matching visibility snapshot. (2) normalizeConsumer's optional-digest branch (knowledge-use-receipts.ts:620-623) is never tested — no use-receipt is created with a consumer.digest omitted, so the ...(input.digest === undefined ? {} : ...) path and its round-trip through verifyKnowledgeUseReceipt are unverified. Also selectedRank non-integer/0/NaN rejection (line 312) has no test. These are coverage nits, not defects.

🟡 LOW Use-receipt identity propagation and optional-field freeze unasserted — src/knowledge-use-receipts.test.ts

The 'binds one returned rank' test checks runId/retrievalReceiptDigest but not that actorId/profileDigest/executionRef propagate from the retrieval into the use receipt (impl useMaterial lines 466-472), and asserts Object.isFrozen on use/use.used but not on use.evidenceRefs/use.attributes (which the retrieval test does cover, line 154-156). Minor parity gap between the two receipt kinds' immutability assertions.

🟡 LOW Use-receipt input validation branches untested — src/knowledge-use-receipts.test.ts

createKnowledgeUseReceipt rejects non-integer or <1 selectedRank (knowledge-use-receipts.ts:312-314), invalid relation (668-680), and invalid consumer kind (606-616). Only the rank-not-returned path is tested. Add cases: selectedRank 0, selectedRank 1.5, relation 'invalid', consumer kind 'forged'.

🟡 LOW Verify-function schema/kind/algorithm and visibility-entry tamper branches untested — src/knowledge-use-receipts.test.ts

Tests exercise only the final receipt-digest mismatch (knowledge-use-receipts.ts:286-288, 389-391). Untested fail-closed branches: schemaVersion/kind/digestAlgorithm mismatch in both verify functions (lines 256-264, 357-365) and the visibility snapshot digest mismatch when entries are tampered while snapshotDigest is left alone (lines 265-270). These are the security-relevant deserialization checks; add one test per branch asserting the specific error message.

🟡 LOW assertKnowledgeRetrievalMatchesVisibility happy path and createdAt handling untested — src/knowledge-use-receipts.test.ts

Only the throwing path of assertKnowledgeRetrievalMatchesVisibility is exercised; the pass path (unchanged visibility) is never asserted. Also isoTimestamp's Date-object input and its undefined-means-now default are untested — the determinism test depends on callers always passing a string, which is worth pinning.

🟡 LOW Extra properties on retriever, evidenceRefs entries, and consumer are dropped before digest recomputation, so verify passes while the returned receipt carries digest-uncovered fiel — src/knowledge-use-receipts.ts

The verify path rebuilds retriever via normalizeRetriever(receipt.retriever) (line 279) and evidenceRefs via normalizeEvidenceRefs(receipt.evidenceRefs) (line 282, and line 385 for use receipts) before recomputing the digest, while results/visibility/top-level fields are digested raw. Probe confirmed: adding extraTampered to receipt.retriever or to an evidenceRefs entry leaves verifyKnowledgeRet

🟡 LOW Missing reasons field produces an opaque raw TypeError — src/knowledge-use-receipts.ts

result.reasons.map(...) dereferences reasons without an Array.isArray guard (the guard exists only in the verify-side validateReceiptResults, line 585). A caller passing a KnowledgeSearchResult lacking reasons gets Cannot read properties of undefined (reading 'map') instead of the module's usual ...reasons must be an array domain error. Cosmetic robustness nit, not a correctness bug.

🟡 LOW Non-string snippet silently recorded as empty string instead of failing loud — src/knowledge-use-receipts.ts

snippet: typeof result.snippet === 'string' ? result.snippet : '' coerces a buggy retriever's non-string snippet (null, number) to '' and issues a receipt attesting an empty snippet. Probe confirmed: snippet: 42 produces results[0].snippet === "" with no error. This masks a caller defect inside a supposedly immutable evidence artifact and contradicts the repo's no-silent-fallbacks doctrine. Fix: throw a labeled TypeError when snippet is present and not a string, matching the treatment of reasons[] entries via nonEmpty.

🟡 LOW Receipt 'proof' is a bare content hash, not keyed or signed — src/knowledge-use-receipts.ts

receiptDigest is canonicalCandidateDigest(material) with no secret or public key, and the docstring at line 59 calls the result 'immutable proof'. A content hash protects against accidental corruption and lets a verifier detect tampering only if the attacker cannot recompute the hash; any writer who can produce a receipt can forge one. This is fine for an internal runtime-owned audit trail (and the docs correctly scope what is and is not proven), but callers should not treat it as authentication against an untrusted actor. Threat-model note, not a defect.

🟡 LOW Snippet silently coerced to empty string instead of failing loud — src/knowledge-use-receipts.ts

snippet: typeof result.snippet === 'string' ? result.snippet : '' turns a missing/non-string snippet into ''. Every other field in this function is validated with an explicit TypeError, and the repo doctrine is 'no fallbacks, fail loud'. Snippet is display-only and deterministic, so impact is minor, but a caller that drops snippet gets a receipt that silently records empty text rather than surfacing the omission. Consider rejecting non-string snippet, or documenting the intentional default.

🟡 LOW Verify accepts internally-consistent receipts whose visibility contains duplicate origin+path entries that create rejects — src/knowledge-use-receipts.ts

createKnowledgeVisibilitySnapshot rejects duplicate ${origin}\u0000${path} identities, but verifyKnowledgeRetrievalReceipt only recomputes visibilityMaterial (positions + digest) and never re-checks uniqueness. Probe confirmed: a forged receipt with entries[0] duplicated at position 1, snapshotDigest and receiptDigest recomputed over the duplicates, passes verifyKnowledgeRetrievalReceipt. Impact is bounded because receipts are unsigned and content-addressed (README states they establish no authenticity), but verify is weaker than create on the same invariants, so a verifier gate accepts shapes the issuer API can never produce. Fix: extract the duplicate check into a shared validator and call it from visibilityMaterial so both paths enforce identical invariants.

🟡 LOW proto attribute key is silently swallowed by the prototype setter — src/knowledge-use-receipts.ts

normalized[name] = value is a plain assignment on an object literal, so a proto key that arrives as an own enumerable property (the exact shape JSON.parse produces) hits Object.prototype's proto setter; primitive values are ignored and the attribute never enters the receipt. Probe confirmed: attributes Object.assign(JSON.parse('{"proto":"evil"}'), {real: 1}) yields receipt attributes {real: 1} with no error. No prototype pollution is possible (values are restricted to string|number|boolean|null and finite-checked), but an evidence attribute silently disappears. Fix: build via Object.create(null) plus Object.defineProperty, or reject keys that fail Object.hasOwn after write.

🟡 LOW proto attribute key silently dropped from the receipt — src/knowledge-use-receipts.ts

normalized[name] = value where normalized = {} and name can be 'proto' invokes the Object.prototype setter; the value is not stored as an own property, so the attribute is absent from the frozen receipt and from the canonical material. Runtime-verified: { __proto__: 'x', real: 1 } yields attributes with only real. A caller that records a 'proto'-keyed attribute gets a receipt that provably does not contain it, with no error. Contrived key, but it is silent data loss in an immutable audit record. Fix: use Object.defineProperty or a null-prototype object, or reject the key.

🟡 LOW no size bounds on visibility/results/attributes despite 'bounded scalar attributes' doc claim — src/knowledge-use-receipts.ts

docs/knowledge-use-receipts.md calls attributes 'bounded scalar attributes', but normalizeAttributes (645-666) caps neither the number of keys nor string lengths, and neither createKnowledgeVisibilitySnapshot nor normalizeRetrievalResults caps entry/result counts. Each entry and result page is fully hashed (twice: snapshot digest + per-result digest), so unbounded inputs produce unbounded hashing work and unbounded receipt records. Only reachable from trusted internal callers today, so low, but a hard cap (e.g., max attributes/entries/results) would make the 'bounded' contract real.

🟡 LOW non-string snippet silently coerced to '' instead of failing — src/knowledge-use-receipts.ts

A retriever result with a non-string snippet is silently recorded as '' in the immutable receipt rather than rejected, masking a schema violation in the evidence record. This contradicts the repo's fail-loud doctrine (AGENTS.md: no silent zeros/defaults) and the module's own fail-closed posture elsewhere (finite scores, contiguous ranks all throw). Fix: if (typeof result.snippet !== 'string') throw new TypeError(...). Also resolves the create/verify asymmetry described in the medium finding.

🟡 LOW use receipt does not surface whether the selected page was invalidated — src/knowledge-use-receipts.ts

The visibility snapshot records invalidated per entry (line 201) and the page digest covers invalidation, but used (lines 326-332) copies only rank/pageId/origin/path/pageDigest, and no check prevents binding supports/extends to a page whose visibility entry is invalidated:true (runtime-verified). A provenance audit reading a use receipt cannot tell that the used page was invalidated at retrieval time without dereferencing the retrieval receipt's visibility entries. The receipts-only-record-facts design is legitim


tangletools · 2026-08-17T16:47:05Z · trace

@tangletools
tangletools dismissed their stale review August 17, 2026 16:47

Superseded by re-review — no blocking findings on latest commit.

tangletools
tangletools previously approved these changes Aug 17, 2026

@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.

✅ Approved — 38 non-blocking findings — a9028df0

Full multi-shot audit completed 6/6 planned shots over 6 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 6/6 planned shots over 6 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 6/6 planned shots over 6 changed files. Global verifier still owns final merge decision.

Full immutable report for this review: trace

Summary comment for this run: full summary


tangletools · 2026-08-17T16:47:05Z · immutable trace

Restore the 4.0.0 through 7.2.6 changelog entries that the receipt change deleted.
Remove the duplicate receipts section that sat below the License heading.
Import canonicalCandidateDigest from agent-interface in the receipts doc; Knowledge does not re-export it.

@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 — 2437bdcb

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

This approval is provisional. It rests on the audit running. If the audit cannot run — for example the CLI bridge rejects it — this approval is dismissed rather than left standing, so an unrun check never reads as a passing one.

tangletools · auto-approval · reason: drewstone_author · 2026-08-17T17:27:49Z

@drewstone
drewstone merged commit aa63827 into main Aug 17, 2026
2 checks passed

@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.

🟡 Value Audit — sound-with-nits

Verdict sound-with-nits
Coverage 2 of 2 lenses (value, usefulness)
Concerns 2 (2 weak-concern)
Heuristic 0.0s
Duplication 0.1s
Interrogation 360.5s (2 bridge agents)
Total 360.6s

💰 Value — sound-with-nits

Adds tamper-evident, content-addressed receipts that cryptographically bind the knowledge visibility→retrieval→use chain, built cleanly on existing repo primitives; ship.

  • What it does: Introduces src/knowledge-use-receipts.ts (749 lines, exported at src/index.ts:29): (1) knowledgePageDigest — canonical RFC 8785 content identity for a KnowledgePage; (2) createKnowledgeVisibilitySnapshot — frozen, ordered, digest-covered record of every OriginatedPage (here/inherited/shared) visible to one retrieval; (3) createKnowledgeRetrievalReceipt — proves one actor/retriever/query operated o
  • Goals it achieves: Makes the provenance chain the repo already cares about tamper-evident and independently verifiable. run-scoped.ts:3-14 states the doctrine — pages must carry origin labels because 'I established this', 'an earlier attempt believed this', and 'the lab curates this' are different provenance claims — but before this change nothing proved what a run could see, what its retriever actually returned, or
  • Assessment: Good on its merits and squarely in the codebase's grain. It extends rather than reinvents: OriginatedPage/PageOrigin from src/run-scoped.ts:22-28, KnowledgeSearchResult from src/types.ts:134, EvidenceRef from agent-eval/analyst, canonicalCandidateDigest/sha256DigestSchema from agent-interface. The digest-envelope pattern (material → content digest, verify recomputes) mirrors the existing selected-
  • Better / existing approach: None found — searched for existing equivalents before concluding. rg for 'receipt' across src found only cost receipts (src/memory/experiment/cost.ts), the selection receipt (kb-improvement), and attempt-log receipts — none bind retrieval-to-use. rag-answer-evidence.ts is a promotion gate over run metrics, not a provenance chain. No receipt envelope exists in agent-interface or agent-eval dist .d.
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error

🎯 Usefulness — sound-with-nits

A well-built provenance chain (visibility snapshot → retrieval receipt → use receipt) that closes a real gap this package's own docs identify, composed entirely from the codebase's established digest and evidence primitives, exported for its declared downstream caller (agent-runtime/product adapters

  • Integration: Library-package integration, not in-repo wiring: exported from the public barrel (src/index.ts:29), which is how every consumer reaches this package (README.md:87-119 quickstart + docs/knowledge-use-receipts.md document the full chain). The input types line up 1:1 with the package's two existing producers: createKnowledgeVisibilitySnapshot consumes exactly what RunScopedStores.loadChain() returns
  • Fit with existing patterns: Fits the grain rather than competing. It reuses the established identity primitives — canonicalCandidateDigest/Sha256Digest/sha256DigestSchema from agent-interface, EvidenceRef from agent-eval/analyst (knowledge-use-receipts.ts:1-6) — instead of inventing a new digest or evidence format, and follows the codebase's existing content-hash-of-canonical-material receipt idiom (same pattern as kb-improv
  • Real-world viability: Built for hostile inputs, not just the happy path: refuses results absent from the visibility snapshot (knowledge-use-receipts.ts:512-517), detects page mutation between snapshot and result materialization (:519-523), enforces contiguous unique ranks and finite/in-range scores (:496-552), rejects nested attribute values that would break canonical serialization (:654-661), omits undefined optionals
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

💰 Value Audit

🟡 Create-path and verify-path result validation are near-parallel and must stay in lockstep [maintenance] ``

normalizeRetrievalResults (src/knowledge-use-receipts.ts:482-554) and validateReceiptResults (src/knowledge-use-receipts.ts:556-589) enforce the same rules (rank contiguity, duplicate pages, visibility joins, finite scores, [0,1] bounds) as two hand-written implementations. If one is extended (e.g., a new score field) and the other isn't, verify() silently accepts material create() would reject. Consider extracting the shared per-row checks into one helper both paths call, keeping only the norma

🎯 Usefulness Audit

🟡 No helper produces OriginatedKnowledgeSearchResult[]; every caller hand-rolls the origin join [ergonomics] ``

The receipt API's two producer halves exist (RunScopedStores.loadChain() at run-scoped.ts:110 and searchKnowledge at search.ts:48) but searchKnowledge returns hits without origin, and grep shows no function in the package that searches a run chain and attaches origins. Each adopter must build an index from chain pages, run searchKnowledge, and join origins back by (origin, path) — exactly the join the receipt then re-validates. A small searchRunChain(stores, runId, query)-style helper here would


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260817T173423Z

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