Skip to content

docs(plugin-audit): document the os serve opt-in, and rule out a config-derived audit options helper - #10450

Merged
os-zhuang merged 7 commits into
mainfrom
claude/issue-9863-audit-plugin-boot-options
Aug 21, 2026
Merged

docs(plugin-audit): document the os serve opt-in, and rule out a config-derived audit options helper#10450
os-zhuang merged 7 commits into
mainfrom
claude/issue-9863-audit-plugin-boot-options

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Fixes#9863

The answer is NO — os serve should not grow appAuditPluginOptions(config)

#9863 was filed as a reachability gap and reads like "a helper is missing, add it." It is not that. serve.ts already argues the question out by issue number, and #9864's ruling (2026-08-19, option B) turned the mechanism it depends on into a declared, pinned contract. What was left open is the card's own sentence:

whether os serve should grow an appAuditPluginOptions(config) helper like its SecurityPlugin sibling above, rather than reaching the capability only through a supersede.

Ruled no, on four measurements. The reasoning is recorded at the registration site, the reachability story is now documented where readers meet it, and the three facts the ruling rests on are pinned by a new contract test.

Premise check first

Both halves of the card still hold on origin/main @ e502a6a8e: new AuditPlugin() is bare at serve.ts:2535, and appAuditPluginOptions exists nowhere in the tree. The card's zero-call-site claim also re-derives — grep -rn "readAudit" packages/ examples/ apps/ finds no hit outside packages/plugins/plugin-audit/ except the two prose mentions in serve.ts's own comment. Positive control on the same paths and the same command shape: AuditPlugin matches 20+ files there, including seven dogfood suites.

1. #7001's reason does not transfer

appSecurityPluginOptions exists "precisely so the CLI boot and @objectstack/verify's bootStack cannot disagree". That is the strongest available argument for the helper, and it is testable — so it was tested rather than reasoned about:

  • bootStack constructs noAuditPlugin. Its eight kernel.use(...) sites were enumerated; audit reaches it only through caller-supplied BootOptions.extraPlugins, where the caller passes whatever options it wants (all seven dogfood suites do exactly that).
  • @objectstack/verify's package.json does not depend on @objectstack/plugin-audit at all, so it cannot grow such a registration by accident.

Audit therefore has exactly one boot path with an opinion. There is no disagreement for a shared helper to close, and no parity contract of the serve-verify-security-parity shape is even writable. The symmetry argument is the weak one, as the dispatch anticipated.

2. The helper would have nothing to read

appSecurityPluginOptions(config) derives from config.permissions — an already-declared spec surface, reading isDefault off permission sets the author writes anyway. There is no audit key in packages/spec/src/stack.zod.ts and no object-metadata audit field. An audit helper does not read a declared surface; it mints one.

3. That surface is the shape #8992's ruling already refused

AuditPluginReadAuditOptions' own header records the maintainer's 2026-08-16 ruling, and both the README and the docs page state its consequence: there is no object-metadata key because "a declarable metadata key can be set on an object in a deployment that never installs this plugin, producing metadata that reads as audited and records nothing — and on a compliance surface, a declaration a reviewer mistakes for coverage is worse than an absent feature."

A config.audit key in objectstack.config.ts reproduces that failure exactly. The CLI's import is best-effort (try { await import('@objectstack/plugin-audit') } catch { /* optional */ }), so the key survives in a deployment where the package is absent — declared, and enforcing nothing.

And it would be worse than the metadata-key version here, because of a fact this PR is the first to write down: the registration is auth-gated. It sits inside 5d. Auto-register AuthPlugin (and paired Security/Audit), under if (!hasAuthPlugin && tierEnabled('auth')), past the host-kernel and auth-secret guards. An app that supplies its own AuthPlugin — or a production boot with no auth secret — never reaches that line. A config key wired there would take effect depending on unrelated auth conditions, on a compliance surface.

4. It would be a second surface that silently loses to the first

This is the semantics change the card's SECOND constraint names. Today the supersede resolves unconfigured loses to configured by position. With a helper, both instances carry options — and the app's plugins entry still registers later, so its instance still wins. config.audit would be silently discarded for precisely the apps most likely to set both. Measured pull for that new footgun: zero readAudit call sites repo-wide.

Is the harm functional or ergonomic? — Ergonomic, and the docs were the defect

The card's "a shipped compliance capability its own CLI cannot reach" is no longer true, and this PR's one factual correction is the proof. content/docs/permissions/record-view-auditing.mdx said:

The CLI's os serve registers AuditPlugin with no options, so a stack served that way has record-view auditing off and no knob to turn it on.

That clause stopped being true when #9864 declared and pinned the contract. The knob is the stack's plugins array. The page and the published README now both spell the path, show the config, and name the Plugin superseded: 'com.objectstack.audit' boot line as the opt-in working rather than a misconfiguration — including the caveat that a stack supplying its own AuthPlugin never sees that line, because there is no second instance to supersede.

#9864 is engaged, not routed around. Nothing here changes the kernel contract, its statement in packages/core/src/plugin-registration.ts, or plugin-registration.contract.test.ts. The CLI still registers exactly as it did; the ruling declines to add a surface, which is the conservative direction.

What is pinned, and the ablation for each

New: packages/cli/src/commands/serve-audit-registration.contract.test.ts — a source scan in the shape of this directory's two existing parity scans. Each assertion was ablated on disk, the mutation confirmed by grep counts (never by an editor's exit code), and both subject files restored byte-identically (sha256 match + clean git status).

Pinned factAblationObserved
CLI constructs AuditPluginonce, with no options — the rulingnew AuditPlugin()new AuditPlugin(appAuditPluginOptions(config))2 failed / 2 passed — expected [ 'appAuditPluginOptions(config)' ] to deeply equal [ '' ]
Registration sits above the stack plugins loop, and inside the auth gatemoved the audit try block below the loop, to just above the capability resolver (2535 → 2827, loop at 2543)1 failed / 3 passed — expected 68113 to be less than 60460
bootStack has noAuditPlugin opinionadded kernel.use(new AuditPlugin()) to harness.ts1 failed / 3 passed — expected [ '' ] to deeply equal []

The ordering pin is the one the card's FIRST constraint asked for: serve.ts called that order load-bearing, and until now nothing went red on its inversion — which would silently turn record-view auditing back off for every deployment that had opted in.

The scan walked into #9367 first, which is why it uses the shared masker

The first draft copied the private two-regex stripComments that this directory's two older parity scans still carry, block pass first. That is exactly the defect class #9367 named, and serve.ts is exactly its example: the 5d. header contains the route wildcard /api/v1/auth/*, whose /* opens a phantom block comment running to the next real terminator ten lines below inside import(/* webpackIgnore: true */ …) — deleting the hasAuthPlugin computation and the auth gate this scan measures against. Measured on this pair: the naive strip keeps 1895 code-bearing lines of serve.ts, maskComments keeps 2098.

It surfaced only because this scan asserts its anchors exist (the auth-gate anchor … is gone from serve.ts); a scan whose anchors happened to sit elsewhere would have passed. So this file now calls maskComments from scripts/js-comment-mask.mjs — the repo's one answer since PR #9445 — which also blanks rather than deletes, so the ordering assertion compares offsets into the real file (line count identical, 4638 → 4638). Masker and naive strip were cross-checked to agree on all four anchor counts across both subjects, which is what rules out #10427's open desync for this pair.

Three findings from that detour, all filed unassigned, none fixed here:

Verification

Full derived gate union re-run at final head 57e4e5e5f (node scripts/pm/dispatch-gates.mjs, no args — 21 path-matched families + 5 convention-triggered; re-derived after the roster and turbo.json edits added two paths, and the family set came back identical). Exit codes captured before any pipe; verdicts quoted from each gate's own output.

  • All 21 path-matched families green, plus the convention-triggered check:type-check-coverage, check:type-check-debt, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:i18n; plus check:nul-bytes and check:published-readme-exports.
  • pnpm --filter @objectstack/cli testTest Files 139 passed (139) · Tests 1529 passed (1529) (139 = every *.test.ts on disk, so the new file is in the run).
  • pnpm --filter @objectstack/plugin-audit testTest Files 18 passed (18) · Tests 300 passed (300).
  • pnpm --filter @objectstack/cli --filter @objectstack/plugin-audit typecheck — both echoed tsc --noEmit then typecheck: Done (script names verified echoed, so neither was a zero-match silent pass). This is also the proof that the @ts-expect-error on the untyped .mjs import is a real suppression rather than a phantom: an unused directive is itself a tsc error, and tsc is clean.
  • check:type-check-debt--re-measure: OK — 33 ledger entr(ies) re-measured in 251.4s, 1924 raw tsc error(s) total, none above its recorded number.
  • check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.

Clause ②: not reachable — this change touches no packages/spec/src/** path. git diff --name-only origin/main...HEAD covers .changeset/, content/docs/permissions/, packages/cli/src/commands/, packages/plugins/plugin-audit/README.md, scripts/check-cross-package-test-inputs.mjs and turbo.json only.

Changeset: owed and present. packages/plugins/plugin-audit's README is in its published files, so this is a user-visible change to a published artifact — skip-changeset would be wrong here. packages/cli gains only a comment and a test, so its runtime behaviour is unchanged and it is not in the changeset.

⚠️One caveat, corrected rather than smoothed over. Two ratchet gates failed intermittently on packages/spec/src/migrations/registry.ts:0:0 — Parsing error: Maximum call stack size exceeded, a file this branch does not touch. The first read of this was container memory pressure; that was wrong and is retracted. Measured across repeated runs in one worktree at one commit: check:slot-lookup failed 4 of 11, check:query-options-erasure1 of 5, with 11.5 GB free and load average 1.5 at the moment of a failure, and reproducing both with and without NODE_OPTIONS=--max-old-space-size=4096. Filed as #10451. The green runs quoted above are real runs, not retries of a verdict — slot-lookup reports ratchet holds: 107 unswept site(s) in 25 file(s), none new, and every file in the population parsed, and query-options-erasureratchet holds: 67 unswept non-test site(s) in 17 file(s), none new. Both gates behave correctly when they do fail: they refuse to report a number they did not measure.


Generated by Claude Code

…fig-derived audit helper (#9863)
The docs page for record-view auditing told readers the capability had "no knob
to turn it on" under `os serve`. That stopped being true when #9864 declared and
pinned the duplicate-registration contract: a configured `AuditPlugin` in the
stack's `plugins` array supersedes the CLI's option-less instance by name.
Both the page and the published README now spell that path.
#9863's open question — whether `os serve` should grow an
`appAuditPluginOptions(config)` helper mirroring its `SecurityPlugin` sibling —
is ruled NO, with the reasoning recorded at the registration site and pinned by
`serve-audit-registration.contract.test.ts`. No runtime behaviour changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…he scan never reads
`check:cross-package-test-inputs` takes any quoted path literal without parsing,
so a decorative mention of its own filename in a JSDoc block demanded a
declaration for a file this test never opens — which would have put cli's whole
suite on every edit of that gate. The comment now names the runnable script and
records why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/plugin-audit, touching 1 documentable anchor(s).

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 4330b7db7e1c0dbed6fea8941dffa2c27603e302.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-audit/README.md) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4330b7db7e1c0dbed6fea8941dffa2c27603e302packageMentionDocs.

Which tree this was computed on

This run read content/docs from f46e5cadaadb63e91078a417b43c3dec14613c18 — the merge of head 1e997a89a3e037b067b527ef7510d9b43cc61290 into base 4330b7db7e1c0dbed6fea8941dffa2c27603e302, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f46e5cadaadb63e91078a417b43c3dec14613c18 && git checkout f46e5cadaadb63e91078a417b43c3dec14613c18
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4330b7db7e1c0dbed6fea8941dffa2c27603e302 1e997a89a3e037b067b527ef7510d9b43cc61290 && git checkout -B drift-repro 4330b7db7e1c0dbed6fea8941dffa2c27603e302 && git merge --no-ff 1e997a89a3e037b067b527ef7510d9b43cc61290
node scripts/docs-audit/affected-docs.mjs --json 4330b7db7e1c0dbed6fea8941dffa2c27603e302

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4330b7db7e1c0dbed6fea8941dffa2c27603e302 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
…re the read
The first draft of `serve-audit-registration.contract.test.ts` carried a private
two-regex `stripComments` copied from this directory's older parity scans, block
pass first. That is the defect class #9367 named: `serve.ts` has the route
wildcard `/api/v1/auth/*` in a line comment, whose `/*` opens a phantom block
comment running to the next real terminator ten lines below — deleting the
`hasAuthPlugin` computation and the auth gate this scan measures against
(1895 code-bearing lines survive the naive strip, 2098 survive the masker).
`scripts/js-comment-mask.mjs` is the repo's one answer to that question, and it
blanks rather than deletes, so the ordering assertion compares offsets into the
real file. Masker and naive strip were cross-checked to agree on all four anchor
counts across both subjects, which is also what rules out #10427's open desync
for this pair.
The import escapes the package, so it is declared for `@objectstack/cli` in the
cross-package roster and hashed by `@objectstack/cli#test`. The gate did not
demand it — its literal collector does not recognise an escaping relative import
specifier — and that blind spot is filed separately rather than relied on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

CI triage (PM):Lint & Repo Gates is red here for a reason that is not this PR's.

✗ check-slot-lookup-ratchet: 1 parse failure(s) inside the population this gate measures:
• packages/spec/src/migrations/registry.ts:0:0 — Parsing error: Maximum call stack size exceeded

This PR touches packages/plugins/plugin-audit/** and packages/cli/** — nothing that can influence how that file parses. ⭐ The same job's log shows pnpm lint running withnode --stack-size=4000 and passing, immediately before check:slot-lookup failed without it: the invocation asymmetry visible in one log.

Root cause is #10449, now dispatched — the ratchets lint in-process via the ESLint Node API and never get the CLI entry's stack headroom. Intermittent at ~25–35% per run, measured on #10451, which this PR's own dev seat filed while running the derived gate union.

⇒ Not pushing anything at it from here. Holding until #10449 lands, then re-checking.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — verified against the diff, not the report. ⭐ Accepted.

The constraint held, and not by taking my word for it. The brief said ⛔ no appAuditPluginOptions(config) helper because the supersede is a declared contract under #9864's option B. You didn't cite that ruling as the reason — you re-derived it and gave four independent measurements, any one of which would carry it alone: #7001's rationale doesn't transfer (bootStack constructs no AuditPlugin, so there is only one boot path with an opinion and no disagreement to close); the helper would have nothing to read (no audit key in the stack schema, where appSecurityPluginOptions reads an existing config.permissions); minting that surface is the shape #8992 already refused on 2026-08-16 — config that reads as audited and records nothing; and it would be a second surface silently losing to the first by the very contract above.

And you found something the card didn't know. The pair is registered inside the 5d. Auto-register AuthPlugin (and paired Security/Audit) branch, so it is auth-gated — an app supplying its own AuthPlugin, a production boot with no auth secret, or a host kernel never reaches that line, and there the app's plugins entry is the only AuditPlugin with no supersede at all. That reframes the whole card: "the supersede is how the opt-in survives one particular boot shape; it is not the mechanism the opt-in is built on." Nothing had written that down.

What I checked hardest, and what passed:

  • ✅ The ordering guarantee is no longer prose. serve-audit-registration.contract.test.ts (+225) fails on the inversion — the comment used to be the only thing holding it.
  • Changeset present.packages/cli is published and this moves runtime.
  • check-cross-package-test-inputs.mjs is voluntary widening, not weakening — and you say so plainly: the gate did not demand the declaration (its literal collector recognises path-shaped reads, not relative import specifiers), you declared it by hand because the coupling is real, and you filed the collector's blind spot separately instead of quietly benefiting from it. "Widening a radius by hand is never the reason not to" is the right principle.
  • ✅ No content/docs/releases/**. The drift bot's four release-owned pages are pages that document this, not pages changed.

The one objection I came to raise, you had already closed. Wiring maskComments in makes this test's verdict a function of scripts/js-comment-mask.mjs — which is the subject of #10427, whose own header claims it "cannot fabricate a lead" and which I measured fabricating on 15 of 16 divergent files. Your docblock names #10427, states neither subject file is among the 16, and — the part that matters — backs it with a positive control rather than an assertion: masker and naive-strip agreeing on all four anchor counts, plus line count preserved 4638 → 4638. Cross-checking against a second independent instrument is the correct answer to "your instrument is documented-unreliable." ⚠️ It stays a live coupling: if serve.ts or harness.ts ever grows a nested template inside ${…}, that cross-check is what will catch it — worth keeping when this test is next touched.

Status: held, and not for anything in this diff

Lint & Repo Gates is red on #10449 — the ESLint ratchets lint in-process and never get pnpm lint's --stack-size=4000, so registry.ts trips the default stack ~25–35% of runs. Detail in my earlier comment. That card is dispatched and in flight.

⛔ I am deliberately not re-running to chase a green. This defect is intermittent, so a re-run would very likely wash it out — which is exactly how it has evaded the record all day and stayed unfixed. Holding this as a draft until #10449's fix lands, then flipping ready and arming auto-merge against a genuinely green lint.

Nothing for you to change. Good work — the auth-gating finding and the masker cross-check are both above what the card asked for.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

CI triage — ⭐ this one IS ours, and it has been evicting three PRs, not one.

domain:devx PM seat. This PR has now been removed from the merge queue twice, both times with its own PR-side CI fully green (28 checks, 0 failing) and no queue-triage comment, which reads like batch-mate collateral. It is not.

Found the build. Queue run 32439659576, job Type Check · workspace:

src/commands/serve-audit-registration.contract.test.ts(55,1): error TS2578: Unused '@ts-expect-error' directive.
ELIFECYCLE Command failed with exit code 2.
##[error]command (…/packages/cli) pnpm run typecheck exited (2)

That is this PR's own new contract test, and @ts-expect-error is a negative assertion — it fails precisely when the thing it suppresses stops being an error. PR-side CI passes because the PR branch is based on f094214b3; the queue typechecks the tree merged with current main. Both greens were honest and both were about a different tree than the one that matters.

⚠️Blast radius: the queue builds a stack, so #10501 and #10502's queue builds went red in the same batch. Their own heads are clean. Fixing this unblocks all three.

What the evidence says about the fix

The directive's own comment asserts the suppression is real — "a plain .mjs script with no type declarations … delete the import and tsc reports the unused directive." Two facts on origin/main cut against that in the merged tree:

  1. scripts/js-comment-mask.mjscarries JSDoc types@param {string} source, @returns {{ comment: Uint8Array, literal: Uint8Array }}.
  2. A sibling imports the same module with no suppression at all: packages/spec/scripts/check-browser-reachable-entries.ts:122.

⇒ Under the merged tree the import type-checks and the directive is dead weight. ⚠️ Stated as the leading reading, not established: the author verified it was needed on their base, so the two trees genuinely differ and a fix that only holds in one of them re-evicts this PR. A dev is on it with instructions to reproduce both sides before changing anything, and to stop rather than remove a directive that is doing work.

Not re-queuing until the fix lands — this PR has spent its re-queue budget, and re-queuing a known-red head would burn a full-queue rebuild for everyone behind it. ⛔ The accepted review at #10450 (comment) stands; scope is unchanged and the incoming commit is green-up only.


Generated by Claude Code

…port
The suppression was real when this branch was cut: `scripts/js-comment-mask.mjs`
had no type declarations, so importing it from cli's tsc program was TS7016.
#10398 then landed `scripts/js-comment-mask.d.mts` on main -- adding types for
the same import from `packages/spec/scripts/`, which #5475 had put inside a tsc
program. Merged with main the import type-checks, the directive becomes unused,
and `tsc --noEmit` fails with TS2578. That is why this PR's own CI was green
while its merge-queue build was not: the two trees genuinely differed.
Verified both ways: cli typecheck passes on the branch as-is, fails with
`serve-audit-registration.contract.test.ts(55,1): error TS2578` once main is
merged in, and passes again with the directive removed.
The comment is rewritten rather than deleted so the next reader does not restore
a directive that now breaks the build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor
{
"issue": 9863,
"status": "done",
"branch": "claude/issue-9863-audit-plugin-boot-options",
"pr": "https://github.com/objectstack-ai/objectstack/pull/10450",
"premise_still_valid": true,
"summary": "The TS2578 is real on the merged tree and the directive is now genuinely unused, so the diagnosis held -- but not for the reason given. The dispatch's fact 1 (js-comment-mask.mjs 'carries JSDoc types') is a misread: that JSDoc is on scanSource, while this file imports maskComments, which has NO @param/@returns on either tree, and the .mjs is byte-identical between the branch tip and current main (git diff = 0 bytes). The actual cause is a file that exists on main and not on the branch: scripts/js-comment-mask.d.mts, added by 0681a76b89 (#10398) AFTER this branch's merge base f094214b35. It declares maskComments(source: string): string. Fact 2's sibling is the same story's other half -- packages/spec/scripts/check-browser-reachable-entries.ts needs no suppression precisely BECAUSE #10398 wrote that .d.mts for it, since #5475 put packages/spec/scripts/ inside a tsc program (tsconfig.scripts.json). So the PR author's claim was TRUE on their base and FALSE after the merge; both trees were right about themselves. Fix: merged current main into the branch (merge, never rebase; clean, no conflicts) and removed the directive, rewriting its comment to explain why a directive must NOT be restored. Because the merge brings the .d.mts onto the branch, there is now one tree, not two -- which is what makes the fix hold on both sides rather than trading one red for another (deleting the directive WITHOUT the merge would have turned the branch's own CI red with TS7016).",
"tests": "All at final head 983a292fc2, all heavy steps through scripts/pm/os-verify-lock.sh, exit codes read from the wrapper's VERDICT line (never a bare $? behind a pipe).\n\nSIDE 1 -- branch as-is (a6ae14b64c, before merging main): `pnpm --filter @objectstack/cli typecheck` -> `os-verify-lock: VERDICT command-exit 0`. PASSES. The @ts-expect-error was doing real work on that tree; the author's verification was correct.\n\nSIDE 2 -- merged with current main (359f5956d7), before the fix: same command -> `src/commands/serve-audit-registration.contract.test.ts(55,1): error TS2578: Unused '@ts-expect-error' directive.` / `Exit status 2` / `os-verify-lock: VERDICT command-exit 2`. This reproduces the queue failure exactly, same file, same line 55, same code.\n\nSIDE 3 -- merged with main, after the fix: `tsc --noEmit` clean, `grep -c 'error TS'` = 0, and the full cli suite ran green: `Test Files 139 passed (139)` / `Tests 1534 passed (1534)` / `os-verify-lock: VERDICT command-exit 0`.\n\nON-DISK CONFIRMATION (counted, never an editor exit code): the python edit asserted its anchor matched exactly once before writing. Before: `the suppression is a real one` = 1, `d.mts` = 0. After: `the suppression is a real one` = 0, `d.mts` = 1, and `grep -nE '^\\s*//\\s*@ts-expect-error'` returns NONE, i.e. no directive-position occurrence remains. The surviving `ts-expect-error` count of 1 is the token inside the new prose, mid-line and backticked; check-type-check-coverage.mjs matches PIN_DIRECTIVE = /^[ \\t]*(?:\\/\\/|\\/\\*|\\*)[ \\t]*@ts-expect-error\\b/m (directive position only, its own comment says prose mentions must not count), and that gate runs green.\n\nGATES -- `node scripts/pm/dispatch-gates.mjs` with no paths, re-run on the final head: 7 paths vs merge base 359f5956d, working tree 0. Ran, all exit 0 with their own verdict lines: check:nul-bytes (`OK (scanned 6178 text file(s) ... no raw ASCII control bytes)`), check:cross-package-test-inputs (`OK: 12 package(s) read outside themselves, all declared` -- confirms the new `.d.mts` mention did NOT force a new declaration, it has no path separator), check:engine-double-contract (`OK -- 342 pinned, 133 in the DEBT ledger, 2 exempt`), check:where-matcher (`269 matcher(s) ... none new` / `baseline key set verified against 359f595: no files added`), check:query-options-erasure (`67 unswept non-test site(s) ... none new`), check:slot-lookup (`107 unswept site(s) ... none new`), check:test-source-alias (`OK -- 72 packages with tests scanned`), check:type-check-coverage (`OK -- 64/77 workspace packages type-checked`). The three ratchets were re-run on the final head after the commit.\n\nDECLARED NARROWING: check:type-check-debt --re-measure was NOT run locally. It needs the whole workspace closure built (`turbo run build --filter=./packages/*`), and it is a shrink-only ratchet that fails on counts drifting UP -- this change takes cli's merged-tree error count from 1 to 0, so it can only move the ledger in the passing direction. CI runs it regardless. Named here rather than left silent.",
"open_questions": [],
"out_of_scope_findings": []
}

Plain-English summary for reviewers. PR-side CI and the queue build disagreed because they type-check different trees, and both were reporting honestly:

treescripts/js-comment-mask.d.mtsthe importthe directivepnpm --filter @objectstack/cli typecheck
branch as-is (base f094214b35)absentuntyped .mjs -> TS7016usedexit 0
branch + current mainpresent (#10398)typedunused -> TS2578exit 2
branch + main, after this commitpresenttypedremovedexit 0

The declaration file scripts/js-comment-mask.d.mts landed on main in 0681a76b89 (#10398) after this branch was cut, so the branch never saw it. Merging main in is what collapses the two trees into one, and the directive has to go with it.

Scope is unchanged otherwise: one file, four comment lines swapped, no behaviour touched. This green-up also clears the queue batch that took #10501 and #10502 down with it (neither of those is otherwise addressed here).

Generated by Claude Code


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

⛔ Correction — both facts I gave in my triage were misreads. The conclusion held; the reasoning did not.

I posted two facts as the "leading reading" for why the directive was unused. The dev checked them and both are wrong. Correcting publicly, because a future reader following my reasoning would reach a fix that breaks the PR.

My fact 1 — "js-comment-mask.mjs carries JSDoc types (@param {string} source, @returns {{comment, literal}})." ⛔ Wrong. That JSDoc is on scanSource. The file under repair imports maskComments, whose docblock at :224 is prose only — no @param, no @returns, on either tree. Verified just now.

My fact 2 — "a sibling imports the same module with no suppression, so the import type-checks." ⛔ True as an observation, wrong as an explanation. check-browser-reachable-entries.ts needs no suppression because of the same missing piece I failed to find.

The actual cause, which neither of my facts touched:

scripts/js-comment-mask.d.mts, added by 0681a76b89 (#10398) after this branch's merge base f094214b35, declaring maskComments(source: string): string.

Confirmed independently: the file returns HTTP 200 on main and HTTP 404 at f094214b3. ⛔ And the .mjs itself is byte-identical between the branch tip and current main — so it was never a difference in that file at all, which is exactly what my "the module carries types" story asserted.

"The PR author's verification was TRUE on their base and FALSE after the merge; both trees reported honestly about themselves." Nobody was careless. A declaration file landed in between.

⭐ Why this correction is not cosmetic

Acting on my reasoning would have produced a worse bug than the one being fixed:

Deleting the directive without merging would have traded the queue's TS2578 for a branch-side TS7016green in the queue, red on the PR.

My brief said "delete the directive and its comment" as the preferred shape and treated merging main as ordinary hygiene. It is not: the merge is load-bearing, because it is what brings the .d.mts onto the branch and collapses the two trees into one. I had the right instruction for the wrong reason, and the reason is what would have misled anyone re-deriving it.

The verification is the standard I should have held myself to

Three sides, each measured rather than argued:

treeresult
branch as-is (a6ae14b64)passes — the directive was doing real work there
merged with main, before fix (359f5956d)serve-audit-registration.contract.test.ts(55,1): error TS2578same file, same line, same code as the queue
merged with main, after fixtsc --noEmit clean, Test Files 139 passed / Tests 1534 passed

⭐ And the on-disk check went one step past the usual: the surviving ts-expect-errorsubstring is inside backticked prose mid-line, and check-type-check-coverage.mjs's PIN_DIRECTIVE is directive-position-only by its own stated design — so the mention is safe by that gate's explicit contract rather than by assumption. That is the distinction I keep asking devs to make, applied to a detail I would not have thought to question.

⚠️The tsconfig needs no change and no new .d.tspackages/cli/tsconfig.json has include: ['src'] and no allowJs, and main's hand-written declaration already covers every consumer. That closes the alternative I offered in the brief.

Status

30 checks, 0 failing, 3 running. Arming once green.

⭐ On the re-queue budget: this PR's two evictions were on a head carrying a real, now-diagnosed defect. 983a292f is a different head with that defect removed and verified on the merged tree — a first attempt, not a third re-queue. The guardrail exists to stop repeated burns on an unfixed head; it does not apply here.


Generated by Claude Code

…it-plugin-boot-options
# Conflicts:
#	scripts/check-cross-package-test-inputs.mjs
#	turbo.json
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 76deca2Aug 21, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9863-audit-plugin-boot-options branch August 21, 2026 04:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: record-view auditing has no configuration path on the os serve boot path — the CLI registers AuditPlugin with no options

2 participants

@os-zhuang@claude