feat(audit): okf audit — corpus-level trust, freshness and lifecycle query - #49
Conversation
Adds the design for a new CLI verb, core query type and agent tool that answer "which concepts are past stale_after and were never verified by a human?" across a whole bundle -- the question OKF v0.2 makes askable but ships no query surface for. Three units: ConceptAudit (core, shared computation), the `okf audit` verb (text + --json), and the read-only okf_audit agent/MCP tool. No new frontmatter fields: everything derives from v0.2 sections 5.3-5.5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven defects found by re-reading the spec against the code: - AuditQuery is a record struct whose generated equality compares the trust set by reference; the type advertised a value equality it does not have. Documented as a remark, not relied on. - No way to select the whole corpus: without filters the verb reports the stale worklist, so --json findings are not the corpus. Documented the three-tier idiom and why no --all is added. - audit-v02.exitcode would be a golden for a constant (exit is always 0); dropped, asserted inline like Info_output_matches_golden. - The set of mode-switching flags was never enumerated: --as-of and --json must not switch to query mode, which the golden depends on. - query.trust JSON ordering was unspecified; pinned to ladder order so the document is reproducible with several tiers. - The position of the audit line in the usage text was unpinned while test 24 checks it; pinned right after validate. - CLAUDE.md and ROADMAP.md are concurrently modified by another session: noted as end-of-branch work to avoid a rebase conflict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All five verified against the code before accepting: - DateOnly has no (s, format, provider, out) overload, so the parsing contract as written would not compile. Pinned the full five-argument call, matching Lifecycle.From. - --status had two incompatible semantics: scalar in AuditQuery, the JSON and the tool, but described as a deduplicated list in the parsing rules. Settled on scalar; --trust is the only multi-valued flag. - The "unreadable files" test described an impossible state: I/O, permission and non-UTF-8 failures throw BundleLoadException and abort the load, while ParseErrors only collects DocumentParseException and ConceptIdException. Reframed around an invalid-frontmatter document. - The agent tool would have fallen back to SystemClock, bypassing the existing UtcNow/Today seam that ReadConcept and Search already use, making its output depend on the run date. It now passes Today through a private IOkfClock adapter, with a test pinning today == stale_after. - "No fixture created or modified" contradicted the new golden files; stated precisely: no fixture bundle and no existing golden change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…umptions The plan covers the spec in six tasks (core query, CLI text modes, JSON, goldens, agent tool, docs), each TDD with real test and implementation code rather than descriptions. Writing it surfaced that the spec was written against the wrong base. FlagValue and Positional's valuedFlags parameter do not exist on origin/main -- both come from the unmerged viewer branch, which added them for `render --out`, and the spec cited that as existing precedent. The plan now opens with Task 0 adding them, copied verbatim from that branch so the eventual merge conflict resolves to identical code, and the spec no longer claims a precedent this base does not have. Same for the subcommand count: six here, not seven. Self-review also caught, in the plan itself: a nullable-narrowing pattern that would not compile under warnings-as-errors, a cap test that would have counted the "concepts:" header as a finding, two unused vocabulary members, and a nested code fence that broke its own block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All four verified against the code first: - The tool bypassed RunTool, the guard every bundle-loading tool uses to turn OkfException into an "Error: ..." string. A bundle deleted after construction would have thrown out of a function tool instead of returning an error. Now wrapped, with a test that deletes the directory before the first load. - CmdAudit resolved the positional before validating flag values, so `okf audit --as-of` reported "missing <bundle>" instead of naming the unvalued flag. Reordered, with a Theory over all four valued flags. - Both renderers spelled the trust/status labels as literals, which is precisely the drift AuditVocabulary was introduced to prevent. They now read every label from it; only the display ORDER stays local (the report shows the strongest tier first). - Task 1's interface block still advertised TrustTierNames/StatusNames, removed from the implementation during the previous review. Also folded in two design contracts the audit showed were missing from the spec rather than from the plan: why StalePolicy and AuditQuery.StaleOnly coexist (restitution vs worklist -- opposite questions), and why CLI input parses strictly while frontmatter parsing stays permissive (a typo absorbed into "stable" would make a worklist lie). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
origin/dev carries the merged viewer work (PR #48), so FlagValue and Positional's valuedFlags parameter now exist: Task 0 added them and becomes a no-op, exactly as the plan anticipated. Removed it, restored the spec's `render --out` precedent, and corrected the subcommand count (seven here, eight with audit). Baseline after the merge: 987 tests, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `okf audit`, wired to the ConceptAudit engine shipped earlier: report mode (bundle-wide trust/status/staleness summary plus a worklist), query mode (--stale/--trust/--status/--type select a bare concept-line listing), and --json (source-generated, AOT-safe) for both. Implemented as one unit per the task-2-3 brief's controller ruling, which forbids the intermediate --json stub the original two-task split called for.
Hand-authored against the v0.2 spec (audit has no upstream reference
binary), verified by running okf4net's own audit against
tests/fixtures/okf_v02 before saving. Both goldens matched the
predicted text exactly.
The JSON parity test's Windows-only backslash normalization needed a
fix along the way: r.Out is serialized JSON, where a native backslash
separator is escaped to the two-character sequence `\` in the JSON
text itself. The originally drafted `r.Out.Replace('\', '/')`
replaced each of those two characters individually, turning one path
separator into "//" instead of "/". Matching on the two-character
escaped sequence instead of the bare char fixes it.
Two CLI-level review gaps closed: - Audit_report_mode_prints_none_when_nothing_is_stale: report mode's "needs attention: none" branch (WriteAuditReport's empty-Findings early return) had no test exercising it. - Audit_finding_line_reports_no_stale_after_when_the_field_is_absent: FormatAuditFinding's "no-stale-after" freshness label (emitted when a concept has no stale_after at all, as metrics/legacy does) was never asserted. Also corrects JsonOutput's class doc comment, stale since WriteAudit was added: it still credited only validate/info.
Adds a PinnedClock IOkfClock adapter over the existing UtcNow/Today seam and a RenderAudit helper (deliberately separate from the CLI's golden-locked renderer) so okf_audit surfaces ConceptAudit's trust/freshness/lifecycle signals to agents, capped at 20 findings and never throwing (RunTool guard). Not added to WriteToolNames: it is read-only. Updates the existing tests that hard-code OkfBundleTools' tool count/order and the MCP server's read-only-mode tool count now that an 11th unconditional tool exists, and syncs the two READMEs' tool tables/counts accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lary spellings
- RenderAudit now takes staleOnly and only headings the worklist "needs
attention" when the selection IS the stale worklist; otherwise it uses
the neutral "selected" heading. Previously okf_audit(stale: false) on a
perfectly healthy bundle printed "needs attention (N):" over concepts
that were not stale at all -- a factual misstatement the agent would
relay verbatim.
- Move the freshness token ("stale <date>"/"fresh <date>"/"no-stale-after")
into AuditVocabulary.Freshness, and the "--trust"/trust comma-list
grammar into AuditVocabulary.TryParseTrustTiers, so neither is spelled
out twice between the CLI renderer and the agent tool renderer. The
CLI's message on an unknown trust tier is unchanged (still pinned by a
CliTests assertion); the tool still returns its own usage message.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDM9Aozt9YciNJ3oncrFxG
IsFiltered's doc read as "did the caller pass a filter flag?", but it is true for the CLI's own report-mode query (new AuditQuery(StaleOnly: true)), which no flag produced. Reword it to say precisely what the property answers -- whether the query constrains the selection -- without touching its behavior; All and IsFiltered both stay as mandated by the design spec (§3.1). Pin both ends with a unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TDM9Aozt9YciNJ3oncrFxG
…ters Only --status deprecated (empty result) and a positional-order [Theory] that ignores stdout covered these two flags before this, so transposing Type and Status in ParseAuditQuery would leave the whole suite green. Add assertions on tests/fixtures/okf_v02 (2 concepts, both type: Metric, both resolving to status: stable): --type Metric selects both lines, --type metric (lowercase) selects none -- pinning the documented ordinal, case-sensitive rule at the CLI boundary -- and --status stable selects both lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TDM9Aozt9YciNJ3oncrFxG
- README.md: GetTools() returns eleven tools unconditionally (was "ten"),
plus a twelfth -- okf_run_computation -- conditional on an attestation
orchestrator, matching the table 18 lines below which already said
eleven/twelfth. Also quote the CLI's own --help wording for the audit
verb line ("Report trust, freshness and lifecycle across the bundle")
instead of paraphrasing it, matching every other verb line in that block.
- src/OKF4net.Agents/README.md: "The nine tools" was stale even before
this branch and is now three tools behind (okf_audit, okf_get_computation,
and the conditional okf_run_computation). Correct the count and list to
match GetTools().
- CLAUDE.md: add audit to the CLI verb list, and add one line under
src/OKF4net/ pointing at ConceptAudit as the single shared corpus-level
query behind okf audit and okf_audit -- the same "do not fork this"
guidance already given for ConceptSearch.
- ROADMAP.md: record that okf audit shipped, in the same style as the
"static render shipped" bundle-viewer entry.
- CHANGELOG.md: bold the okf audit entry to match its feature-scale
neighbors (**okf render ...**, **okf validate/okf info gain a --json
flag**), and remove the blank line that had made the Added list loose
around it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TDM9Aozt9YciNJ3oncrFxG
Every other okf_audit test calls the C# method directly, so nothing covered the framework's JSON argument binding: a model passes `stale` as a boolean and `trust` as a comma-separated string, and the binding has to reach the method's parameters for the filters to take effect. The bundle is built inline with the UtcNow seam pinned, so one concept is stale AND unverified while another is equally stale but human-reviewed. A binding failure that dropped the trust argument would list both — verified by mutating the script's trust argument to null and watching the test fail, then restoring it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
okf_audit was already reaching the sample's agent — Program.cs filters GetTools() by WriteToolNames, so a new read-only tool is exposed with no code change. What was missing is that nothing told the agent, or the reader, that a corpus-level question is now answerable in one call: a model asked "what is stale?" would browse concept by concept, which is the very behaviour the audit surface exists to replace. Adds one sentence to the system instructions, a "Questions worth asking" section splitting retrieval questions from corpus-level ones, and a note in "What it does" on how the two use the tools differently. The section is honest about the demo: nothing in this bundle is stale yet, so the trust question is the one that bites today, and the bundle starts reporting seven stale concepts on 2027-01-01. Also realigns the sample's Microsoft.Agents.AI pin from 1.15.0 to 1.17.0. That break predates this branch: a dependabot bump moved OKF4net.Agents to 1.17.0 and the sample's own pin stayed put, which NU1605 rejects as a downgrade. Nothing caught it because samples/ is outside OKF4net.sln and outside CI — the sample could not be built at all before this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Fix CLI option parsing around consumed values and the -- separator; update the plan’s stale expected heading.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a corpus-level okf audit query for trust, lifecycle, and staleness across CLI, Agents/MCP, documentation, and samples.
Changes:
- Adds shared audit computation and vocabulary handling.
- Adds CLI text/JSON output with filters and golden fixtures.
- Exposes the read-only
okf_audittool with tests and documentation.
File summaries
| File | Reviewed scope / final note |
|---|---|
tests/OKF4net.Tests/Mcp/OkfMcpServerTests.cs |
MCP registration coverage |
tests/OKF4net.Tests/GoldenParityTests.cs |
Golden output parity |
tests/OKF4net.Tests/CliTests.cs |
CLI behavior and output |
tests/OKF4net.Tests/AuditTests.cs |
Core audit logic |
tests/OKF4net.Tests/Agents/OkfBundleToolsTests.cs |
Read-only tool filtering |
tests/OKF4net.Tests/Agents/OkfAuditToolTests.cs |
Agent audit behavior |
tests/OKF4net.Tests/Agents/AIFunctionExposureTests.cs |
Tool exposure |
tests/OKF4net.Tests/Agents/AgentIntegrationTests.cs |
End-to-end agent binding |
tests/fixtures/README.md |
Fixture documentation |
tests/fixtures/golden/audit-v02.out |
Text golden output |
tests/fixtures/golden/audit-v02.json |
JSON golden output |
src/OKF4net/Audit.cs |
Shared audit model and computation |
src/OKF4net.Mcp/README.md |
MCP documentation |
src/OKF4net.Cli/OkfCli.cs |
Moderate finding: option parsing must skip consumed values and honor -- |
src/OKF4net.Cli/JsonOutput.cs |
Audit JSON serialization |
src/OKF4net.Agents/README.md |
Agent documentation |
src/OKF4net.Agents/OkfBundleTools.cs |
Agent audit tool |
samples/acme-retail-agent/src/AcmeRetailAgent/Program.cs |
Sample agent instructions |
samples/acme-retail-agent/src/AcmeRetailAgent/AcmeRetailAgent.csproj |
Dependency alignment |
samples/acme-retail-agent/README.md |
Sample usage guidance |
ROADMAP.md |
Roadmap update |
README.md |
Project usage documentation |
docs/superpowers/specs/2026-08-21-okf-audit-design.md |
Audit design specification |
docs/superpowers/plans/2026-08-21-okf-audit.md |
Nit: update expected heading to selected (2): |
CLAUDE.md |
Architecture guidance |
CHANGELOG.md |
Changelog entry |
Review details
Suppressed comments (7)
docs/superpowers/specs/2026-08-21-okf-audit-design.md:311
- The French word
presentshould be accented asprésenthere.
JSON doit pouvoir distinguer « champ absent » de « champ present mais illisible »
docs/superpowers/specs/2026-08-21-okf-audit-design.md:556
- The French verb
assèreis misspelled; it should beassure.
la surface de fixtures. Le test l'assère en ligne (`Assert.Equal(0, r.Code)`),
samples/acme-retail-agent/README.md:67
- Because
okf_auditdefaultsstaleto true, a human-verification query otherwise lists only concepts that are both stale and unverified.skills/run-on-bqhas nostale_after, so the example as written does not isolate it; please state that this question must be run withstale: false(or show the exact prompt/tool arguments).
- *Which concepts have never been verified by a human?* — the interesting
one today: eight of the nine concepts carry a `human:` verifier, so this
isolates `skills/run-on-bq`.
src/OKF4net.Agents/OkfBundleTools.cs:478
- The function metadata still describes the output as concepts “needing attention” even when callers pass
stale: falseor another non-staleness filter; those results can include fresh concepts and are intentionally headedselected. Since this description is what the model sees when deciding how to use the tool, please describe these as concepts selected by the filters instead.
[Description("Audit the bundle's trust, freshness and lifecycle signals: counts by trust tier and status, plus the concepts needing attention. Filter with stale/trust/status/type.")]
src/OKF4net.Agents/OkfBundleTools.cs:231
- Adding
okf_audithere leaves the publicweb/reference out of sync: the Home CLI table anddocs/Cli.tsxstill describe seven commands, while Home/Agents/MCP still describe ten tools and omit the audit surface. Please update those public pages (or explicitly defer the feature from the site) so the shipped API and its documentation do not contradict each other.
AIFunctionFactory.Create(Audit, "okf_audit"),
tests/OKF4net.Tests/Agents/AIFunctionExposureTests.cs:31
- The exposure suite now lists
okf_audit, but it does not assert the new function's generated schema. The end-to-end test only bindsstaleandtrust, so a regression that drops or renames the optionalstatus/typeparameters (or changes their requiredness) would still pass; add the same schema/optional-parameter coverage used for the other exposed tools.
"okf_audit",
tests/OKF4net.Tests/Mcp/OkfMcpServerTests.cs:95
- The MCP tests only prove that
okf_auditappears in the tool list; they never invoke it throughCallToolAsync. Because this tool adds multiple optional arguments and the MCP adapter performs its own schema/argument conversion, a binding or invocation regression could pass while the Agent-level test remains green. Add an MCP call using the audit filters and assert its returned finding.
"okf_append_log", "okf_audit", "okf_browse", "okf_changes_since", "okf_get_computation",
- Files reviewed: 26/26 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| var text = ToolsOver(tmp, new DateOnly(2026, 8, 21)).Audit(stale: false); | ||
|
|
||
| Assert.Contains("needs attention (2):", text); |
| } | ||
|
|
||
| return new AuditQuery( | ||
| HasFlag(args, "--stale"), |
What
Adds
okf audit, a corpus-level query over a bundle's OKF v0.2 trust (§5.3), lifecycle (§5.4) and staleness (§5.5) signals. It answers, in one command, the question v0.2 makes askable but ships no query surface for:Four surfaces, one computation:
src/OKF4net/Audit.cs—ConceptAudit(the shared query),AuditQuery,AuditFinding,AuditReport, andAuditVocabulary, the single spelling of the trust/status/freshness vocabularies so no two layers can drift.src/OKF4net.Cli/— theokf auditverb. Without a filter flag it selects exactly what--staleselects and prints a summary plus the worklist; with any filter flag it prints one line per concept, so the output pipes.--jsonalways emits the full document.tests/fixtures/golden/audit-v02.{out,json}— byte-exact goldens, hand-authored and verified against the spec text (no upstream CLI to capture), pinned with--as-ofso they cannot drift with the calendar.src/OKF4net.Agents/— the read-onlyokf_audittool, also served over MCP.Counts always describe the whole bundle while
findingsdescribes the selection:auditis a worklist, not an inventory.Design
Spec and plan are in the diff:
docs/superpowers/specs/2026-08-21-okf-audit-design.mdanddocs/superpowers/plans/2026-08-21-okf-audit.md.Deliberate decisions worth reviewing:
auditis notvalidate.validateanswers "is this bundle conformant?" and exits non-zero when it isn't;auditreports and always exits 0. No existingvalidate/infooutput changes, so no existing golden moves.Lifecycle.Fromresolves an unknownstatustostable(§5.4, §11);--status retiredfails. A typo silently absorbed intostablewould make a worklist lie.StalePolicyandAuditQuery.StaleOnlycoexist. They answer opposite questions: "should I surface this to a consumer?" versus "is this on my worklist?".Verification
TreatWarningsAsErrors,dotnet format --verify-no-changesclean.PackageReferenceadded to the zero-dependency projects.--jsononbundles/acme_retail, the error paths, and a realokf-mcpstdio session callingtools/callforokf_audit.AgentIntegrationTestsnow drivesokf_auditthrough the framework's real function-invoking pipeline, which is what covers the JSON argument binding; the test was mutation-checked (drop thetrustargument → it fails).Also in here
The
samples/acme-retail-agentsample already exposed the new tool (it filtersGetTools()byWriteToolNames), but nothing told the agent a corpus-level question was answerable in one call. Adds that to the system instructions plus a "Questions worth asking" section.That work surfaced a pre-existing break: a dependabot bump moved
OKF4net.AgentstoMicrosoft.Agents.AI1.17.0 while the sample's own pin stayed at 1.15.0, which NU1605 rejects as a downgrade — so the sample could not be built at all. Realigned. Nothing caught it becausesamples/is outsideOKF4net.slnand outside CI.🤖 Generated with Claude Code
https://claude.ai/code/session_01TDM9Aozt9YciNJ3oncrFxG