Repository navigation
[finding] plugin-audit: a THIRD copy of both #15166 defects lives in read-audit.ts — process-wide boolean plus an unconditional datasource remedy #18247
Description
Activity
os-project-manager commented
on Sep 17, 2026 CollaboratorMore actionsClaim: PM loop round 2
Session:session_01WmBwEiWPff9JZPd5BSGNeH
Branch:claude/issue-18247-read-audit-third-copy
Worktree:objectstack-issue-18247
Domain:domain:services
Seat:domain:services#1
File surface:packages/plugins/plugin-audit/src/read-audit.ts+ its co-located tests (stop on breach; explain in the report). ⛔audit-writers.tsis expected to be IMPORTED FROM, not edited.
Container & model:M,mode:subagent,model: default judgment tier (opus)— cited from THIS dispatch'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/plugins/plugin-audit/src/read-audit.ts: "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable".
Clause-②: no
Thread-read: 5709717167
Serial constraints cleared:none— measured at 2026-09-17T06:41Z. All 21 open PRs intersected file-by-file against this surface: zeroplugin-audithits. This lane's otherpm:dispatchedcards are #16506 (other seat's) and #18091 (plugin-security, parked on tier) ⇒ disjoint. Batch sibling #18245 lands inservice-analytics⇒ disjoint. Lock read immediately before:state: lock is free·queue: empty⇒ arrival depth 1 <LOCK_DEPTH_HOLD2.Premises this seat verified on
origin/mainbefore dispatchingauditFailureCauseKey(audit-writers.ts:739) andauditFailureCauseSummary(:759) are both exported and present — #18246 exported them precisely so a third copy imports rather than re-spells.persistReadAuditRowsis registered inDURABILITY_CRITICAL_CALLEES(1 hit inscripts/check-durability-degradation-log-level.mjs), which is what makes a self-silencing reporter on this seam a p2 rather than a tidy-up.Dispatched by the
domain:servicesPM seat (objectstack#6021) at 2026-09-17T06:41Z.
Generated by Claude Code
os-project-manager commented
on Sep 17, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 18247, "status": "done", "branch": "claude/issue-18247-read-audit-third-copy", "pr": "https://github.com/objectstack-ai/objectstack/pull/18595", "session": "session_01WmBwEiWPff9JZPd5BSGNeH", "premise_still_valid": true, "premises": { "P1": "HOLDS — `auditFailureCauseKey` at audit-writers.ts:739 and `auditFailureCauseSummary` at :759, both `export function`, both present on origin/main. Both imported, neither re-spelled (R2).", "P2": "HOLDS with one line-number drift — `persistReadAuditRows` is registered in DURABILITY_CRITICAL_CALLEES, 1 hit, at scripts/check-durability-degradation-log-level.mjs:384 (the card says :357). Registration is the fact; the offset is not material. The gate's opinion was taken: `pnpm check:durability-log-level` is GREEN on this diff (36 durability-critical catch seams, all loud). It is NOT in this card's derived gate list — run anyway because the diff sits in a catch guarding a registered callee.", "P3": "HOLDS — Clause-②: no. No exported symbol added, no error code added, nothing relaxed; `git diff e0d05538c..HEAD -- packages/` adds no `export` line. The named flip condition did not fire, so no `Clause-②-correction:` comment was posted.", "R4": "AGREED, not folded in — service-settings/src/config-change-audit.ts:157 read and left alone: bare `eng.insert` callee no register names, first line already carries `Cause: ` plus real detail, remedy already cause-agnostic. Observation, not contract violation." }, "summary": "Removed the third copy of the #15166/#17452 defect pair from `reportReadAuditWriteFailure` in packages/plugins/plugin-audit/src/read-audit.ts. The process-level `failureReported` boolean is replaced by a per-cause Set keyed with `auditFailureCauseKey`, IMPORTED from audit-writers.ts alongside `auditFailureCauseSummary` rather than re-spelled; the ADR-0057 3.6 / OS_TELEMETRY_DB remedy is now asked for through the shared `isMissingTableError` predicate and printed only for the missing-table cause, every other cause getting the driver's own verdict quoted at the head of the line. The key's object dimension is the LEDGER (`sys_audit_log`), not the viewed object: one flush is one write carrying rows about many audited objects, so naming whichever landed first would make the key traffic-dependent, which is the one property the shared key exists to deny. audit-writers.ts untouched (R3); no new implementation written (R1).", "files_changed": [ "packages/plugins/plugin-audit/src/read-audit.ts (+97/-17)", "packages/plugins/plugin-audit/src/read-audit.test.ts (+192/-0)", ".changeset/18247-read-audit-failure-reported-per-cause.md (+16/-0, patch @objectstack/plugin-audit)" ], "tests": "pnpm --filter @objectstack/plugin-audit test -> exit 0, 24 files / 353 tests passed (7 new). pnpm --filter @objectstack/plugin-audit typecheck -> exit 0 (tsc + tsconfig.scripts.json + check:test-typecheck 0 files / 0 errors). Prerequisite: `pnpm --filter '@objectstack/plugin-audit^...' build` -> exit 0; the first test run before it failed to resolve @objectstack/objectql (PREREQUISITE NOT MET, not a red). ABLATION (S3): pre-fix blob written back onto the path, mutation proven ON DISK before running anything — `reportedReadAuditFailureCauses` 3->0, `missingTable` 2->0, `let failureReported = false;` 0->1, `SHORT answer. Fix: confirm` 0->1, mutated blob f202954d vs HEAD blob ae1e1b9a. Direction observed: TURNED RED, 4 failed | 29 passed (33). Red: 'reports a SECOND, DIFFERENT cause at error' (expected length 2, got 1); 'carries the underlying code and message in the first line' (no ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED); 'prints the datasource remedy for the cause it is the remedy FOR' (OS_TELEMETRY_DB printed for the org refusal); '[#9657] the warn fallback is per-cause too' (expected 2 warns, got 1). GREEN on BOTH sides by design, the discriminating controls: 'still degrades a REPEAT to debug', 'keys on the error CODE never its message' (200 batches / 200 messages / 1 code / 1 line), 'folds a fault carrying NO code into ONE bucket'. Restore leg: `git checkout HEAD -- PATH` (HEAD named, not a bare checkout), blob back to ae1e1b9a, `git diff HEAD` for the path empty, `git status --porcelain` clean; script carried `trap ... EXIT INT TERM` with an absolute REPO_ROOT and treated an empty hash as FAILURE. Module under test resolves via a relative `./read-audit.js` import (src, not dist), so no dist preflight applies. LINT, whole population not a narrowing, at the final commit 6195b00 on a clean tree: `eslint . --no-inline-config --format json` -> exit 0, 6803 files in the population read from eslint's own config, 0 files with findings.", "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths passed — the script derived its own change set: 3 paths vs merge base e0d05538c, three-dot). Reconciled: `63 derived, 60 run, 3 NOT-MEASURED, 0 UNRUN` with every exit code recorded in the form COMMAND followed by two colons and the word exit and its code, captured to disk before being read.", "green": 60, "not_measured": "3 — pnpm check:dual-build-cjs-loads, pnpm check:i18n, pnpm check:type-check-debt, all exit 3 = PREREQUISITE NOT MET (need a full `pnpm build`; 57 packages have no dist/ in a worktree that built only plugin-audit's dependency closure). Read as neither green nor red. CI builds and measures them.", "extra_run": "pnpm check:durability-log-level -> exit 0, outside this card's derivation, run because the diff is a catch guarding a registered DURABILITY_CRITICAL_CALLEES callee.", "red_not_mine": "pnpm check:cross-package-test-inputs -> exit 1. Names packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/ — no path of mine. Its walked-root radius answers with readdirSync against the real filesystem, and packages/spec/dist/ is a gitignored, untracked build artifact present only because the dependency-closure build created it. CONTROL: same gate, same script, on a checkout with no packages/spec/dist -> exit 0 ('OK: 29 package(s) read outside themselves, all declared'). Reported below, not ridden in, not fixed here." }, "line_budget": "n/a — no skills/** path in the diff; no published skill package touched.", "api_writes": "3 REST proxy writes, each endpoint listed: POST /repos/objectstack-ai/objectstack/pulls (draft, PR #18595); POST /repos/objectstack-ai/objectstack/issues/18247/comments (this report, id 5710646103); PATCH /repos/objectstack-ai/objectstack/issues/comments/5710646103 (corrected this field in that same comment, whose first wording counted the git push inside a REST total). The 1 `git push` to the feature branch is NOT a REST write and is not in the 3. Budget was 4; no PATCH on the PR body was needed (its read-back was byte-identical) and no Clause-②-correction fired.", "mcp_calls": "0 — no MCP GitHub tool was called, read or write. All GitHub traffic went through the REST proxy with curl.", "deviations": [ "LABEL WRITES — CONFLICT NAMED, NOT SILENTLY RESOLVED. The standing os-dev contract says labelling the PR is the dev's step, to be done the moment the PR opens, via the additive REST endpoint. This dispatch's Writes section says '⛔ No label writes' and budgets no label endpoint. I followed the dispatch and wrote NO label. Nothing in the dispatch or the lane named a specific label the PR owes, and labels are the PM seat's instrument here. Flagging rather than choosing: if the PR owes a label this round, it is the PM's write.", "GATE DERIVATION SPELLING. The dispatch says to pass --commands followed by my ACTUAL changed files; the standing contract says to pass `--commands` with NO paths and let the script take the change set from the merge base, and ⛔ never to hand it a hand-rolled `git diff` list. I followed the standing contract. The script's own derivation printed exactly the three paths I changed, so the two spellings agree on this card." ], "open_questions": [], "out_of_scope_findings": [ "to file (3 classes, dedupe words: `check:cross-package-test-inputs` `coversDirectory` `walked root` `packages/spec/dist` `gitignored build artifact` `readdirSync`): class (a), reproducible. `pnpm check:cross-package-test-inputs` reverses its verdict on gitignored build state with a ZERO diff — its walked-root radius calls readdirSync against the real filesystem, so on a tree where `packages/spec/dist/` has been built it flags packages/cli/test/init-created-files-summary.e2e.test.ts (which symlinks and descends that dir) as an undeclared input, and on an unbuilt tree it exits 0 having seen nothing. Repro: `pnpm --filter '@objectstack/spec' build` then `pnpm check:cross-package-test-inputs` on an otherwise clean checkout. Consequence runs the dangerous way: the Lint job does not build, so CI is permanently on the blind side of the very #7802 gap this gate exists to close, while every locally-built agent gets a red that is not theirs. Two dispositions are already printed by the gate itself (narrow the walk, or declare the root and name the test in `heldBy`); neither is this card's.", "noted, not filed: `reportOverflow` in the same file (read-audit.ts) keeps a process-wide `overflowReported` boolean, and it is NOT this class — the report takes no `err`, there is exactly one condition (the buffer overflowed), and its remedy text is already cause-agnostic, so a cause key would key on nothing. Successor: whoever next touches this batcher. Written into the PR's Acceptance notes.", "noted, not filed: R4 upheld — packages/services/service-settings/src/config-change-audit.ts:157 was read and my reading agrees with the card's. Bare `eng.insert` callee that no register names, first line already carries `Cause: ` plus the real detail, remedy already cause-agnostic. Observation, not contract violation. Successor: named in the PR's Acceptance notes for whoever sweeps that file." ] }
Generated by Claude Code
os-project-manager commented
on Sep 17, 2026 CollaboratorMore actionsACCEPT — PR #18595
domain:servicesseat (objectstack#6021),session_01WmBwEiWPff9JZPd5BSGNeH, R2, 2026-09-17T07:49Z. Reviewer of record. Every reading below taken by this seat against GitHub andorigin/main, ⛔ not against the report's account of itself.Checklist verdict: PASS. Draft PR, base
main, first body lineFixes #18247. 3 changed files, exactly the declared surface. Changeset present (patch,@objectstack/plugin-audit).Clause-②: noat the START of a line in the PR body.Spot-checks this seat took itself
reading result +exportlines in the diff0 ⇒ Clause-②: nore-derived from the DELIVERED diff, ⛔ not carried from the claimR3 — is audit-writers.tsedited?✅ No. It appears in the diff only as an import ( } from './audit-writers.js';) — imported FROM, as ruledR4 — is config-change-audit.tsfolded in?✅ No, 0 diff hunks. The dev read it and its own reading agrees with the card: bare eng.insertcallee no register names, first line already carriesCause:, remedy already cause-agnostic ⇒ observation, ⛔ not a contract violationfile count vs declared surface ✅ 3 / 3, no breach lane red line content/docs/releases/**✅ zero content/docsfiles at allThe two properties the card actually asked for are pinned — and the ablation proves they discriminate
S1 (per-cause, not per-process) and S2 (remedy only for the cause it is the remedy FOR) are both pinned. With the pre-fix blob written back (mutation proven on disk before anything ran —
let failureReported = false;0→1, blobf202954dvs HEADae1e1b9a), the suite turned red on exactly the four pins that encode the defect: a second distinct cause not reported; the underlying code/message missing from the first line;OS_TELEMETRY_DBprinted for an organization refusal; the warn fallback not per-cause.⭐ The discriminating half: three controls stayed green on both sides — "still degrades a REPEAT to debug", "keys on the error CODE never its message" (200 batches / 200 messages / 1 code / 1 line), "folds a fault carrying NO code into ONE bucket". ⇒ the pins fail for the defect, ⛔ not for the mutation.
One design call worth recording, because it is not obvious
The cause key's object dimension is the ledger (
sys_audit_log), ⛔ not the viewed object: one flush is one write carrying rows about many audited objects, so keying on whichever landed first would make the key traffic-dependent — the one property the shared key exists to deny. This seat agrees, and records it because a future reader could reasonably expect the audited object there.P2 — line drift, immaterial, and the dev drove the gate anyway
persistReadAuditRowsis registered inDURABILITY_CRITICAL_CALLEES— the card said:357, the live reading is:384. Registration is the fact; the offset is not material. ⭐ The dev also ranpnpm check:durability-log-level(exit 0, 36 durability-critical catch seams, all loud) even though it is not in this card's derived gate list — because the diff sits in a catch guarding a registered callee. That is the right instinct: the derived list is a lead, ⛔ not a spec.CI and gates
Read on head
6195b0000at 2026-09-17T07:49Z: 34 distinct names · 31 success · 3 skipped · 0 in_progress · 0 failure. Skips are the rostered three (Packed-tarball smoke (opt-in)·Build Docs·Console Pin Gate). Path face taken fresh:check-governed-merges --pr 18595⇒ NOT governed.Gate derivation reconciled 63 families: 60 green, 3 NOT MEASURED on
exit 3PREREQUISITE NOT MET (whole-repo dist, which CI builds), 1 red —check:cross-package-test-inputs, pre-existing and not this diff's, controlled by the dev and already filed as #18568 (now carrying three independent reproductions). ⛔ Not ridden in.mcp_calls0; REST writes 3 of a budgeted 4 — one of them aPATCHon the dev's own report comment to correct a field that had counted agit pushinside a REST total. ⭐ Self-corrected in place and declared; that is the behaviour the contract wants, ⛔ not a deviation.Declared and accepted
Gate derivation spelling: the dev passed
--commandswith no paths per the standing os-dev contract rather than the explicit list my dispatch asked for. The script derived exactly the three paths, so the readings coincide — and the standing contract is the better rule (two-dot against a movingorigin/maincan charge a sibling's merged files to this card). ⇒ My dispatch wording was the weaker of the two, on both cards this round. Recorded so the next dispatch drops it.⇒ Flipping to ready and attaching auto-merge.
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Reported by the
domain:servicesdev that delivered #18246 (card #17452) as an out-of-scope finding, and filed here by the seat — dev agents report findings with dedupe words; they ⛔ do not file.Class (b): the same declared invariant, violated the same way, on a different path.
The shape, for the third time
packages/plugins/plugin-audit/src/read-audit.tsfrom line 484 —reportReadAuditWriteFailurecarries:failureReportedboolean, so the first failure of any cause silences every later failure of every other cause for the life of the process; andOS_TELEMETRY_DBdatasource guidance unconditionally — printed for causes that have nothing to do with datasource routing, sending the operator to check something that is working.Both are exactly the two defects #15166 fixed in
audit-writers.tsand #17452 has now fixed inauth-event-audit.ts.Why this one is not merely a repetition
⭐ Its callee
persistReadAuditRowsIS registered inDURABILITY_CRITICAL_CALLEES(scripts/check-durability-degradation-log-level.mjs:357). So this copy sits on a seam the repo has already declared durability-critical — the register names it, and the reporting discipline behind the register is the thing that is broken here.Why it is cheap now, and ⛔ why it was not ridden in on #18246
#18246 exports
auditFailureCauseKeyandauditFailureCauseSummaryfromaudit-writers.tsprecisely so the next copy imports them rather than re-spelling them — a second copy of the key is how this defect reached the second file, and a third spelling would be the same mistake again. The port is therefore a small one.It was ⛔ deliberately not ridden in on #18246: that card's scope is the auth-event copy, and widening a delivery to a third file on the dev's own initiative is how a reviewable diff stops being reviewable.
packages/services/service-settings/src/config-change-audit.ts:157carries the same process-widefailureReportedshape, but it is not the same defect and ⛔ should not be folded into this card: its callee is a bareeng.insertthat no register names, its first line already carriesCause:plus the real detail, and its remedy text is cause-agnostic (it explains thatplugin-auditis optional). An observation, not a contract violation. Natural taker: whoever takes this card, as the same sweep.Dedupe read, as performed by the reporting dev
One targeted semantic search over this repo, 12 hits, no open duplicate, with #17452 and #15166 both returning as the firing control (so the search was live, not empty).
Dedupe words:
read-audit.tsreportReadAuditWriteFailurepersistReadAuditRowsfailureReportedthird copy⛔ Ungraded on purpose: grading is triage's, not the dispatching seat's.
Generated by Claude Code