Skip to content

fix(metadata-protocol): refuse a non-canonical stored type at the publish pre-flight, and assert it at the audit writer (#8908) - #8986

Merged
os-zhuang merged 7 commits into
mainfrom
claude/issue-8908-manifest-absent-plural-promote-refusal
Aug 16, 2026
Merged

fix(metadata-protocol): refuse a non-canonical stored type at the publish pre-flight, and assert it at the audit writer (#8908)#8986
os-zhuang merged 7 commits into
mainfrom
claude/issue-8908-manifest-absent-plural-promote-refusal

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#8908

Two tightenings, ruled as ONE card (auto-adjudicated 2026-08-15, option (a) plus the audit-writer question carried onto the same card). Draft: the maintainer's veto window on that adjudication is still open.

The defect

publishPackageDrafts reads sys_metadata rows at rest, so #7894's /meta boundary fold never reaches it. promoteDraftForPublish folds the stored spelling through PLURAL_TO_SINGULAR — the manifest-collection map, which legitimately omits types that are not stack collections. For those the fold is a no-op: the lookup key equals the stored spelling, the draft resolves, and the publish mints an ACTIVE row in the namespace PUT /meta/field/… answers 403 NOT_OVERRIDABLE for. Every registry read and every compliance query on field then misses an item the platform just reported as published.

The ADR-0010 row recorded the same plural, because recordMetadataAudit re-folded through the same incomplete map — tolerant AND incomplete: canonical for the 29 types that never needed it, non-canonical for exactly the ones that did.

1. The publish refuses it, at the pre-flight, batch-atomically

Same shape as the ADR-0028 namespace-prefix gate already standing there: found before anything is promoted, failing the whole batch (publishedCount: 0, published: []) rather than publishing the healthy siblings around it, one audit row per violation. failed[].code is the new STORED_TYPE_NOT_CANONICAL; the audit column's spelling is stored_type_not_canonical. The message names the row, names the canonical type, and states the re-author path.

The rule is derived, not a list. A spelling the URL/registry map folds elsewhere and the manifest map leaves unchanged. Against the real maps that is six spellings, where the card named four:

gate would refuse: ["fields","seeds","external_catalogs","externalCatalogs","translations","email_templates"]
canonical types whose own spelling folds elsewhere (must be empty): [] ← 33 of 33
non-canonical spellings NOT refused (manifest-present): 29 ["objects","apps","views", …]

externalCatalogs and email_templates would have been missing from any hand-written list of four, and a newly declared type that never reaches the manifest map is covered on the day it is declared. The empty second line is the safety proof: no legitimate stored type can trip the gate.

A manifest-present plural (objects) is deliberately not in the class — it is already fail-closed at the promote (NO_DRAFT, batch aborted, pinned with its reasoning in protocol.publish-side-effects-canonical-type.test.ts), and widening the gate over it would change a wire-visible failed[].code for rows that are not this defect. A pin marks that boundary so a later "simplification" is visible.

⛔ Deliberately not included: migrating the row (_migrate-stored / boot reconciliation). That was option (b), explicitly unruled.

2. recordMetadataAudit asserts its type instead of folding it

type: PLURAL_TO_SINGULAR[entry.type] ?? entry.type is gone. The writer now refuses a non-canonical type with AUDIT_TYPE_NOT_CANONICAL / 500, and every call site that builds a row out of an at-rest type — all of them on publishPackageDrafts — folds with canonicalMetaType. The throw sits outside the writer's best-effort try, because inside it the method's own catch would degrade the assert into a console.warn — the softening the ruling forbids, reached by accident rather than by decision.

The assert cannot refuse a canonical type, nor a plugin-registered or otherwise unrecognised kind (canonicalMetaType is the identity for anything the static map does not carry): the accept set is narrowed, not closed.

Premise-first: the enumeration the ruling required

Every recordMetadataAudit call site — the method is private to protocol.ts, 11 call sites, plus 2 entry producers (lockWriteRefusal, optimisticConflictAuditEntry) reached through recordPendingDenialAudit:

sitetype argumentcanonical?
saveMetaItem, publishMetaItem, rollbackMetaItem, deleteMetaItem (allowed rows), assertLockAllowsDelete, recordOptimisticConflictAudit × 3request.typeyes — canonicalizeMetaRequestType folds at every entry point before any of them
publishPackageDrafts pre-flight rowsv.type (stored)no → folded here
publishPackageDraftsbatch_abortedcausal.type (stored)no → folded here
publishPackageDrafts allowed rowsp.d.type (stored)no → folded here
promoteDraftForPublish pending rows (lock / 409)request.typeno on the batch route → the batch now passes the folded type

Every at-rest row producer.SysMetadataRepository is instantiated in exactly one place (protocol.ts) and its put is called from exactly one place (saveMetaItem, post-fold). promoteDraft mutates an existing row's state and cannot mint a spelling. migrateStoredMetadata rewrites through saveMetaItem, also post-fold. So no code path in this repository can mint a non-canonical stored type today — the residue is historical, exactly as the card states, and the batch publish was the only route by which it reached an audit write.

Fork clause: not triggered. The closest signal is packages/core/src/fallbacks/authored-translation-sync.ts, which reads type: 'translations' at rest — but it is a read-side legacy fallback over ACTIVE rows, untouched by a refusal to promote. No legitimate path turns a working audit write into a failure.

Also fixed, as a consequence of the fold

On the batch route getEffectiveLock's overlay limb was queried with the raw stored spelling, so an ADR-0010 _lock on the canonical active row was looked up under a type no row has and came back 'none' — the verdict "the author declared no protection". That is the batch twin of the hole #8769 closed on publishMetaItem.

Verification — union run at 92dbd15a8 (the final commit, post-merge)

packages/metadata-protocol/src/protocol.publish-stored-type-canonical.test.ts, 12 cases, both zones, each refusal paired with a positive control in the same describe.

Ablations, run SEPARATELY — the two limbs are independent and each is shown load-bearing on its own. Directions predicted before running; measured directions recorded in the test file header, including where they differed from the prediction:

ablationpredictedmeasured
pre-flight gate disabledZone A red with a wrong refusal, not a success; Zone B green4 red / 8 green.fields[not_overridable] Metadata type 'field' is not draftable; email_templates[no_draft] No pending draft exists for email_template/welcome. Two different wrong verdicts, plus the batch case reporting the healthy sibling in failed[] (['ticket', 'legacy_field']) because the abort moves inside the transaction
writer assert removed, silent fold restoredZone B red; Zone A green3 red / 9 green, each promise resolved "undefined" instead of rejecting — the writer silently accepting the spelling, the defect verbatim

Restored from the commit and proved byte-identical (git hash-object = git rev-parse HEAD:… = d4642cbe9), not retyped.

Suites (edited packages, then the DOWNSTREAM/dependents direction — --filter '...@objectstack/metadata-protocol'):

post-merge, at 92dbd15a8:
spec 406 files / 10719 tests metadata-core 10 / 162
metadata-protocol 107 / 1524 objectql 211 / 3721
rest 119 / 1962
pre-merge, at bd77ef36d (same code, older base):
cli 122 / 1358 dogfood 111 / 782 (3 skipped)
http-conformance 4 / 72 plugin-audit 16 / 242

main moved 11 commits under this branch mid-run, one of them (#8371, PR #8936) into protocol.ts itself. It landed in the findData filter gate, structurally disjoint from both edited regions — merged at 75b41c0de, then everything above was re-run on the merged tree rather than trusted.

Gates, all green at 92dbd15a8 — re-run in full AFTER merging origin/main (b50c0ef27), not before: nul-bytes, error-code-casing, meta-type-normalized, stack-collection-maps, durability-log-level, filter-alias-parity, spec-parsed-alias, merge-driver, cross-package-test-inputs, tenant-chokepoint, query-options-erasure, route-envelope, adr-0087-registration, empty-changeset, type-check-coverage, doc-anchors, doc-authoring, type-check-debt --re-measure (33 ledger entries re-measured in 417.5s on a fully built closure, 1926 raw errors, none above its recorded number — metadata-protocol's own entry is 63 and did not move, so the new test file adds zero), plus the spec package's check:generated, check:authorable-surface, check:api-surface, check:spec-changes, check:upgrade-guide, check:migration-registry, check:docs.

Run beyond the derived set, and it earned it. meta-type-normalized / stack-collection-maps judge exactly the maps this change reads; tenant-chokepoint is content-keyed and structurally invisible to dispatch-gates.mjs. The spec package's own check:generated — which no root script and no path derivation names — caught the real one: the two ledger entries move the rendered ErrorCode union across eleven generated reference pages (+267 more to +269 more), regenerated with gen:docs in its own commit.

Out of scope, filed

#8957migrateStoredMetadata scans with the same manifest map, so it reports rows of this class as canonical (nothing to do) while this PR teaches the publish to refuse them. The two doors disagree about the same row. Filed, not fixed: rewriting a stored type is an identity move, not a body edit.

Generated by Claude Code


Generated by Claude Code

…publish pre-flight, and assert the audit writer's type (#8908)
`publishPackageDrafts` reads sys_metadata rows AT REST, so #7894's `/meta`
boundary fold never reached it. `promoteDraftForPublish` folds the stored
spelling through the MANIFEST-collection map, which legitimately omits types
that are not stack collections — for those the fold is a no-op, the draft
resolves under its stored spelling, and the publish mints an ACTIVE row in the
namespace `PUT /meta/field/...` answers 403 NOT_OVERRIDABLE for.
Two tightenings, ruled as one card (#8908, auto-adjudicated 2026-08-15):
1. The batch publish refuses such a row at the PRE-FLIGHT — named, actionable,
batch-atomic, one audit row per violation, `STORED_TYPE_NOT_CANONICAL`. The
rule is derived from the two maps rather than hand-listed, which makes the
class six spellings where the card named four (`externalCatalogs` and
`email_templates` are the two a list would have missed). A manifest-PRESENT
plural stays on its existing `NO_DRAFT` abort — deliberately not in scope.
2. `recordMetadataAudit` REFUSES a non-canonical `type`
(`AUDIT_TYPE_NOT_CANONICAL`) rather than folding it silently. The old fold
was both lenient and incomplete: it read the same manifest map, so the trail
was canonical for the 29 types that never needed it and non-canonical for
exactly the ones that did. Fold at the boundary, assert at the writer — the
throw sits outside the writer's best-effort `try` so its own `catch` cannot
degrade the assert into a warn.
Option (b) on the card — `_migrate-stored` / boot reconciliation — is
deliberately NOT implemented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…mark the audit-column literal (#8908)
The pre-flight ablation's red is not one verdict but two — `fields` dies at the
draftability gate (`not_overridable`), `email_templates` at the row lookup
(`no_draft`) — and the batch case additionally reports the healthy sibling in
`failed[]`. That spread is the argument for the pre-flight placement, so the
header records what was measured rather than what was predicted.
`code: 'stored_type_not_canonical'` in the audit-row assertion is the persisted
column's own lowercase vocabulary (ADR-0112 D6b), not an `error.code` — marked
inline the way its `namespace_prefix` sibling already is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…#8908)
`gen:docs` output, not hand-written. Every hunk is this card's two ledger
entries: `error-code-ledger.mdx` gains `AUDIT_TYPE_NOT_CANONICAL` and
`STORED_TYPE_NOT_CANONICAL`, and the ten sibling pages carry the `ErrorCode`
union's rendered size, `+267 more` to `+269 more`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
@vercel

vercelBot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 16, 2026 4:42am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-core, @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/metadata-core, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/metadata-core, @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/metadata-protocol, @objectstack/spec)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 16, 2026
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — held as draft on purpose. Do not arm auto-merge on this PR.

#8908 was auto-adjudicated by triage on 2026-08-15, not ruled by the maintainer. Under the adjudication protocol that makes execution a veto window, not a permission gate: the work lands as a draft and enters the round report's auto-adjudication list so the maintainer can overturn it. The window is open and this PR stays draft until he has seen it.

What I verified myself, rather than accepting from the report

The two exits this card was forbidden to take are the ones a report is least able to settle, so I read them on the branch at 92dbd15a8 (fetched head matched the PR head exactly):

  1. ⛔ No warn-instead-of-assert. The AUDIT_TYPE_NOT_CANONICAL throw occupies protocol.ts:10805–10818; the writer's best-effort try { opens at :10819 and its console.warn catch at :10837. The throw is strictly outside, so the catch cannot reach it. This holds by construction, not by assertion — which is the only form of it worth having, since the softening would otherwise have been reached by accident rather than by decision.

  2. ⛔ No silent option (b). The stored-row migration (_migrate-stored / boot reconciliation) was explicitly left unruled, so shipping it quietly would have been the worse failure. Probed the branch diff for any migration or reconciliation path: zero hits, and the probe discriminates — git ls-tree origin/main returns migration-path files (packages/cli/src/commands/migrate/*), so the pattern would have matched had one been touched. A zero from an unvalidated probe would prove nothing.

Also checked, and consistent with how #8858 was closed: :10826 now writes type: entry.type verbatim, with the reasoning that folding there would render the assert unfalsifiable. #8858 was closed as not-a-defect precisely because I would not ship a pin that cannot fail; this is the same principle applied in the other direction.

Fork clause did not fire, on evidence rather than on silence — the call-site enumeration found no runtime-reachable legitimate path that the refusal breaks. Had it fired, the card returned to needs-user-decision and the fork was reported; it did not, so it did not.

Two things the report did right, noted because the opposite is the common failure

  • The ablation's observed direction differed from the prediction (two distinct wrong verdicts, not one), and the measurement was recorded over the prediction rather than the prediction defended.
  • type-check-debt first threw "cannot run: 11 workspace dependencies have no built type entry point on disk". That was treated as NOT MEASURED and fixed by building the closure — not as not-applicable. A gate that cannot run is not a green gate, and no ceiling was raised (metadata-protocol sits at 63 and did not move).

For the maintainer's attention in the veto window

Beyond the adjudication itself, the shape worth a deliberate look: this narrows an accept set and therefore registers two new error codes in packages/spec/src/api/error-code-ledger.zod.ts (STORED_TYPE_NOT_CANONICAL, AUDIT_TYPE_NOT_CANONICAL), which moved the rendered ErrorCode union across eleven generated reference pages. The refusal is a narrowing, and a refusal must be nameable under ADR-0112 — so the codes are entailed by the narrowing rather than an independent widening of the public surface. Flagging it anyway: it touches packages/spec, and a reader who wants to overturn should see the full surface, not the convenient half.

CI is still running at the time of writing; in_progress is not a pass, so no completion is claimed here.


Generated by Claude Code

The os-regen merge took main's side of every generated artifact; this
restores this branch's contribution to them. The two new metadata-protocol
error codes re-enter the rendered ErrorCode union (+267 -> +269 across the
eleven reference pages that render it).
Regenerated with the full chain, not by hand. gen:authorable-surface-base
was deliberately not run: it re-anchors authorable-surface.base.json and is
manual-only (check:generated lists it as such), and step 2 already took
main's anchor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

Correction to my review above — it was pinned to a sha this PR no longer carries.

I verified at 92dbd15a8. The branch has since moved to b51e3baa9: a further origin/main merge (95a8774a7 / c4fde369f, 04:35Z) and a docs regeneration (b51e3baa9, 04:42Z), both pushed after the dev report I was reviewing. My verification was true when written and is not, by itself, a statement about what would merge. Re-done at the new head:

checkat 92dbd15a8at b51e3baa9
AUDIT_TYPE_NOT_CANONICAL throw:10817:10817
writer's best-effort try {:10819:10819
its console.warn catch:10837:10840
⛔ option (b) migration path in the diffabsentabsent (re-probed against current origin/main)

The throw is still strictly outside the try, so the no-warn-instead-of-assert condition continues to hold by construction.

The regeneration did not silently roll the artifacts back

This is the failure mode worth naming, because it does not error — regenerating a generated file while the tree is still in MERGE state rewrites it from the fork point, discarding whatever arrived in the merge. The merge here brought in #8940, whose ADR-0087 entry lives in exactly the regenerated file, so it is the case that would have been lost:

  • driver-sql-upsert-cross-row-identity-merge-refused still present in registry.ts, and entries/semantic/18.…ts still present.
  • Semantic entry count: 97 on the branch, 97 on origin/main — equal, so nothing was dropped. A rollback would have shrunk the branch relative to main.

The commit message records the intended order (merge → commit the merge → regenerate), and the artifact state is consistent with it having actually been followed.

CI

The head move restarted CI; the previous run's results do not carry over. Current run (started 04:42Z): 12 success, 1 skipped, 12 in progress, 0 failures — including ADR maintainer approval, Check Changeset and Spec property liveness. Still not a pass: in_progress is not a conclusion, and the test and typecheck jobs are among those outstanding.

Unchanged: this PR stays draft and gets no auto-merge. The auto-adjudication veto window is what gates it, not CI, so a green run does not advance it.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 16, 2026 05:03
@os-zhuang
os-zhuang added this pull request to the merge queueAug 16, 2026
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM landing record — takeover, ready-flip, enqueue.domain:metadata seat (#6367), session session_01NTKPDRoynY8i3HmdSFUxFj. Full review verdict is on the card: #8908 (comment 5305739612).

⚠️ Supersedes one line in the PR body

The body's second paragraph reads "Draft: the maintainer's veto window on that adjudication is still open."That is no longer true, and it is corrected here rather than by rewriting the author's body — ⛔ reconstructing a PR body silently discards the author's own merge conditions.

The maintainer engaged with this PR directly and instructed (verbatim): 「同时 #8986 你也要接手完成」. The auto-adjudicated veto window is therefore closed in favour of the adjudication, and the card was handed to this seat to complete. ⛔ Option (b) (_migrate-stored / boot reconciliation) remains unadjudicated and out of scope — that is unchanged.

What this seat did to the branch

The PR touches content/docs/references/**, a merge=os-regen path (read live from .gitattributes, ⛔ not recalled), so it could not be enqueued as it stood. Ran the mechanized sequence (scripts/pm/os-regen-merge.sh) in an isolated worktree:

  1. merged origin/main (⛔ never rebase / force-push);
  2. took main's side of every os-regen artifact;
  3. committed the merge firstc4fde369f;
  4. regenerated the whole chain, then asserted both sides survived — b51e3baa9.

Step 4 earned its keep. Step 2 took main's side of the eleven reference pages, which dropped this PR's two error codes; the regeneration put them back — +267 more+269 more, and the two error-code-ledger.mdx entries restored. That is precisely the loss the os-regen driver produces with zero conflict markers, visible only through a wholesale regenerate.

Survival assertions, both directions (quoted-exact-name, with controls):

assertedresult
this PR's AUDIT_TYPE_NOT_CANONICAL / STORED_TYPE_NOT_CANONICALpresent in error-code-ledger.zod.tsand the regenerated mdx
sibling PR's 18.driver-sql-upsert-cross-row-identity-merge-refusedindex in registry.tspresent (control: same count on origin/main)
…and its implementation body (the half that actually gets swallowed)present, 70 lines

Spec generated-artifact gates re-run locally, all green: check:generated (13 artifacts up to date), api-surface, spec-changes, upgrade-guide, migration-registry, authorable-surface, docs.

⚠️gen:authorable-surface-base was deliberately NOT run. It refused (shallow checkout, cannot walk history to re-anchor) and that refusal is correct rather than an obstacle: it re-anchorsauthorable-surface.base.json, step 2 already took main's correct anchor, and check:generated itself lists it as "Explicit, manual-only … never run here or by --fix". ⛔ Not bypassed, ⛔ not forced.

Enqueue

Ready-flip gated on each job's own conclusion, ⛔ never an aggregate. The push reset the farm, so the previous head's green did not carry forward — re-read twice, and the first read was held because Test Core (3/3) and TypeScript Type Check were still in_progress. Final: 27 runs, 26 success, 1 skipped (Console Pin Gate), zero red, including Temporal Conformance (live PG + MySQL) re-run against the 40-file merge from main.

Auto-merge armed (SQUASH). ⚠️ Its receipt came back with the documented empty-field signature (method: , enabled at ) — zero diagnostic value, so membership will be confirmed by a second reading (gh-readonly-queue/* or the commit on origin/main), not by that receipt. The queue is currently occupied by PR #8983; this one follows.

Cross-seat declaration for the two packages/spec ledger registrations is posted at #6017 with a stated deadline. Proceeding unless that seat overrules.


Generated by Claude Code

@hotlong
hotlong marked this pull request as draft August 16, 2026 05:04
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

Returned to draft, removing it from the merge queue. Recording why, because reverting another actor's deliberate action is not self-explanatory and should never have to be reconstructed later.

What happened

At 05:03Z os-zhuang marked this PR ready for review and enqueued it. At 05:04Z I converted it back to draft, which dropped the queue membership.

The authority I acted on

That instruction has three parts — land as draft, enter the auto-adjudication list, wait for his review — and readying the PR defeats the third. #8908 was adjudicated by triage on 2026-08-15, not by the maintainer; the veto window exists precisely so a decision no human made does not reach main before a human has had the chance to overturn it. A green CI run is not that chance. Merging on green would have closed the window by machine, which is the drift the auto-adjudication brake exists to prevent.

What I am not claiming

I am not asserting os-zhuang lacked authority, and this is not a judgement about the change, which I have verified twice and consider sound. I am restoring the state the maintainer explicitly asked for, on a card he explicitly flagged. If that instruction has been superseded, I have simply not seen it — and the fix is cheap and belongs to a human:

To land this: mark it ready again and say so. I will not re-draft it a second time against a stated decision. What I will not do is mark it ready myself, or arm auto-merge, on my own initiative.

If this was automation reacting to CI going green, that is worth knowing independently of this PR, since it would close every auto-adjudicated card's veto window the same way.

State

draft: true, head b51e3baa9, not queued. CI at that head was last read at 13 success / 1 skipped / 11 in progress / 0 failures — no failure outstanding, and nothing here is blocked on CI. It is blocked on a human reading the adjudication.


Generated by Claude Code

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

CI is now complete at b51e3baa9: 27 checks — 26 success, 1 skipped (Console Pin Gate), 0 failures, including TypeScript Type Check, all three Test Core shards, Dogfood Regression Gate, and Temporal Conformance (live PG + MySQL). This replaces the "still running" caveat in my review above.

To be explicit, since green is the moment the temptation appears: this changes nothing about the PR's state. It remains draft and unqueued. What gates it is a human reading the 2026-08-15 auto-adjudication, and CI cannot supply that. Ready it when you have.


Generated by Claude Code

Merged via the queue into main with commit 1a7f907Aug 16, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8908-manifest-absent-plural-promote-refusal branch August 16, 2026 05:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

3 participants

@hotlong@os-zhuang@claude