Repository navigation
fix(plugin-audit): a fired milestone row carries metadata.kind 'milestone' (ADR-0052 §5), served to a reader served its watched fields - #22783
Conversation
… (ADR-0052 §5), served to a reader served the watched fields The activityMilestones branch of the CRUD mirror keeps the row's type as its declaration names it and records the domain kind in metadata.kind, declaring the watched fields it derives from in text_sources.kind. The served-row redaction keeps the kind for a reader served those fields and drops it for one who is not, beside the text-column rule it already applies. Claude-Session: https://claude.ai/code/session_01CBAfsWMSfM3EToQGVStEcp Co-authored-by: Claude <noreply@anthropic.com>
…e served activity read Claude-Session: https://claude.ai/code/session_01CBAfsWMSfM3EToQGVStEcp Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBAfsWMSfM3EToQGVStEcp Co-authored-by: Claude <noreply@anthropic.com>
…r a by-id null Claude-Session: https://claude.ai/code/session_01CBAfsWMSfM3EToQGVStEcp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check7 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f0f4f64f90509fea950dffe1ce86e3c1073e1d65 && git checkout f0f4f64f90509fea950dffe1ce86e3c1073e1d65
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e84aeb36ce14169a633670f14ce8280fc998e2b9 f5281e88b9752d85530d63d084d69dde80c2787a && git checkout -B drift-repro e84aeb36ce14169a633670f14ce8280fc998e2b9 && git merge --no-ff f5281e88b9752d85530d63d084d69dde80c2787a
node scripts/docs-audit/affected-docs.mjs --json e84aeb36ce14169a633670f14ce8280fc998e2b9 |
Fixes #22771
Clause-②: no
What changed
packages/plugins/plugin-audit/src/audit-writers.ts: theactivityMilestonesbranch of the CRUD mirror writesmetadata.kind: 'milestone'on the row it writes.typestays what the declaration names:completed, orupdatedwhen it names none. ADR-0052 §5 puts the kind in this slot: "typestays domain-NEUTRAL … Domain kind rides inmetadata.kind." No newtypevalue, no new column, nopackages/specchange.metadata.text_sources.kindlists every field that a declared milestone watches. The first match wins, so "a milestone fired" is a fact about all of those fields, not only the one that fired.packages/plugins/plugin-audit/src/activity-field-redaction.ts:redactActivityRowskeepsmetadata.kindfor a reader served every listed field, and drops it for a reader who is not. A kind on a row the mirror did not write belongs to its author and is served as written. Text columns get the same treatment today.@objectstack/plugin-auditpatch changeset.Measured on
origin/maine84aeb3 before writingactivityType = milestone.typeand records no provenanceaudit-writers.tshasif (milestone.type) activityType = milestone.type;. The row carriestext_sources, but only for the interpolated tokens and the label. Nothing marks it as a milestone.metadata.text_sourcesis written by the CRUD mirror onlywriteAudit). So every mirror row carriestext_sources, milestone or not, and it cannot tell them apart.redactActivityRowsdeletestext_sourcesbefore servingmetadata.kind" needs a code changemetadatakey other thantext_sourcesuntouched: it narrows onlyold/new. The "keep" half needed no code. The code added is the opposite direction (assumption 3).sys_activitywriter already usesmetadata.kind, and nokindvocabulary is declaredgit grep "metadata\.kind"outside docs: 0 hits. The only hit is ADR-0052 line 201. Control on the same subject and failure shape:metadata\.oldormetadata\.newinpackages/**/*.ts: 8 hits. The repo's onlysys_activityinsert ispersistAuditTrailRow(audit-writers.ts), called by the CRUD mirror alone.auth-event-auditandread-auditwritesys_audit_logonly. Comments writesys_comment.packages/specdeclaresSYS_ACTIVITY_BUILTIN_TYPESandFeedItemTypefortype, and nothing formetadata.kind. App writers in other repositories (hotcrm) cannot be grepped here, and their kinds stay served as written.metadata.kindexposes a classification only, never field contentkind: 'milestone'means "a watched field just entered its declared value". To a reader not served that field, that is field content. So the kind is judged like a text: it is kept only for a reader served every watched field.Keys the redaction keeps, before and after
These are the top-level
metadatakeys a reader with a security answer is served on a CRUD-mirror row. Fail-closed paths are unchanged: they drop the wholemetadatacolumn. The no-answer path is unchanged: it passes the row whole.old/newtext_sourceskind(milestone rows only)text_sources.kindis served; dropped otherwiseTests (HEAD
f5281e88b)New file:
src/activity-milestone-kind.integration.test.ts, 10 tests. It uses a realObjectKernel, the real SQLite driver and the realAuditPlugin. Rows are written by the real CRUD mirror and read through the middleware chain the plugin mounts: the console's activity read (findonsys_activitybyobject_name+record_id) and the by-id read, as non-system callers. The only stand-in is the security service.metadata.kind: 'milestone', through both the list read and the by-id read. This holds for a typed milestone (completed) and an untyped one (updated).metadataof a milestone row is pinned per reader withtoEqual.text_sourcesis never served, a withheld change key is dropped from both sides, andkindis the only key judged by its declared fields.Runs (under
scripts/pm/os-verify-lock.sh):pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2: Test Files 46 passed (46), Tests 726 passed (726), at0655363f8. The only later commit,f5281e88b, is a type annotation in the new test. The new file re-ran atf5281e88b: 10 passed.pnpm --filter @objectstack/plugin-audit typecheck: VERDICT command-exit 0 atf5281e88b.check:test-typecheck: OK … 0 file(s) / 0 error(s)in debt. The first run had 2 TS2345 errors in the new test (Row | nullpassed into the metadata reader);f5281e88bfixed them.Ablations (committed state
2f5b81f3e; each leg went throughscripts/ablation-replace.mjswith its own restore)The suite resolves the subject through relative
src/imports, not through a packageexports, so nodist/rebuild is needed per leg. Each leg's anchor hit 1 time before and 0 times after, and the blob changed. Each restore was proven by blob equal to HEAD and an emptygit diff HEAD. The tree was clean afterwards.kindfor every rowkindtext_sources.kindlists only the fired milestone's fieldGates (HEAD
f5281e88b)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands. All 67 were run, and each exit code was recorded before any pipe.pnpm check:dual-build-cjs-loadsandpnpm check:i18nfirst exited 3 (PREREQUISITE NOT MET: nodist/). They exited 0 oncecheck:type-check-debthad built the workspace (i18n: "OK (9 package(s) — all bundles in sync").--ranreconciliation: "67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN".Lint was narrowed, and the narrowing is measured.
pnpm exec eslint --no-inline-config --format jsonon the 3 changed.tsfiles gave 3 files in the JSON, 0 errors and 0 warnings. These are the diff's whole TypeScript population; the fourth path is a.changesetMarkdown file, which nofilesglob ineslint.config.mjsmatches.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any file it does not touch.Acceptance notes
type, which comes from the declaration, and itssummary. The summary keeps the template's literal wording, andtext_sources.summarynames only the interpolated tokens, never the watched field. Probe at2f5b81f3e, through the same served read, as a reader withheldstage:{type: 'completed', summary: 'Won item'}, withstageredacted out ofmetadata. Both columns state that the withheld field enteredwon. This PR'skindis gated, so it adds nothing to that disclosure. The type and summary halves are reported for the seat to route.Generated by Claude Code