Skip to content

refactor(plugin-webhooks,platform-objects)!: 18.x step 7c: retire the step 1–3 dual reads (declared-webhook mirror skip, sys_setting_audit global scope) - #24836

Merged
objectstack-fleet[bot] merged 55 commits into
mainfrom
claude/issue-24583-7c-dual-reads
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 55 commits into
mainfrom
claude/issue-24583-7c-dual-reads

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #24583 (step 7, part 7c: the step 1–3 dual reads). #24583 remains open: parts 7a and 7b land separately.

Clause-②: yes (narrowing)

Base. Branched from origin/main merged with claude/issue-24581-ceremony-open (#24681, steps 5+6), with merge commits only. #24681 has merged (12d14765c), and main was merged in again at 78f99c34c. The one conflict was my edit of declared-webhooks.ts against the squash of the same lines; plugin-webhooks is byte-identical between #24681's head c6f775cbd and main. The deferred migration.mdx regeneration is discharged at 8f6bf26e6 and equals main. The diff against main is 26 files, +252 / −291, with no governed path.

What changes

plugin-webhooks: step 1's dual read retires.

  • classifyDeclaredNameHolder is deleted. So are the dispatcher's mirror skip (AutoEnqueuer's mirrorIds) and the redeliver guard's mirror branch.
  • Any sys_webhook row that holds a declared webhook's name now answers as itself, and the declaration is withheld, reported once per name (regime C). The dispatcher's holder read asks for id and name only.
  • A delivery whose ref_id names a row is judged by that row.
  • Docblocks are rewritten in declared-webhooks.ts, auto-enqueuer.ts, redeliver-guard.ts, sys-webhook.object.ts, webhook-outbox-plugin.ts and webhook-provenance.ts.
  • bindWebhookProvenanceStamp is kept, not deleted (measured below: customized still has a reader).
  • migrateLegacyWebhookSecrets: only its docblock narrows (measured below: no code narrowing is possible).

Settings audit: step 2.

  • sys_setting_audit.scope no longer declares global, so the engine refuses a global-scope row there (VALIDATION_FAILED on scope).
  • The four platform-objects translation bundles and the es-ES source hashes are regenerated (node scripts/check-i18n-bundles.mjs --write, one leaf each).
  • The "history reads both tables" docblocks are fixed in settings-service-plugin.ts and sys-platform-setting-audit.object.ts.

Identity mail: step 3, docblocks only. sys-identity-email.object.ts, sys-email.object.ts, email-log-objects.ts and email-plugin.ts now say the ceremony moved the rows. Nothing is removable:

  • The body read gate on sys_email stays, because the move selects rows by shape.
  • A mail job with no object still reads sys_email, because a queued tenant mail row stays there.

Pins rewritten to the new rule, none weakened:

  • declared-webhooks.test.ts §3 and its redelivery case.
  • sys-setting.scope-options.test.ts: the audit table's options are the spec enum minus global, and the sibling's are ['global'].
  • settings-deployment-level-expand.test.ts: the dual-read case becomes a refusal pin plus a tenant-rung control.
  • The es-ES echo ledger: 45 → 44 echoes.
  • settings-audit-global-rung-sibling.dogfood.test.ts: part B now boots over the same file and pins the refusal, with its control.

Regime C, recorded and pinned

Step 5a leaves a seeded row whose name no installed package declares, and reports it. If a package declares that name later, the row now withholds the declaration and answers as itself. Before, it was skipped as a pristine mirror. The remedy is to rename or delete the row. Pinned by a seeded row the ceremony left, its name declared later, withholds the declaration (declared-webhooks.test.ts).

Premise measurements ("after certification")

  • Certified database, from the 18.0→HEAD harness after the ceremony (green on 8f6bf26).

    • Every sys_webhook copy under a declared name is deleted, and its deliveries are re-sourced to declared:NAME.
    • SELECT COUNT(*) FROM sys_setting_audit WHERE scope = 'global' returns 0.
    • Both assertions sit in upgrade-18x-harness.dogfood.test.ts.
  • Fresh birth-marked install. R1 below, after a global setting write through the settings door:

    • sys_setting_audit: 1 row, 0 global.
    • sys_platform_setting_audit: 2 rows, 2 global.
    • sys_webhook: 0 rows.
    • The settings writer sends a global-scope entry to the sibling (settingAuditObjectFor).
  • Walled boot. The same reading under isolated with the real @objectstack/organizations: 1/0, 2/2, 0.

  • customized still has a live reader: the ceremony. runDeclaredWebhooks in organization-ownership-apply.ts settles a seeded row by it: a pristine copy is deleted, an edited one is kept as a clone. The ceremony re-runs from the start when a superseding reviewed plan replaces its checkpoint. A seeded row 5a kept can be edited and then declared later, so dropping the stamp would let that re-run delete an administrator's edit. "Once nothing reads customized" does not hold, so the stamp stays and its docblock names the ceremony as its reader.

  • migrateLegacyWebhookSecrets. What 5a leaves:

    • authored rows;
    • the ceremony's clones, which keep their definition_json;
    • seeded rows no package declares.

    Any of these can still carry a cleartext blob, and the sweep is keyed on the data. So the code stays, and the docblock now names exactly these rows.

  • no-organization-object. The one shipped composition with no organization object is examples/embed-objectql, a lean ObjectQL embedding. It uses neither webhooks nor settings, so none of 7c's readers is on its path. Every objectstack.config.ts app composes plugin-auth, which registers sys_organization. The os init output was not measured.

Proof (head 8f6bf26 unless stated)

  • R1. pnpm dev -- --fresh -p 38963: health 200, seeded-admin sign-in 200, 0 error lines in the boot log. Readings above.
  • R2 + R3.
    • upgrade-18x-harness.dogfood.test.ts: 8/8.
    • organization-ownership-boot-refusal-order.dogfood.test.ts passes.
    • identity-mail-read-gate.dogfood.test.ts uses ceremoniedFixture and passes.
    • The six-file dogfood run totals 50/50, together with settings-audit-global-rung-sibling, webhook-materialization and wall-declared-webhook-deployment-wide.
  • R4.
    • OS_TENANCY_POSTURE=isolated os dev --fresh --seed-admin, run from a scratch app that re-exports the showcase config and declares the real @objectstack/organizations: health 200, sign-in 200, 0 ERROR lines. The banner lists the Organizations plugin, and the walled membership-policy gate was satisfied with invite-only.
    • Step 4's 7 walled e2e cases (OS_TEST_TIERS=nightly, serve-organizations-host-resolution + serve-organizations-mount-failure): 7/7.
  • Suites.
    • plugin-webhooks 222/222 (21 files).
    • service-settings 895/895.
    • platform-objects 1199/1199.
    • plugin-email 571/571.
    • typecheck exits 0 for all four. check-i18n-bundles: all bundles in sync.
  • Gates.
  • Ablation of the main pin. Through scripts/ablation-replace.mjs, the mirror skip was put back into AutoEnqueuer.resolveDeclared: a pristine package row is not counted as a holder. The anchor went 1 → 0 and the blob changed. declared-webhooks.test.ts went red, 2 failed / 20 passed: the regime-C pin and the switched-off-row pin. After the restore, git diff HEAD was empty. The test imports the source by relative path, so no dist rebuild was involved.

NOT MEASURED

  • os init output reaching no-organization-object: out of 7c's readers' reach, not booted.

Acceptance notes

  • Under isolated, the showcase's seed prints [tenant-audit] warnings ("bulkCreate on tenant-scoped object ... without options.tenantId"). They exist before this change and are not touched here.

Generated by Claude Code

claude added 30 commits October 19, 2026 13:06
…r retired; webhook activation door

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…he 18.0 dual read; door pins for webhook

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…asserts no row and a dispatch

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…the checklist, the webhooks guide and the showcase; changeset

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…tcher's switch read

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…posture builds no wall, as its 18.0 seeded copy stamped with the Default Organization was

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…red writes; the declared-webhook double is drivable by the limit gate

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…does not implement

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…authors; checklist and admitsOrganization docblock say a declared webhook is deployment-wide under single

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…the fixture's customized declared-webhook mirror

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…n fail on a double delivery

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…0g-webhooks-registry

# Conflicts:
#	content/docs/permissions/tenant-audit-census.mdx
#	docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…0g-webhooks-registry

# Conflicts:
#	content/docs/permissions/tenant-audit-census.mdx
#	docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…-registry' into claude/issue-24581-ceremony-open
…he ceremony, then boot; boot-refusal-order step 2 back on the binary
…el — the cipher names no organization

Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Co-authored-by: Claude <noreply@anthropic.com>
…sys_flow_credential leave reach)

Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Co-authored-by: Claude <noreply@anthropic.com>
… retired old home — sys_setting_audit refuses a global-scope row

Claude-Session: https://claude.ai/code/session_01KMpT5c9dcX9kj3rBBWaki8
Co-authored-by: Claude <noreply@anthropic.com>
…-dual-reads

# Conflicts:
#	packages/plugins/plugin-webhooks/src/declared-webhooks.ts
…ration.mdx, equal to main)

Claude-Session: https://claude.ai/code/session_01KMpT5c9dcX9kj3rBBWaki8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/platform-objects, @objectstack/plugin-email, @objectstack/plugin-webhooks, @objectstack/service-settings, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/platform-objects/src/audit/sys-identity-email.object.ts, packages/platform-objects/src/system/sys-platform-setting-audit.object.ts, packages/plugins/plugin-email/src/email-log-objects.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/webhooks.mdx (via AutoEnqueuer (symbol, a top-level class), SysWebhook (symbol, a top-level const object), managed_by (literal, a string literal in holders))
  • content/docs/deployment/cli.mdx (via managed_by (literal, a string literal in holders))
  • content/docs/permissions/authorization.mdx (via managed_by (literal, a string literal in holders))
  • content/docs/permissions/permission-sets.mdx (via managed_by (literal, a string literal in holders))
  • content/docs/permissions/system-context.mdx (via sys_setting_audit (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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

  • content/docs/releases/v12.mdx (via managed_by (literal, a string literal in holders))
  • content/docs/releases/v15.mdx (via managed_by (literal, a string literal in holders))

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
  • 7 changed file(s) yielded no anchor (packages/platform-objects/src/audit/sys-identity-email.object.ts, packages/platform-objects/src/system/sys-platform-setting-audit.object.ts, packages/plugins/plugin-email/src/email-log-objects.ts, …) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 146 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 1366740c6c80be43b18b6b72a68736032245e3e9 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 1366740c6c80be43b18b6b72a68736032245e3e9

⚠️ 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 1366740c6c80be43b18b6b72a68736032245e3e9 → 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: TIER_DEFAULT
Head-sha: 8f6bf26e61f0f3d2a49359d17b3d10d4f663b6a7
Local-runs: none

PR #24836 · card #24583, step 7 part 7c · merge-base ddb3360a4954c1ccb6878deb3ca0b303c79d3289 · 26 files, +252/−291 · face: declaration (Clause-②: yes (narrowing) on the PR body and in .changeset/24583-7c-dual-reads-retired.md). Read: the diff, the dev report (6287753302), the claim (6284522037), the step-1 settlement notes (6271949316 on #24581), ADR-0131 D2/D6/D7/D10, the ceremony on main (packages/cli/src/utils/organization-ownership-apply.ts, landed by #24681 at 12d14765c), and the head's check-runs. Nothing built, run or re-run.

① Derived judgments

1. Webhooks — the dual read is gone; what remains is regime C, and it is pinned.

  • Deleted, confirmed against main: classifyDeclaredNameHolder (main declared-webhooks.ts:264; no definition at head — the only surviving mention in the tree is liveness-ledger prose, residual R2); AutoEnqueuer.resolveDeclared's mirrorIds and the continue skip (main auto-enqueuer.ts:475, :564-567, :586); the redeliver guard's mirror branch (main redeliver-guard.ts:143-146) and judgeDeclared's known parameter. At head the holder read asks fields: ['id', 'name'] (auto-enqueuer.ts:547), every holder lands in heldByRow (:560), and the declaration is withheld and said once per name by reportDeclarationWithheld (:614-618).
  • Can a pristine mirror still exist after the ceremony? On a certified database, no. runDeclaredWebhooks (organization-ownership-apply.ts:3178) selects every managed_by package/platform row (:3187-3188); for each name a package declares it deletes a pristine or switch-only copy after re-sourcing its deliveries to declared:NAME (:3256-3262, the resource closure ≈:3225-3240 throws if any delivery still names the row), or clones an edited copy under a free name with managed_by 'admin' and customized false (:3264-3286); a customized copy under a wall blocks the step (:3209-3218), so that database is not certified. On a fresh install there was never a seeder: bootstrapDeclaredWebhooks has no definition in the tree, and no live code writes managed_by: 'package' or 'platform' into sys_webhook. The one survivor is the 5a leftover — a seeded row whose name no installed package declares (:3201-3208, kept and reported). If a package later declares that name, the row now withholds the declaration instead of being skipped. That is regime C (ADR-0131 D6: same-name shadowing is retired, a declaration arriving over an existing row is reported and never becomes an overlay; the remedies are rename or delete the row, or clone plus the sys_metadata_activation switch), and declared-webhooks.ts:59-76 records exactly that. Pinned: declared-webhooks.test.ts:470 ("a seeded row the ceremony left, its name declared later, withholds the declaration: one delivery, from the row" — one call with refId: 'whk_left' at the row's URL, not the declaration's task-v2; one warn; no writes) and :492 (row active: false, ledger true: nothing fires — the row governs, not the ledger).
  • Deliveries whose ref_id is a mirror row id: on a certified database 5a re-sourced them before the delete, and the step refuses to delete while one remains. A delivery naming a clone keeps the clone's id, and the clone row exists; one naming a 5a leftover names a live row. The guard judges each by the row: redeliver-guard.ts:123 (declared refs go to judgeDeclared), :126-130 (row read), :132 (row gone → refused, fail-closed), :147 (inactive → refused). No misjudgement. Pinned at declared-webhooks.test.ts:590 (a pristine holder under a switched-off declaration: replay allowed, by the row) and :600.

2. Kept on measurement — both right, both complete-feature-safe.

  • bindWebhookProvenanceStamp (webhook-provenance.ts:70; bound at webhook-outbox-plugin.ts:330). The claim's condition, "once nothing reads customized", does not hold. The ceremony reads it at organization-ownership-apply.ts:3188 (the select), :3209 (the wall blocker) and :3256-3257 (delete vs activation row vs clone), and the apply "starts over (each step is idempotent)" when the reviewed plan's inventory digest supersedes the recorded checkpoint (:205-207; restartedFrom at :596 and :4138). Without the stamp, an administrator's edit of a 5a leftover whose name a package declares later would read as pristine on that re-run and be deleted. Keeping it is correct; its docblock names that reader (webhook-provenance.ts:6-15), and the stamp stays pinned (declared-webhooks.test.ts:526; webhook-provenance.per-row.test.ts, unchanged).
  • migrateLegacyWebhookSecrets (migrate-webhook-secrets.ts:87): docblock only — the file's 23 changed lines all sit in the header comment (:8-27); no code line moves. The narrowing in prose is correct and complete for the code: authored rows, the ceremony's clones (which keep definition_json, :3273) and undeclared seeded rows can each still carry a cleartext blob, and the sweep is keyed on data, so a provenance filter would skip rows that still hold a key. Complete-feature-safe: on a fresh install the sweep finds nothing and is idempotent; nothing it reads was removed.

3. Settings audit — global retired from sys_setting_audit.scope, declared as a narrowing.

  • Writers. buildSettingAuditWriter routes by settingAuditObjectFor (settings-service-plugin.ts:572-573, :618): a global entry inserts into sys_platform_setting_audit (:651); the sys_setting_audit insert is the non-global branch only (:655). config-change-audit.ts writes sys_audit_log (:300), never this table. No other writer in packages/, apps/, examples/ or scripts/. The only scope = 'global' rows in the tree are the 18.0 fixture's (packages/qa/dogfood/test/upgrade-harness/fixture-18.0/database.sql:660, :662), which 5b moves: SIBLING_MOVES global-audit-move selects by scope, never by NULL (organization-ownership-apply.ts:3353-3359), as the step-2 settlement note required, and the harness pins COUNT(*) WHERE scope = 'global' = 0 on the certified file (upgrade-18x-harness.dogfood.test.ts:248). So after 5b no global row remains on a certified database, and the v18 boot guard refuses an uncertified one.
  • A stored global row, were one to exist: nothing on read. Select options are validated in the record validator (record-validator.ts:55-57), which the write paths call (engine.ts:17827, :18172); no read path validates, so the row reads back as stored and shows as a select-option miss (its translation leaves are gone). keptOptionValues is the import's exception (import: createMissingOptions keeps an unmatched select value through coercion, then the engine's option check refuses it anyway — the setting has no effect on a writable field #22183), not a reader's. A later update of such a row would be refused on scope — moot for an append-only ledger. A new insert is refused VALIDATION_FAILED on scope, pinned twice: settings-deployment-level-expand.test.ts:217 (refused before the driver; the writer's global row lands in the sibling; control at :211) and the dogfood settings-audit-global-rung-sibling.dogfood.test.ts:134 (booted again over the same file, through the system context; control at :127).
  • Declaration. The changeset carries Clause-②: yes (narrowing), the **BREAKING** banner and one ADR-0087 marker, not-required (no-migration-prescription), with its reasons (no spec key, export, type or error code removed; a stored select value refused on one platform object; both narrowings reach only rows the ceremony settles before certification). AGENTS.md §3 (:1087-1101): (narrowing) is BREAKING, yes takes at least minor, exactly one marker — met; Check Changeset is green on this head. Spec liveness needs nothing: sys_setting_audit.scope is a platform-object field, not a packages/spec property, no ledger row names it, and Spec property liveness is green. The four translation bundles (en, es-ES, ja-JP, zh-CN) each drop the global leaf, the es-ES source hash for it is gone, and the echo ledger steps 45→44 with its decision line removed (objects-es-es-echo-decisions.test.ts:245-248).

4. Identity mail — docblocks only. sys-email.object.ts:100-111 (comment lines inside the gate's rationale), sys-identity-email.object.ts:22-25, email-log-objects.ts:15-21, email-plugin.ts:1000-1002 (a comment above the unchanged sweep). No executable line changes in plugin-email or in the two platform-objects mail objects; the body read gate and the no-object mail job's sys_email read stand, as the claim said nothing was removable.

5. The two object files in the diff. sys-webhook.object.ts: two docblock hunks — the header's account of the 18.0 rows (:20-24, now regime C) and the managed_by field comment (:275-277: customized is the ceremony's record, no longer the dispatcher's switch). Why: both described the dual read this PR retires; the field list, options and gates are untouched (managed_by: Field.select( at :278). sys-platform-setting-audit.object.ts: two docblock hunks — "one reader reads both tables" → "either table … never global there" (:15-16), and "a reader of the global rung's history reads both" → "this table alone" (:30-33). Why: the claim's "history reads both tables" docblock fix. scope stays ['global'] (:86-87), pinned at sys-setting.scope-options.test.ts:64.

6. Tests — rewritten to the new rule; nothing weakened. declared-webhooks.test.ts §3: the four old mirror-skip pins are replaced by their inverses (:470, :492, :590); "customized, switched off, said once" folds into :470 and :492, since customized no longer participates in dispatch; the stamp pin (:526) keeps the customized: true assertion and drops its dispatch half, which the rule no longer owns. :508, :534, :552, :600 are unchanged in substance. sys-setting.scope-options.test.ts:64 now pins the split both ways (audit table = spec enum minus global; sibling = ['global']). The expand test and the dogfood test each replace the dual-read case with a refusal plus a tenant-rung control. The ablation (dev-reported, not re-run here): the mirror skip restored in resolveDeclared reds :470 and :492 — the two regime-C pins — and nothing else, which matches the rule's surface.

7. Check-runs on 8f6bf26e6 (polled 09:32Z): 26 success, 7 skipped, 7 in progress — Test Core 1/6 to 6/6 and Type Check · workspace. No failure. The dogfood shard matrix is skipped for this PR's paths; the dogfood rewrites rest on the dev's reported 50/50 run.

② Semver level

minor on @objectstack/plugin-webhooks and @objectstack/platform-objects, with the ! title and the **BREAKING** banner: a run-time narrowing (a pristine seeded holder no longer yields to its declaration; a stored select value refused on one platform object), with no spec key, export, type or error code removed. This is the repo's pre-GA rule — ADR-0087 "Ratified: the pre-launch launch-window exemption", amended 2026-09-13 (#18003): a metadata-facing break ships minor, carrying the banner and its ADR-0087 disposition — and the same wording as the precedents in plugin-webhooks 18.0.0-next.1 and platform-objects 18.0.0-next.3. Level correct. Clause-②: yes (narrowing) is the right line: measured (R1–R4, the harness, four suites) and a narrowing.

③ Boundary flags

  • Governed paths: none (0/26; Governed Surface Queue Guard green). ⛔ Not the ceremony files of feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 and ⛔ not docs/adr/** — both held.
  • Base discipline: branched from main merged with the ceremony branch under 6270035914; feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 landed (12d14765c), main merged again (78f99c34c), and the deferred migration.mdx regeneration is discharged at the head, equal to main.
  • Residuals, none blocking:
    • R1 packages/platform-objects/src/system/sys-setting.object.ts:133-135 still says the sibling audit object "keeps all three, because the audit writer records a global-scope change too" — false after this PR. Fix: one line, "keeps tenant and user; the global rung's audit row lives in sys_platform_setting_audit".
    • R2 packages/spec/liveness/webhook.json, props.name.evidence (verifiedAt 2026-10-19), reads "classifyDeclaredNameHolder decides which answers" — a deleted symbol, in prose the liveness gate does not resolve (it checks path#symbol anchors; auto-enqueuer.ts#resolveDeclared still exists, so the anchor holds). Fix: amend the parenthetical to "the row holding it answers and the declaration is withheld (regime C)". Outside the claim's file surface, so no breach.
    • R3 .changeset/24529-declared-webhooks-from-registry.md:25 (step 1, unreleased) still teaches the mirror skip; it and this changeset ship in one CHANGELOG. A one-line "superseded in 7c" note would spare the docs sweep a hit. Not 7c's file.
    • R4 The changeset attributes the launch-window convention to "ADR-0131 D2, D6, D7"; those decisions motivate the change, while the convention is ADR-0087's section named above. Wording only.
    • R5 Dev-reported deviation: commit 174abe92a carries a trailer naming a model; later commits do not, and history on the shared branch was not rewritten. A squash on landing resolves it.

Implemented-by: claude/issue-24583-7c-dual-reads
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

…e, the webhook liveness evidence, 24529's pending note (deliberate correction), the launch-window citation

Claude-Session: https://claude.ai/code/session_01KMpT5c9dcX9kj3rBBWaki8
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat addendum · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-20T09:45Z

Head cdd162150. The review PASSED at 8f6bf26e6 (6287997289). Since then the branch has taken one clean merge of main (1366740c6) and one text-only commit for the review's R1–R4.

In-seat check: PASS.

  • Across the PR's 29 files, only four carry changed hunks against the reviewed head:
    • sys-setting.object.ts: a comment;
    • packages/spec/liveness/webhook.json: props.name.evidence prose;
    • .changeset/24583-7c-dual-reads-retired.md: the ADR-0087 citation;
    • .changeset/24529-declared-webhooks-from-registry.md: below.
  • No code changed.
  • The dev reports check:generated, check:liveness, platform-objects 1199/1199 and plugin-webhooks 222/222, all green.

Deliberate correction (#17712). .changeset/24529-declared-webhooks-from-registry.md is an unreleased note from another PR. Its two 18.0-database bullets taught the mirror skip that this PR retires. They are corrected in place, not restored: the ceremony deletes the pristine copy and re-points its deliveries at declared:NAME, and the boot guard refuses until certification. Check Changeset is red on that file by design; it is not a required check. The seat batches this correction, with any others step 7 makes, into one confirmation card for the maintainer.

Not governed; 556 changed lines. The seat marks it ready and arms auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 09:46
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 09:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit bd6dab0 Oct 20, 2026
42 of 43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24583-7c-dual-reads branch October 20, 2026 10: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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants