Skip to content

fix(rest): /meta/:type/:name/audit is an authoring door, refused as /history and /diff refuse (#20441) - #20472

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20441-audit-authoring-door
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20441-audit-authoring-door

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20441
Clause-②: no

GET /api/v1/meta/:type/:name/audit is now an authoring door. A caller that mayReadPendingDrafts does not admit is refused as /history, /diff and GET /api/v1/meta/_drafts refuse: 403, code FORBIDDEN, in the same nested error envelope. The decision is made on the caller before the protocol is resolved, before the query is parsed and before any event is read. This executes triage's grade 5871509797 on the card, which carries ruling 5865708652 (letter B, the maintainer's 「同意」 through the director seat, on #20378) to this door.

Reach, measured before any edit (H0)

On main at acd009521e, on the real-stack harness of meta-history-diff-authoring-door.test.ts (better-sqlite3 in memory, the real sys_metadata* objects, a real ObjectStackProtocolImplementation, the real routes; the stubs are resolveExecCtx and the tenancy probe). A system caller published app/atlas and view/opportunity.pipeline; an author (manage_metadata) then saved a draft of each, and a draft of the never-published app/beacon and view/opportunity.forecast. A member with no authoring capability (/meta/_drafts answers them 403) then read:

member reads plain read /audit
app/atlas 200 200, two events; one is note: "draft", actor: "u_author", with its time
view/opportunity.pipeline 200 200, the same shape
app/beacon (draft only) 404 200, one event, note: "draft", actor: "u_author"
view/opportunity.forecast (draft only) 404 200, the same
app/nowhere (missing) 404 200 { "events": [] }

So the card's premise holds, and the reading is one step wider than the card: for an item with nothing published, /audit also told a member that it exists (one event against { "events": [] } for a missing name), where the plain read answers 404 (ADR-0045 §3). Both are closed by the same guard.

No member-facing consumer (H1, the ruling's stop valve)

  • objectui at the pin dd3f7e1be3: the one caller is AuditPanel (client.audit(type, name)). It is mounted only in MetadataResourceEditPage, the metadata designer on the metadata/:type/:name routes, as the audit sheet beside the history sheet #20440 already gated. That is an authoring surface.
  • cloud at origin/main 3efda046: zero callers. git grep for auditMetaItem, getAudit, /audit and .audit( exits 1. The control query of the same shape (/meta/, historyMetaItem, /history) hits.
  • SDK: client.meta.getAudit (packages/client) has no in-repo caller outside its own tests. packages/client-react has none (git grep exits 1). There is one docs example.
  • The runtime dispatcher's /meta domain serves no /audit (its three-segment branch answers /published only), so this handler is the one owner.

What changed

  • packages/rest/src/rest-server.ts: one shared refusal. A module function refuseNonAuthoringCaller(caller, res, reading) sits beside mayReadPendingDrafts. It asks that predicate. If the predicate refuses, it sends the 403 FORBIDDEN nested envelope and answers true, following the refuseRepeatedQueryParams convention. #20440 wrote this guard inline at the head of /history and /diff. Both heads now call the helper with their own door names, so their answers are byte-identical to before. /audit calls it at its head too. Three inline copies of one refusal would be three places to drift, so the three doors share one function. Only the door's own name differs: "Reading a metadata item's audit trail" here.
  • The /audit handler. It resolves its caller once at the head (auditCtx). The organization scope further down reads that same value instead of a second resolution. Admitted callers read exactly what they read before: the #20156 per-caller refusal (eventDoorRefusal), the #9426 501, the #20139 limit parse and the #8747 organization scope are unchanged. The #8747 comment that said the route "carries no capability gate" now says that was true then, and that the scope still does the tenant separation for the builders the gate admits.
  • Tests.
    • meta-history-diff-authoring-door.test.ts (real stack). The existing authoring-door file now runs every pin over /diff, /history and /audit, and its drafts are saved by the author caller, so the actor a refusal must never carry is a real one:
      • For app and view, the member gets 403 FORBIDDEN. The envelope keys equal those of the member's own /meta/_drafts answer.
      • The published, draft-only and missing names answer byte-identically.
      • The answer carries no event key, note, actor or occurredAt. auditMetaItem joins the spies that stay uncalled, and a builder's call on the same door proves the spy is live.
      • An unparseable limit answers the member the same refusal, while a builder gets 400, so the member's refusal is decided before the parse.
      • Every builder (studio.access, setup.access, manage_metadata) reads both saves of app/atlas and view/opportunity.pipeline: save:allowed:active, and save:allowed:draft by u_author. Each builder also reads the draft-only items' draft event.
      • The one-predicate pin covers all four doors.
    • meta-alternate-door-read-gates.test.ts (the #20156 census). The /audit row gains authoring: true. Each refused census cell also asserts that auditMetaItem was never called.
    • execctx-consumer-census.test.ts. With the umbrella isolated, /audit now refuses an absent context at its own gate, so it moves from the serving list to the refusing list, as /meta/_drafts sits there. The case's title stated the serving list's length wrongly before this change ("six", for a list of five). It now says "four", its length after the move. The caller resolution keeps its .catch on the invocation line and adds no prose mention, so the census's 66 sites, 90 mentions and 13 same-line catches do not move.
    • meta-audit-capability-gap.test.ts and rest-server-audit-org-scope.test.ts ask what an admitted caller gets (the 501, and the organization of the read), so they now call as a manage_metadata holder. Their comments that said the route has no capability gate are corrected.
  • Docs. In content/docs/ui/apps.mdx, the sentence that names the doors needing the capability outright now names /audit beside /diff and /history. In content/docs/api/client-sdk.mdx, one comment beside the client.meta.getAudit example states the authoring-only rule, as the line beside diffItem does.
  • Changeset: @objectstack/rest patch, Clause-②: no. It pulls the declared contract (ADR-0106 D4, 「draft/preview reads are admin-gated upstream」) back in, as the ruling graded the sibling doors.

Verification

All of the following ran at head dc5963dc3a (the branch after merging origin/main e956924e17) unless a line says otherwise.

  • Build: pnpm --workspace-concurrency=2 --filter '@objectstack/rest^...' build exited 0. pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 exited 0, 71 successful, 71 total; the whole-tree gates need it.
  • pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: 216 passed (216) files, 3916 passed | 26 skipped (3942) tests.
  • pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2: 1 file, 8 passed (8).
  • pnpm --filter @objectstack/rest typecheck exited 0: check:test-typecheck: OK, 0 files in the debt ledger.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 91 commands; all 91 ran and each exited 0. --ran with the exit codes recorded: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).
  • pnpm lint (the whole repository) exited 0 in 29s.
  • node scripts/check-issue-citations.mjs --base origin/main exited 0: 8 citations, all resolve.

Ablation (H3), at 8ca554d06c, before the merge; the merge touched no file here. The tests import ./rest-server.js relatively, so no dist sits between the mutation and the run. Through scripts/ablation-replace.mjs, inside a script with its own EXIT INT TERM restore trap, the /audit head's if (refuseNonAuthoringCaller(auditCtx, res, became if (false && refuseNonAuthoringCaller(auditCtx, res,. The anchor went from 1 to 0 and the marker from 0 to 1, read on disk inside the mutation. The six related files then ran:

red green
14 of 356, every one an /audit refusal pin: the 9 census cells for the caller who may not read drafts; the member pins for app and view; the limit pin; the one-predicate pin; and the execctx census's isolated-umbrella case every builder pin; every /diff and /history pin; both /layers controls; the capability-gap and org-scope files; the draft-door census

The restore was proven: the blob is 0ab6c5c1edbb, equal to HEAD, and git diff HEAD is empty (0 bytes), with a clean git status.

Acceptance notes

  • The refusal message is the one byte-level difference from /meta/_drafts, as on the sibling doors. It names the door ("audit trail"), never drafts. It is not pinned, since no consumer parses it.
  • The helper reaches past the claim's surface. The claim named "the /audit handler only". Sharing one refusal meant replacing the inline guard at the head of /history and /diff with a call that sends the same bytes, which is partition 3 of the dispatch. The #20378 pins for those two doors are unchanged and green, and the ablation shows none of them depends on the /audit guard.
  • Who loses the door. /audit also lists denied and forced attempts, not only draft saves. A caller without an authoring capability now reads none of them. Triage's grade makes this choice over a member log with only the draft rows removed, because such a log reads as true and complete. A manage_org_presentation holder, who may save org-scoped views, is refused /audit, as they already are /history, /diff and /meta/_drafts.
  • objectui. A caller without an authoring capability who opens the metadata designer's audit sheet now gets the 403 there, as the history sheet has answered them since #20440. No objectui change is needed; AuditPanel renders load errors.
  • The sibling card is separate. The mislabelled default /diff range ([finding] GET /meta/:type/:name/diff with no from / to labels toVersion as the newest history row (a draft save) while it compares against the active row, so the default diff names the wrong versions #20397) is not addressed here.

Declared narrowing: the verify lock

scripts/pm/os-verify-lock.sh printed this for every build, test, lint and ablation run above (this host is macOS):

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.

pnpm lint

It printed the same disclosure for each of the other wrapped commands: the two builds, the two rest test projects, the typecheck, the targeted runs and the ablation. The 91 derived gates ran directly, as the lock covers only builds and tests.


Generated by Claude Code

hotlong and others added 4 commits September 28, 2026 23:43
…history and /diff refuse

A caller that mayReadPendingDrafts does not admit is refused 403 FORBIDDEN
before the protocol is resolved, the query parsed or any event read. The
refusal is one shared helper, refuseNonAuthoringCaller, now used by the
/history, /diff and /audit doors alike.

Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289
Co-authored-by: Claude <noreply@anthropic.com>
The real-stack authoring-door file now runs every refusal, builder and
one-predicate pin over /diff, /history and /audit, with the drafts saved
by an author. The census row for /audit turns authoring, and the two
/audit route tests whose question is an admitted caller's now call as one.

Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289
Co-authored-by: Claude <noreply@anthropic.com>
…ading

With the umbrella isolated, /audit now refuses an absent context at its
own authoring-door gate, like /meta/_drafts. The audit door's caller
resolution keeps its catch on the invocation line and adds no prose
mention, so the census's site and mention counts do not move.

Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 10 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via diffItem (sdk, the bare tail of client method meta.diffItem, bound to GET /api/v1/meta/:type/:name/diff), getAudit (sdk, the bare tail of client method meta.getAudit, bound to GET /api/v1/meta/:type/:name/audit), getHistory (sdk, the bare tail of client method meta.getHistory, bound to GET /api/v1/meta/:type/:name/history), meta.diffItem (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/diff, selected by route anchor /:type/:name/diff), meta.getAudit (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/audit, selected by route anchor /:type/:name/audit))
  • content/docs/kernel/contracts/metadata-service.mdx (via getHistory (sdk, the bare tail of client method meta.getHistory, bound to GET /api/v1/meta/:type/:name/history))
  • content/docs/plugins/adding-a-metadata-type.mdx (via /:type/:name/history (route, bridged from symbol refuseNonAuthoringCaller — its route source's handler names it))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via /:type/:name/diff (route, bridged from symbol refuseNonAuthoringCaller — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 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 e956924e17b8407ff443b12ccdeefa64d3c5de06 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ee9ab8015ed497e5e20cb98342a6809c0c657f05 — the merge of head dc5963dc3aeea1788b26a88e547a6d9364cafc17 into base e956924e17b8407ff443b12ccdeefa64d3c5de06, 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 ee9ab8015ed497e5e20cb98342a6809c0c657f05 && git checkout ee9ab8015ed497e5e20cb98342a6809c0c657f05
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e956924e17b8407ff443b12ccdeefa64d3c5de06 dc5963dc3aeea1788b26a88e547a6d9364cafc17 && git checkout -B drift-repro e956924e17b8407ff443b12ccdeefa64d3c5de06 && git merge --no-ff dc5963dc3aeea1788b26a88e547a6d9364cafc17

node scripts/docs-audit/affected-docs.mjs --json e956924e17b8407ff443b12ccdeefa64d3c5de06

⚠️ 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 e956924e17b8407ff443b12ccdeefa64d3c5de06 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: dc5963dc3aeea1788b26a88e547a6d9364cafc17
Local-runs: none

① Derived judgments

Inputs: card #20441 (body; grade 5871509797; claim 5873212630 in its amended text of 16:13Z; os-dev report 5873955841), PR #20472 (body, 9-file list, diff against main, +217/−77), ruling 5865708652 read at its source on #20378, PR #20440's diff for the guard comparison, and the head's check-runs, read at 16:19 UTC. The head's rest-server.ts and four test files were fetched raw at the head sha through the REST contents API to read the diff's surroundings. Nothing was built, run or re-run.

  1. GET /api/v1/meta/:type/:name/audit — accept set narrows. Right. The environment-scoped mount shares the handler through registerPerItemRoute, so both paths take it. A caller mayReadPendingDrafts does not admit (no isSystem; none of studio.access, setup.access, manage_metadata in systemPermissions, per isObjectSchemaMaskExempt) is refused 403 FORBIDDEN in the nested error envelope at the head of the handler: after the door's one resolveExecCtx, before resolveProtocol (so before the The REST audit route answers a MISSING auditMetaItem capability with {events: []} — a compliance surface reporting "this item has no audit trail" #9426 501 probe), before refuseRepeatedQueryParams and the declared limit parse, before eventDoorRefusal, before auditMetaItem. That is the grade's direction (ruling B's letter carried to this door) in the ruling's own execution shape: decided on the caller, before any read, the /meta/_drafts shape, a door-named message, one answer for a published, a draft-only and a missing name. The PR's H0 table shows the door was also an existence oracle (a draft-only item answered 200 with one event where a missing name answered an empty events array); the same guard closes it.

  2. refuseNonAuthoringCaller(caller, res, reading) — no public-surface change. Right. Module-level, not exported. The /history and /diff answers are byte-identical to what PR fix(rest): /meta/:type/:name/diff and /history are authoring doors, refused as /meta/_drafts refuses (#20378) #20440 shipped (compared against fix(rest): /meta/:type/:name/diff and /history are authoring doors, refused as /meta/_drafts refuses (#20378) #20440's diff at blob e0f7a215db): the same predicate (mayReadPendingDrafts, truth table preserved by the early return false), status 403, code FORBIDDEN, the same envelope key order (error.code, then error.message), and the same messages — Reading a metadata item's version history requires an authoring capability (studio.access, setup.access or manage_metadata). and Comparing a metadata item's stored versions requires an authoring capability (studio.access, setup.access or manage_metadata). The guard's position on both doors (after the door's one resolveExecCtx, before resolveProtocol) is unchanged. The refactor changes no byte those two doors send.

  3. One owner. packages/runtime/src/domains/meta.ts at the head registers no /audit route (one prose mention only), so the REST handler is the only door to this trail. Right.

  4. Pins (judged as text, not run).

    • meta-history-diff-authoring-door.test.ts: every refusal, builder and one-predicate pin runs over /diff, /history and /audit on the real stack; the drafts are saved by the author caller so u_author is a real actor the refusal must not carry (it is in ANSWER_KEYS); auditMetaItem joins the uncalled spies with a builder live-spy control; the limit pin loops over /history and /audit; every builder reads save:allowed:active and save:allowed:draft and the draft-only items' draft event; the four doors agree on who is refused. Right.
    • meta-alternate-door-read-gates.test.ts: the /audit row is authoring: true; the refusal cell adds auditMetaItem never called — the census's protocol mock declares auditMetaItem as a vi.fn (line 288), so the assertion is live, not vacuous. Admitted callers (reader, author) fall through to the unchanged events branch. Right.
    • execctx-consumer-census.test.ts: /audit moves to REFUSES_ON_ITS_OWN, which asserts 403 FORBIDDEN for an absent context with the umbrella isolated and a non-403 entitled control — consistent with isObjectSchemaMaskExempt(undefined) being false. SERVES_ON_ITS_OWN now holds four routes and the title says four. Right.
    • meta-audit-capability-gap.test.ts and rest-server-audit-org-scope.test.ts: their callers now hold manage_metadata, so each still asks what an admitted caller gets (the 501; the organizationId threading); the absent-context case still expects the umbrella's 401 with auditMetaItem uncalled. Right.
  5. Docs. content/docs/ui/apps.mdx names /audit beside /diff and /history under the shared reason; content/docs/api/client-sdk.mdx puts the authoring-only comment beside getAudit as the ruling asked beside diffItem. Right. The Docs Drift Check's other two hand-written pages name /history and getHistory, whose behavior this diff leaves byte-identical; no edit is owed there.

  6. Check-runs at the read (34 names, newest run per name): 30 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 2 in progress (Test Core (1/6), Test Core (4/6)), 0 failure. Lint & Repo Gates, Check Changeset, Build Core, Build Docs, all five Type Check jobs, Temporal Conformance, the Dogfood gates, Test Core 2/3/5/6 and the claim and single-writer guards have concluded success. No verdict is inferred for the two running shards; the landing waits for them as the seat's gate, not this record's.

② Semver level

  • .changeset/20441-audit-authoring-door.md: @objectstack/rest patch; its body carries Clause-②: no; the PR body's declaration line reads Clause-②: no. Check Changeset: success.
  • Right. The diff narrows who is served on one REST door of a released package by pulling a declared contract (ADR-0106 D4) back in — the class the maintainer's ruling 5865708652 graded for the sibling doors as patch with Clause-②: no, and triage's grade carries that letter to this door. It widens no accept set and adds no public surface (the helper is not exported), so yes would be wrong; the ruling's grading takes no (narrowing) arm; skip-changeset would be wrong for a fix in a released package. The changeset body states what an SDK reader of getAudit now receives and how to read the trail, so the consumer-facing note is present.

③ Boundary flags

The dev report 5873955841 carries open_questions: [], out_of_scope_findings: [] and five deviations. Each is answered:

  • (a) The /history and /diff heads were edited to call the shared helper, beyond the claim's original "the /audit handler only". Answered: the claim 5873212630 in its amended text names exactly this; byte-identity is verified in ① item 2; the grade's "reuse PR fix(rest): /meta/:type/:name/diff and /history are authoring doors, refused as /meta/_drafts refuses (#20378) #20440's guard, do not write a second one" is met in its strongest form — one function, three doors. No other open PR may claim the same single-writer path: success. Accepted.
  • (b) Three more test files changed. Answered: named in the amended claim, and each change follows from the gate (① item 4). Accepted.
  • (c) Two docs lines added where the claim had found none to fix. Answered: named in the amended claim; mirrors the docs parameter of the sibling ruling. Accepted.
  • (d) Attribution. The three non-merge commits end with the model-free pair (Claude-Session: with the session URL, and Co-authored-by: Claude at the noreply address), and the PR footer is the session-URL form. AGENTS.md's commit-message rule prescribes exactly that pair and the pre-push hook refuses a model identifier in it; the harness reminder yields to the repo rule. Accepted.
  • (e) Ablation ran at 8ca554d06c, before the merge. The merge dc5963dc against its first parent touches 27 files: two changesets, docs/releases-maintenance.md, packages/metadata-core, packages/metadata, packages/platform-objects and packages/spec — none under packages/rest or content/docs. The report's "only spec and metadata-core" under-enumerates, but the claim that matters holds: no file of this diff moved between the ablation and the head, and the head's own Test Core shards are the post-merge answer. Accepted.

Stop valve (grade 5871509797). The dev measured no member-facing consumer: objectui at pin dd3f7e1be3 has one caller, AuditPanel, mounted only in MetadataResourceEditPage (the metadata designer, beside the history sheet #20440 already gated); cloud at 3efda046 has zero callers with a same-shape control query that hits; the SDK's getAudit has no in-repo caller outside its tests and one docs example. This review did not re-measure (outside the brief's inputs); on the dev's measurement the valve did not trip and no needs_decision is owed. Recorded as the dev's reading.

No note is owed on #20156. Ruling 5856774816 item 2 covers the three stored-version doors (/layers, ?layers=true, /diff); /audit is an events door it never ruled on, so the narrowing note the sibling ruling asked for does not extend here.

No escalation.

Implemented-by: claude/issue-20441-audit-authoring-door
Reviewed-by: local_1d2a197c-c20e-4e90-9be8-413d4d432289

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 16:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 8e02859 Sep 28, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20441-audit-authoring-door branch September 28, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] GET /meta/:type/:name/audit lists pending draft-save events (actor, time, note: draft) to a member who may not read drafts

1 participant