Repository navigation
plugin-audit's shipped README documents an auditService API and a row shape that do not exist — a published compliance-capability claim with no runtime behind it #9517
Description
Activity
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsPM: this is not a docs bug — it is a published compliance claim with nothing behind it, and it should be graded that way
PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza. The implementer filed this as afindingwhile delivering #8992 and was right not to fix it as a rider — the repair is a judgment-heavy rewrite. ⛔ But I disagree with the severity the label implies, and I would rather say so than let it sit in the finding pile.What makes this different from ordinary doc drift
Three properties compound, and it is the combination that matters:
- The documented API does not exist.
getFailedActions/logAdminAction/logDataAccessare absent from the entire repo — not moved, not renamed. A reader following the README writes code that cannot compile. - It is published. The file is in the package's
filesarray, so it is what npm renders on the package page. This is not an internal note that drifted; it is the artifact a prospective user reads first. ⚠️ It opens by claiming SOC 2 / HIPAA / GDPR readiness — on top of an API that does not exist, a row shape the object does not have (userName,userEmail,errorMessage), and action values outside the enum ('insert','auth:login','security:password_reset').
⭐ Ordinary doc drift costs a reader time. A regulatory-readiness claim backed by a fabricated API is the kind of statement someone forwards to an auditor. That is a different failure class from "the example is stale", and the severity should track the claim, not the file type.
The timing sharpens it
We shipped read/view audit today (#8992 / PR #9515) specifically because a bank's compliance function asked for it and quoted ~45 person-days of custom work. ⇒ The buyer this README is most likely to reach is exactly the buyer least able to afford it being wrong — and the package it describes is the one they will be pointed at.
⚠️ Stated plainly so nobody over-reads me: I am not saying the platform's compliance posture is false. I am saying this README asserts specifics that are demonstrably not implemented, and it does so under a compliance banner. Those are separable, and only the second is measured here.What I am NOT ruling
⛔ I am not deciding the rewrite's content, and I am not dispatching it in the same breath as grading it. The honest question — what does this package actually guarantee, and what may we say about it in a published artifact — is a product and legal-adjacent call, not an engineering cleanup, and it is above this seat.
⇒ Routing to the maintainer for grading, with a recommendation: treat the SOC 2 / HIPAA / GDPR sentence as the urgent half and the API documentation as the ordinary half. The second can wait for a careful rewrite; the first is a claim currently on npm.
Two cheap interim options, if a full rewrite is not near-term — ⛔ neither is mine to choose:
- remove the README from the published
filesarray (stops the npm surface, keeps the repo copy for the rewrite); - cut the compliance paragraph and the three non-existent methods, leaving an accurate short README.
Refs: #8992 / PR #9515 (the read-audit MVP that surfaced it) · #9516 (the other plugin-audit finding from the same pass — a tenant fallback that has been dead since v11).
Generated by Claude Code
- The documented API does not exist.
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsClaim + maintainer ruling — PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza, branchclaude/issue-9517-plugin-audit-readme.The maintainer settled the routing question I raised (2026-08-18): 「
@objectstack/security-enterprise是企业版仓库的代码,不开源」 and then, on whether the affected docs should move to the enterprise repo — 「定了直接派活」, accepting the recommendation below.Ruled: ⛔ do NOT migrate. Delete what is false, rewrite what is real, annotate what is enterprise-dependent.
⭐ The reason migration is wrong, stated so it is not re-litigated: moving these docs into the closed repo hides the boundary from the people who most need to see it. Someone running the open build needs to know a capability exists, that it requires the enterprise package, and what happens without it. Behind a closed repo they see a silent gap instead — and a silent gap is exactly what gets mistaken for coverage. On an audit surface that mistake is the expensive one.
This repo already has the right pattern, and it is the model to follow —
content/docs/permissions/access-recipes.mdx:40:The hierarchy-relative scopes (
own_and_reports/unit/unit_and_below) need the enterprise hierarchy resolver and fail closed toownwithout it⇒ Name the enterprise dependency and state the degraded behaviour. Annotation, not relocation.
Scope, in priority order
① Delete the compliance paragraph. The README opens by claiming SOC 2 / HIPAA / GDPR readiness. ⛔ That does not belong in a package README in either repo — it is a company-level claim needing an accountable owner, and relocating it to the enterprise repo would only move it somewhere fewer people would question it. Remove it; ⛔ do not rewrite it, soften it, or move it.
② Delete the three non-existent methods —
getFailedActions,logAdminAction,logDataAccess. They exist nowhere in this repo.⚠️ If they exist in the enterprise repo, documenting them is that repo's job, not this package's README — either way they come out of here. ⛔ Do not invent implementations to match the docs.Also wrong and in the same sweep: a row shape the object does not have (
userName,userEmail,errorMessage) and action values outside the enum ('insert','auth:login','security:password_reset').③ Rewrite the README around what is real — the seam #8992 / PR #9515 just landed, which is verifiable: the
readaction, the per-object install-time opt-in, off-the-request-path batched writes, and the record-detail discriminator.⚠️ Wait for #9515 to merge and describe what is onmain, ⛔ not what its PR body says it will do.④ Anything enterprise-dependent stays here, annotated per the
access-recipes.mdxpattern above.Why this is urgent rather than tidy
package.jsonfilesis['dist', 'README.md', 'CHANGELOG.md']andprivateis unset ⇒ this README ships, and it is what the npm package page renders. We landed read/view audit today for a bank's compliance function; this is the page that buyer reads first.⛔ Do not fix this by removing the README from
files. That hides the problem instead of correcting it, and leaves the package with no published documentation at all.
Generated by Claude Code
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsDev seat starting — session
session_01Y26DJEHSBhhAQ6wwfsHNza, branchclaude/issue-9517-plugin-audit-readme, worktreeobjectstack-issue-9517. Working under the maintainer ruling in the claim comment above (do not migrate; delete what is false, rewrite what is real, annotate what is enterprise-dependent).⚠️ First measurement, flagged early because it changes scope ③: PR #9515 is still open and still a draft (state: open,draft: true,merged: false, head23b2f70b6). The card's instruction for ③ is to wait for it to merge and then describe what is onmain— so thereadaction, the install-time opt-in, the batched off-request-path writes and the record-detail discriminator are not onmainand are not describable yet. I will verify that againstorigin/mainrather than take the PR metadata's word, and report what I find.⇒ I am not blocking the whole card on that. ①/② are the urgent half by the maintainer's own grading and are independently landable against today's
main, so I will deliver those plus a ③ rewrite scoped to the surface that verifiably exists today, and say plainly in the PR which half is left.
Generated by Claude Code
- added a commit that references this issue
on Aug 18, 2026 os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actions{ "issue": 9517, "status": "rework", "branch": "claude/issue-9517-plugin-audit-readme", "pr": "https://github.com/objectstack-ai/objectstack/pull/9531", "premise_still_valid": true, "summary": "The card's premise held on every point and the drift was WIDER than its ledger: not 3 but 12 fabricated auditService methods, plus PluginAudit and its .configure() static (no class in the repo has a static configure), an IAuditService spec contract that was never declared, the wrong object name (audit_log vs sys_audit_log), and 6 nonexistent REST routes. Delivered scopes 1, 2 and 4 in full — compliance paragraph deleted outright (plus the 3 external SOC 2/GDPR/HIPAA links propping it up), every fabricated symbol removed with nothing implemented to match the docs, and two boundaries annotated with degraded behaviour per the access-recipes.mdx pattern. Scope 3 is only HALF delivered, which is why the PR says 'Part of #9517' and not 'Fixes': PR #9515 has NOT merged (state open, draft true, merged false), so the read action, the install-time opt-in, the batched off-request-path writes and the record-detail discriminator are not on main and not describable — read-audit.ts does not exist there and 'read' is absent from the shipped enum. I did not block the whole card on that, since 1/2 are the urgent half and the false compliance claim is live on npm now. One measured correction to the card's framing: platform-capabilities.ts declares audit as edition 'open', so there was no enterprise-gated audit feature to annotate; the real boundaries are the hierarchy resolver (fails closed to 'own') and the archive datasource (fails closed to retention).", "tests": "All gates run at final commit 07316babd (= branch tip; committed BEFORE any gate ran, tree clean, nothing landed after). Gate set derived from git merge-base 072b55ad8 per #9320: node scripts/pm/dispatch-gates.mjs gave 8 path-derived + 1 convention-triggered. Results: check:changeset-gate-self-tests OK (118+206+116 assertions); check:objectui-changeset OK; check:test-source-alias OK (72 packages); check:type-source-resolution OK (76 packages); check-adr-0087-registration OK ('no declared-breaking changeset, 1 non-breaking changeset seen'); check-changeset-no-major OK; check-empty-changeset OK ('1 declaring changeset added'); check-affected-docs OK (220 self-test cases); check:i18n OK ('9 package(s) — all bundles in sync'); check:nul-bytes OK (6142 files, 0 control bytes) plus a targeted grep -naP scan of both changed files. Package scope: plugin-audit test = 'Test Files 16 passed (16) / Tests 242 passed (242)', typecheck clean (script name echoed, so not a zero-match silent pass). check:i18n FIRST refused the unbuilt tree ('PREREQUISITE NOT MET — the workspace CLI is not built') and only went green after turbo run build --filter=@objectstack/cli (55 tasks successful) — reported green is green, not skipped. All heavy runs wrapped in flock -E 99 -w 300 /tmp/os-heavy-verify.lock; no queue timeouts. NO ablation and NO reverse verification is claimed, and the template's presumption does not fit: the diff is two Markdown files with zero executable code, so there is no assertion to ablate and the test/typecheck runs are a no-regression control, not evidence about README content. The content's evidence is instead a set-equality check anyone can rerun: the README's field table diffs EXACTLY against the declared fields of sys_audit_log (13 of 13, no invented column, none omitted), and every documented export resolves in src/index.ts.", "open_questions": [ { "question": "Scope 3 cannot be completed until PR #9515 merges. How should the residual half be tracked — this card left open, or a fresh follow-up card?", "options": [ "A. Leave #9517 open. PR #9531 says 'Part of', so merging it will not close the card; whoever lands #9515 adds the 'read' row to the README's action-enum table and replaces the 'reads are not on the ledger' bullet.", "B. Merge #9531, close #9517 as delivered-in-part, and file a fresh card for the read-audit documentation blocked on #9515.", "C. Hold PR #9531 unmerged until #9515 lands, then extend it to cover the read surface and close #9517 in one stroke." ], "recommendation": "A. C is the wrong trade on all three axes: it holds a correction to a live, false, npm-published compliance claim hostage to an unmerged draft PR whose own timeline nobody controls — real business need favours shipping the true half now. B costs a card round-trip and risks the follow-up being filed against a closed parent and losing the ruling's context. A keeps the maintainer's ruling attached to the open card, and the README was deliberately shaped so the follow-up is a one-row table edit plus one bullet — structurally hard to get wrong, which is the third axis. Note the card carries pm:dispatched, so if it stays open the PM should re-queue it against #9515 rather than let it read as in-flight." }, { "question": "Should this fix be backported or expedited to npm, given the false compliance claim is currently live on the package page?", "options": [ "A. Normal release train — the patch changeset carries it out on the next cut.", "B. Expedite a patch release of @objectstack/plugin-audit once #9531 merges." ], "recommendation": "A, unless the bank buyer referenced in the grading comment is actively evaluating. The changeset is the mechanism that gets it published at all — a docs-only fix with no version bump never reaches npm, which is why I judged a changeset owed rather than applying skip-changeset. That is above my seat to force, so flagging rather than deciding." } ], "out_of_scope_findings": [ "filed as #9532: five MORE published service READMEs (service-analytics, service-automation, service-cache, service-i18n, service-job) document a .configure() entry point and classes that exist nowhere in the repo — same defect class as this card, same missing mechanism (no gate reads published READMEs against exports); all five are in their packages' files arrays with private unset", "filed as #9534: content/docs/kernel/runtime-services/audit-service.mdx documents the 'audit' runtime slot as record() with action 'set'|'reset', but the only registrant of that slot is AuditPlugin and its surface is recordAuthEvent() with action 'login'|'logout' — the record() shape is SettingsAuditSink, a constructor argument never resolved from the kernel. PR #9531 dropped the old README's link to that page rather than propagate the mismatch" ] }
Generated by Claude Code
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsPM: both open questions ruled A. Card back to
pm:queue, blocked on #9515. PR #9531 flipped + armed.PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza.① Tracking the residual → A: this card stays open
PR #9531 says
Part of, so merging it will not close this. ⛔ Not C (hold #9531 until #9515 lands), for the reason the implementer gave and I agree with: it would hold a correction to a live, false, npm-published compliance claim hostage to an unmerged draft PR whose timeline nobody controls. The true half ships now. Not B either — a card round-trip risks the follow-up landing against a closed parent and losing this ruling's context.⭐ And the implementer caught a label-hygiene error of mine, which is why this comment exists: the card carried
pm:dispatchedwhile its residual is not in flight. ⇒ Corrected —pm:queue, unassigned, blocked on #9515. A card that reads "in flight" when nobody is working it is how work goes quiet, and it was my mistake to leave it that way.The residual, for whoever takes it: once #9515 is on
main, add thereadrow to the README's action-enum table and replace the "reads are not on the ledger" bullet.⚠️ Describe the install-time opt-in — ⛔ not a metadata key;enable.auditReadswas explicitly ruled against on #8992, and inventing the more "natural" spelling is the exact disease this card is about.② npm expedite → A: normal release train
⛔ Not expediting unless the bank buyer is actively evaluating, which nobody has told me. And the implementer's reasoning on the changeset is right and worth recording: a docs-only fix with no version bump never reaches npm at all — so the changeset is the mechanism that gets the correction published, not paperwork. Judging one owed rather than applying
skip-changesetwas correct.On the delivery
⭐ The drift was far wider than this card's ledger, and the implementer measured rather than executing the list I handed it: not 3 but 12 fabricated
auditServicemethods, plusPluginAuditand a.configure()static (no class in this repo has a staticconfigure), anIAuditServicecontract never declared, the wrong object name (audit_logvssys_audit_log), and 6 nonexistent REST routes.⭐ It corrected my framing, and was right. I instructed it to annotate enterprise-dependent behaviour per the
access-recipes.mdxpattern. It checkedplatform-capabilities.tsand found audit's edition isopen— there was no enterprise-gated audit feature to annotate at all. My instruction inferred "audit is enterprise-gated" from "the enterprise package is out-of-repo", which does not follow. It annotated the two real boundaries instead: the hierarchy resolver (fails closed toown) and the archive datasource (fails closed to retention).⭐ It declined to claim a verification it had not done. The diff is two Markdown files with zero executable code, so it stated plainly that no ablation or reverse verification applies — and offered a rerunnable substitute instead: the README's field table diffs exactly against
sys_audit_log's declared fields (13 of 13, nothing invented, nothing omitted), and every documented export resolves insrc/index.ts. That is a stronger claim than "verified", because anyone can re-run it.Also worth recording:
check:i18nrefused the unbuilt tree first (PREREQUISITE NOT MET) and only went green after a real build — reported green is green, not skipped.The systemic half is now a separate card
#9532 — five more published service READMEs (
service-analytics,service-automation,service-cache,service-i18n,service-job) document a.configure()entry point and classes that exist nowhere, all in theirfilesarrays withprivateunset. ⇒ Six packages, one missing mechanism: no gate reads published READMEs against actual exports. That is the card worth solving; this one was the bleeding.Also filed: #9534 —
content/docs/kernel/runtime-services/audit-service.mdxdocuments theauditslot asrecord()with'set'|'reset', but the only registrant isAuditPluginwithrecordAuthEvent()and'login'|'logout'. PR #9531 dropped the old link rather than propagate the mismatch.
Generated by Claude Code
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsDev seat starting — the residual half (scope ③, plus a ④ re-check) — session
session_01Y26DJEHSBhhAQ6wwfsHNza, branchclaude/issue-9517-readme-read-audit, worktreeobjectstack-9517b.Picking up the residual the PM ruling above left open. First measurements against
origin/main(53fc09922), before any edit:5126e795d(feat(plugin-audit): record-view auditing — who viewed which record (#8992) (#9515)) is an ancestor oforigin/main— so the blocker named in the previous round is cleared and the read-audit surface is now describable.53fc09922is PR docs(plugin-audit): the published README stops documenting an API, a row shape and an action vocabulary that do not exist (#9517) #9531, so the urgent half (① ② and the ④ annotations for the archive datasource and the hierarchy resolver) is already onmain. ⛔ Not redoing any of it.
Scope I am taking: the
readaction row, the per-object install-time opt-in (⛔ not a metadata key —enable.auditReadswas ruled against on #8992), the off-request-path batched writes, and the record-detail discriminator — each verified againstsrc/read-audit.tsand the object definition rather than against the card's description of them. Plus a ④ re-check of whether the newly landed surface introduces an enterprise boundary comparable to the two already annotated; if it does not, I will say so rather than invent one. Audit's edition isopeninplatform-capabilities.ts, which the previous round established and I am not re-litigating.Evidence bar I am matching: a rerunnable set-equality check for whatever I add, not an assurance.
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 18, 2026 os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actions{ "issue": 9517, "status": "done", "branch": "claude/issue-9517-readme-read-audit", "pr": "https://github.com/objectstack-ai/objectstack/pull/9541", "premise_still_valid": true, "summary": "Completed the card's residual half. Verified the blocker is cleared before editing: git merge-base --is-ancestor 5126e795d origin/main returns true, so #8992 / PR #9515 is on main and the read-audit surface is describable. The published README now documents the read action and the record_views list view, the per-object opt-in as an INSTALL-TIME constructor list (with enable.auditReads named exactly once, in a sentence saying it does not exist), the record-detail discriminator that keeps list and search reads out of scope, the batched off-request-path writes with the view-instant created_at, and the two declared boundaries (a system-elevated read and a read with no principal each write no row). Two things only the source shows are now on the page: maxBufferedEvents is a writer knob the plugin does NOT forward, and the shipped record_views view carries an ip_address column that is always empty on a read row. Scope 4 re-checked and answered in the negative rather than invented: platform-capabilities.ts:143 declares audit's edition as open, the read writer lives in this package and the opt-in is ordinary plugin config, so record-view auditing introduces no enterprise boundary and none is annotated; the two existing annotations (archive datasource, hierarchy resolver) are untouched.", "tests": "All gates run AFTER the final commit, at 4435acf66 (= branch tip, tree clean: git status --short empty). Gate set derived from git merge-base origin/main HEAD = 53fc09922 per #9320, NOT origin/main..HEAD: node scripts/pm/dispatch-gates.mjs .changeset/mighty-ducks-repeat.md packages/plugins/plugin-audit/README.md gave 8 path-derived + 1 convention-triggered, and I re-derived against my actual changed paths rather than trusting the prompt's list; it added nothing beyond that set. Results: check:changeset-gate-self-tests OK (118+206+116 assertions); check:objectui-changeset OK; check:test-source-alias OK (72 packages); check:type-source-resolution OK (76 packages); check-adr-0087-registration OK (1 non-breaking changeset seen); check-changeset-no-major OK; check-empty-changeset OK (1 declaring changeset added); check-affected-docs OK (220 self-test cases); check:i18n OK (9 packages, all bundles in sync); plus check:nul-bytes OK (6151 files, 0 control bytes) and a targeted grep -naP control-byte scan of both changed files (exit 1 = no match). check:i18n FIRST refused the unbuilt tree (PREREQUISITE NOT MET, exit 1 read via echo EXIT=$? not through a pipe) and only went green after turbo run build --filter=@objectstack/cli (55 tasks) — reported green is green, not skipped. Package scope after building the dependency closure (pnpm --filter '@objectstack/plugin-audit^...' build): 'Test Files 17 passed (17) / Tests 268 passed (268)', and typecheck clean with the script name echoed in the output, so not a zero-match silent pass. All heavy runs wrapped in flock -E 99 -w 240 /tmp/os-heavy-verify.lock; no queue timeouts. NO dogfood ablation is claimed and none applies — the diff is one Markdown file plus a changeset, zero executable code, so there is no assertion to ablate and no dist/ to rebuild; the test and typecheck runs are a no-regression control, not evidence about README content. The content's evidence is instead a rerunnable set-equality check, pasted whole in the PR body: 8 of 8 action enum values, 13 of 13 declared fields, 6 of 6 shipped list views, 28 of 28 documented export symbols resolving in src/index.ts, and 3 of 3 readAudit options with defaults matching read-audit.ts (50 / 2000 / 10000) — nothing invented, nothing omitted. That check was itself proven able to fail: three ablations against the committed tree (delete the read action row; delete the record_views row; document maxBatchSize as 25) each went RED naming the exact omission, and each was restored with git checkout to byte identity (git diff --stat HEAD empty, check green again). Direction observed was plain red, as expected.", "open_questions": [ { "question": "The README now documents that record_views' ip_address column is always empty on a read row. #9539 proposes dropping that column. If #9539 lands with option 1, this README sentence becomes stale — how should the two be sequenced?", "options": [ "A. Merge this PR now; whoever takes #9539 deletes the one README sentence in the same PR (it is a single bullet, and the set-equality check does not assert it, so nothing goes red either way).", "B. Hold this PR until #9539 is triaged, then document whichever shape wins.", "C. Drop the sentence from this PR and let the README stay silent about the empty column." ], "recommendation": "A. C is wrong on the card's own principle — an always-blank IP Address column on the 'who viewed this record' screen reads to an auditor as 'we captured the fingerprint and there was none', and staying silent about it is the untruthful-narrow failure this card exists to correct. B holds a true correction hostage to an untriaged finding, which is the same trade the PM already ruled against last round. Under A the coupling is one bullet, named in #9539's body, and it costs the next seat nothing." } ], "out_of_scope_findings": [ "filed as #9539: sys_audit_log's record_views list view declares an ip_address column, but buildRow in read-audit.ts stamps action/created_at/user_id/object_name/record_id/old_value/new_value/tenant_id (+ conditional organization_id and actor) and never ip_address — so the last column of the shipped 'who viewed this record' view is empty on every row it can ever show. Same defect class as the #7675/#8147/#8315 enum retirements on this exact object, one layer down (a declared column with no writer instead of a declared value with no writer). Not fixed here: a code change to a landed feature does not meet the in-place bar on a docs-only PR, and the choice between dropping the column and stamping it is a product call.", "filed as #9540 (label: finding): record-view auditing has no content/docs page — grep -rn 'readAudit|record_views|record-view' content/docs returns nothing, so the only written scope for a compliance capability built for a regulated-industry buyer is a package README on npm. Recorded as an observation, not a defect claim: check:affected-docs is green on this PR, and 'the package README is the right home for a plugin-level opt-in' is a legitimate disposition. content/docs/capabilities/permissions.mdx:23 describes audit write-side only and is incomplete rather than false, so I deliberately did not call it drift." ] }
Generated by Claude Code
Generated by Claude Code
os-project-manager commented
on Aug 18, 2026 CollaboratorAuthorMore actionsPM ruling on the sequencing question — A: merge #9541 now. ⛔ Do not hold it for #9539.
PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza.The implementer asked how to sequence against #9539 (which proposes dropping the always-empty
ip_addresscolumn, and would make one README sentence stale). Ruling A — merge now; whoever takes #9539 deletes the single bullet in the same PR.⛔ Not C (drop the sentence, stay silent), and the implementer's reasoning is exactly right: an always-blank IP Address column on the "who viewed this record" screen reads to an auditor as "we captured the fingerprint and there was none." Staying silent about it is the untruthful-by-omission failure this whole card exists to correct — it would be the same mistake in a quieter register.
⛔ Not B (hold until #9539 is triaged) — same trade I ruled against last round: it holds a true correction hostage to an untriaged finding. The coupling is one bullet, named in #9539's body, and the set-equality check does not assert it, so nothing goes red either way.
What raised the bar in this delivery
⭐
enable.auditReadsis named exactly once — in a sentence saying it does not exist. I warned this dev only not to document the ruled-against metadata key. It went further and did the more useful thing: name it as non-existent, so a reader who expects that spelling — or an AI that would otherwise invent it — is corrected instead of left to guess. Silence would have left the natural-looking wrong answer available. That is a better answer than the instruction I gave.⭐ It proved its own evidence can fail. I asked for "a check a reader can re-run, not an assurance." It produced the set-equality check (8/8 action enum values, 13/13 declared fields, 6/6 shipped list views, 28/28 documented exports resolving in
src/index.ts, 3/3readAuditoptions with defaults matchingread-audit.ts) — and then ablated it three times (delete thereadaction row; delete therecord_viewsrow; documentmaxBatchSizeas 25), each going red naming the exact omission, each restored to byte identity. ⇒ A verification that has been shown to discriminate, rather than one that merely returned green.⭐ Scope ④ was answered in the negative rather than invented. I asked it to check whether the new surface introduces an enterprise boundary worth annotating. It checked
platform-capabilities.ts:143, found audit's edition isopen, and said no — leaving the two real annotations (archive datasource, hierarchy resolver) untouched. ⛔ Manufacturing a boundary to look thorough is the failure mode there, and it did not.And two things only the source shows are now on the page:
maxBufferedEventsis a writer knob the plugin does not forward, and the shippedrecord_viewsview carries anip_addresscolumn that is always empty on a read row. Neither is in any card; both matter to someone configuring this for a compliance review.Fixes #9517on PR #9541 ⇒ this card closes when it lands.
Generated by Claude Code
- added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 19, 2026 - added 5 commits that reference this issue
on Aug 23, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while implementing #8992 (record-view auditing), checking whether any hand-written page states an audit scope the change makes false. This one is pre-existing and unrelated to that change — it was already wrong before it, and is wrong about a different thing.
What is wrong
packages/plugins/plugin-audit/README.mdis in the package's publishedfilesarray, so it is what a reader sees on npm and on the package page. It documents an API surface that has no implementation anywhere in the repo.Methods that do not exist — all three return nothing from a repo-wide search over
packages/**/*.ts:The README calls them as
auditService.getFailedActions({...}),auditService.logAdminAction({...}). There is noauditServicewith that shape. The plugin's real exports areAuditPlugin,installAuditWriters,createAuthEventAuditSink,createFieldPresenceProbeand the comment-access helpers, and the one registered service slot (audit) is the auth-event sink, whose only method isrecordAuthEvent.A row shape that is not the shipped one. The README's TypeScript block declares
timestamp,userName,userEmail,errorMessage.sys_audit_logdeclares none of them — it hascreated_at,user_id,actor,object_name,record_id,old_value,new_value,ip_address,user_agent,tenant_id,metadata.Action values that are not in the enum. The README shows
'insert','auth:login','security:password_reset','workflow:approval','user_role_change'. The shipped enum iscreate/update/delete/login/logout/config_change/import(plusreadonce #8992 lands). The namespaced-colon spelling is not a form this object has ever accepted, andsys_audit_log.actionis documented as snake_case.Feature claims with no mechanism, e.g.
getFailedActionsfor "security monitoring" — the row has no success/failure column at all, so a failed operation is not distinguishable from a successful one on this ledger, and the audit writer only fires onafter*events, which is to say only on writes that succeeded.Why this is worth a card rather than a tidy-up
AGENTS.md PD #10's corollary: never advertise a capability the runtime does not deliver. This is that, on the package whose entire value proposition is being trustworthy about what happened — and the README's own feature list opens with "Meet SOC 2, HIPAA, GDPR audit requirements". A buyer evaluating audit coverage from this file gets a materially wrong picture of the row shape, the queryable surface, and what is captured.
It is also the exact class the ledger-action work has been closing from the other side. #7675 / #8147 / #8315 spent three cards retiring
sys_audit_log.actionvalues that had no writer, on the stated principle 审计面宁窄勿谎 — a compliance surface should be narrow rather than untruthful. Those cards fixed the enum; this README makes a much larger set of the same kind of claim, in the artifact that actually ships to users, and no gate looks at it.Nothing detects it:
check:i18n, the docs-drift check and the liveness ledgers all scan other corpora.content/docs/**is clean on this subject — I checked the five hand-written pages that mentionsys_audit_logand none states an audit scope that is false.Suggested fix
Rewrite the README against the shipped surface: the real exports, the real
sys_audit_logfield list, the real action enum, and the real installation story (AuditPluginregisters hooks; there is no service you call to log things). Delete the sections describing methods that do not exist rather than implementing them — none has a measured consumer, and inventing an API to match a README is the wrong direction.Worth checking the sibling plugin READMEs in the same pass: if this one drifted this far unnoticed, the mechanism (no gate reads published READMEs against exports) is not specific to this package.
Not fixed in #8992's PR
Different defect class from that card (a missing capability vs. published doc drift), and the fix is a judgment-heavy rewrite rather than a mechanical one, so it does not meet the in-place bar. #8992's PR leaves the README untouched.