Skip to content

fix(spec): read a stored object_extension row as an objectExtensions entry, and list its refused fieldPatches keys - #29589

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-e9cv0w
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-e9cv0w

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread

Fixes #29554

Clause-②: no (no accept set moves; the stored pass and the --stored listing widen)
Metadata-protocol: none

维护者速读

  • 改了什么:存量 object_extension 行现在按 objectExtensions 条目走存量转换,os migrate meta --stored 会列出 fieldPatches 里不可能是字段名的键。
  • 为什么改:此前存量通道找不到该类型的集合,原样返回整行,任何转换与拒收形状检测都跳过扩展行,升级前看不到问题。
  • 风险与回滚:读取存量扩展行时会多跑已有的 objectExtensions 转换(如时间默认值去掉 Z);规范行按引用原样返回。回滚即撤销本 PR。
  • 先看:packages/spec/src/conversions/refused-shapes.ts 新检测器;packages/spec/src/conversions/stored.ts 的一行映射;.changeset/29366-field-patches-key-grammar.md 被改的那句。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/conversions/stored.test.ts src/conversions/refused-shapes.test.ts,应见 110 个测试全部通过。
  • 需要你做的:确认对 spec(object-extension fieldPatches): the record key is a bare z.string(), so fieldPatches: { Industry: {…} } parses and is refused only downstream by the target judge #29366 尚未发布的 changeset 那一句的更正(check-empty-changeset 对此按设计报红,只能由人确认)。

What changes

  • packages/spec/src/conversions/stored.ts: STORED_ONLY_COLLECTIONS maps object_extension to objectExtensions, beside translation. A stored extension row is one objectExtensions entry, so every conversion that walks that collection now replays over it at each rehydration seam, and the stored pass hands it to the refused-shape detectors.
  • packages/spec/src/conversions/refused-shapes.ts: one detector, object-extension-field-patch-key-grammar-refused, appended last (additive, so spec seat 5's detector lands after it). Its predicate is the refusing door's own: ObjectExtensionSchema.safeParse on the extension, keeping each invalid_key issue located at fieldPatches.KEY. It lists, never rewrites, and closes each reason with the parse's own message.
  • packages/spec/src/migrations/entries/semantic/18.object-extension-field-patch-key-grammar-refused.ts: the header and acceptanceCriteria said --stored does not list such a row and named a manual check. They now say the row is listed, one TODO per refused key at fieldPatches.KEY, and left as stored.
  • .changeset/29366-field-patches-key-grammar.md: the matching sentence in that pending note is corrected the same way (see Acceptance notes).
  • .changeset/29554-stored-object-extension-rows.md: patch on @objectstack/spec.

Why

The card's premise holds on origin/main ff827bcf: applyConversionsToStoredItem returned a stored object_extension row by reference with 0 notices and 0 TODOs, while the control object row with the same time default got one time-default-utc-suffix-dropped notice.

Measured before choosing the map:

  • Stored type names. The save door folds the URL segments object_extension, object_extensions and objectExtensions to object_extension (meta-url-data.generated.ts), so object_extension is the one stored spelling. The protocol's singular/plural read fallback (PLURAL_TO_SINGULAR[t] ?? SINGULAR_TO_PLURAL[t]) finds no other spelling for it, and migrateStoredMetadata reports a row stored under another spelling as skipped (isNonCanonicalStoredType) before reading its body. So no plural spelling is mapped, and a test pins that both plurals still pass through by reference.
  • The omission is deliberate in the shared maps. check:stack-collection-maps records objectExtensions as a deliberate missing waiver of the manifest-collection map: extensions merge into their target rather than registering as named items, and pluralToSingular feeds registration and the authoring lint. Adding it there would advertise objectExtensions as a named-item collection to every reader. The stored-only map exists for exactly this case, so the entry goes there.

Tests

  • stored.test.ts: a stored object_extension row with a Z-suffixed time default is wrapped as objectExtensions[0], converted, and noticed at objectExtensions[0].fields.opens_at.defaultValue; the input is not mutated. Controls: a canonical extension row passes through by reference; the same body as an object row converts exactly as before; object_extensions and objectExtensions row types pass through by reference.
  • refused-shapes.test.ts: the registered id list gains the new id last; one TODO per refused key (Industry, annual-revenue) at objectExtensions[0].fieldPatches.KEY, the row unchanged; the listed keys equal the parse's invalid_key issues (Industry, Annual Revenue) and each reason ends with the parse's message; snake_case keys are not listed and parse; a __c key (another entry's arm) is not listed by this detector.
Ablations (committed first, restored by blob hash)
  • scripts/ablation-replace.mjs deleting object_extension: 'objectExtensions', from stored.ts: 3 failed / 107 passed (the conversion test and both listing tests), restored, blob e26e3a1e == HEAD, git diff HEAD empty.
  • Deleting objectExtensionFieldPatchKeyGrammar, from REFUSED_STORED_SHAPES: 3 failed / 84 passed (the id list and both listing tests), restored, blob d205cc7d == HEAD, git diff HEAD empty.
End-to-end probe through migrateStoredMetadata (scratch test, not committed)

Built the @objectstack/objectql... closure and ran a throwaway copy of the protocol.stored-migration.test.ts harness:

  • An object_extension row with fieldPatches: { Industry: … } and nothing else: skipped, 1 TODO (object-extension-field-patch-key-grammar-refused at objectExtensions[0].fieldPatches.Industry). On ff827bcf the stored pass returned that row untouched with 0 TODOs, which this pass records as canonical.
  • The same key plus a Z-suffixed time default: preview pending with the conversion and the TODO both printed under the row; --apply reports it failed because the save door refuses the key, as for every other refused shape that also converts.
  • A packaged extension with only the Z-suffixed default on opens_at__c: --apply reports rewritten and the stored body becomes "12:00".

Acceptance notes

  • Corrected a pending release note. .changeset/29366-field-patches-key-grammar.md (PR feat(spec)!: key an object extension's fieldPatches by the field map's own key grammar #29513, not yet released) said os migrate meta --stored does not list such a row and told readers to check by hand. This PR makes that false, so the sentence now says the row is listed. check-empty-changeset refuses any change to a changeset this PR did not add and stays red by design for the deliberate-correction class; please confirm the correction.
  • The missing waiver reason in scripts/check-stack-collection-maps.mjs still says there is no singular metadata-type name for objectExtensions; ADR-0146 D10 added object_extension. The waiver itself stays right (the manifest map should not carry it); only its reason text is dated. Noted, not filed (wording).
  • No protocol.ts file is touched (freeze until 06:00 UTC on 3 November).

Verification

Gates at head 91fbde28 (this branch merged with origin/main dbca3ef0):

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 81 derived commands; all run, plus check:stack-collection-maps, check:meta-url-spelling, the spec typecheck and vitest run src/conversions src/migrations (28 files, 981 tests).
  • --ran reconciliation: 81 accounted, 80 run, 1 NOT MEASURED, 0 unrun.
  • Every run exits 0 except node scripts/check-empty-changeset.mjs --base origin/main (exit 1, the correction above).
  • NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, it needs every package built; CI's Lint & Repo Gates runs it).
  • check-plugin-teardown-shape --self-test first exited 3 on the shallow clone; after fetching its pinned fixture commit it exits 0. check:lean-entry-closure first exited 3 for want of objectql/dist; after the closure build it exits 0.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is left to CI.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: no generated artifact moved (check:api-surface, check:authorable-surface, check:docs exit 0).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Maintainer approval

Warren approved the rewrite of the pending .changeset/29366-field-patches-key-grammar.md sentence (option A): 「全部批准」 on the decision card 「六个命名/措辞全部按推荐批准吗?」 in the project thread "Decide pending spec name approvals", 2026-11-03 00:08Z. Recorded on this PR in seat comment 6591818192. That is the person's written confirmation the DELIBERATE CORRECTION class of check-empty-changeset asks for; Check Changeset stays red by design and is not a required context.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbXpawDQBhgKU2ssyrw


Generated by Claude Code

…entry, and list its refused fieldPatches keys

The stored pass returned an object_extension row untouched because its type
mapped to no stack collection, so no conversion or refused-shape detector ran
over a stored extension. Map it in the stored-only map, add the
REFUSED_STORED_SHAPES detector for
object-extension-field-patch-key-grammar-refused, and correct that entry and
the #29366 changeset, which said --stored does not list such a row.

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

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/data-modeling/object-extensions.mdx (via object_extension (symbol, a field of const object STORED_ONLY_COLLECTIONS), fieldPatches (literal, a string literal in refusedFieldPatchKeys), objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/deployment/install-modes.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/getting-started/quick-start.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/protocol/kernel/i18n-standard.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/protocol/objectql/schema.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))

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

  • content/docs/releases/v17/17-5.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/releases/v18/18-0.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 168 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 d3175d59d036d3c2819d5e7913c8c406c1ea634d → packageMentionDocs.

Which tree this was computed on

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

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

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

1 similar comment
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/data-modeling/object-extensions.mdx (via object_extension (symbol, a field of const object STORED_ONLY_COLLECTIONS), fieldPatches (literal, a string literal in refusedFieldPatchKeys), objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/deployment/install-modes.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/getting-started/quick-start.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/protocol/kernel/i18n-standard.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/protocol/objectql/schema.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))

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

  • content/docs/releases/v17/17-5.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))
  • content/docs/releases/v18/18-0.mdx (via objectExtensions (literal, a string literal in detect; a string literal in object_extension))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 168 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 d3175d59d036d3c2819d5e7913c8c406c1ea634d → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d3175d59d036d3c2819d5e7913c8c406c1ea634d → 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

domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 91fbde288442373ea5a36341ba83dea58172938c
Local-runs: none
Scope: packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/conversions/stored.ts, packages/spec/src/migrations/entries/semantic/18.object-extension-field-patch-key-grammar-refused.ts + packages/spec/src/conversions/walk.ts (mapCollection), packages/spec/src/conversions/registry.ts (the seven conversions that walk objectExtensions[]), packages/spec/src/data/object.zod.ts (ObjectExtensionSchema.fieldPatches, FieldNameKeySchema, the __c and replaced-field refinements), packages/spec/src/meta-spelling/manifest-collection-spelling.ts and meta-url-data.generated.ts (the shared maps and the URL fold), packages/metadata-protocol/src/protocol-object-extension-item.ts (registrableObjectExtension, the INVALID_METADATA the reason cites), packages/metadata-protocol/src/protocol.ts (resyncStoredObjectExtensions ~L2005-2035, the --stored pass ~L16500-16770), packages/metadata-protocol/src/protocol-stored-item-conversion.ts and packages/metadata/src/loaders/database-loader.ts (the rehydration seams), packages/metadata-protocol/src/protocol-stored-overlay-types.ts (isNonCanonicalStoredType), scripts/check-stack-collection-maps.mjs (the objectExtensions waivers), scripts/check-empty-changeset.mjs (the DELIBERATE CORRECTION class), .changeset/29366-field-patches-key-grammar.md and .changeset/29554-stored-object-extension-rows.md (read to judge ② and ③, not reviewed).

Inputs read: card #29554 body and all three comments (triage, claim, os-dev-report); PR #29589 body, file list (7 files) and the net diff against merge base dbca3ef0; the check-runs on the head at two polls. Not read: the dispatch order, the seat's own conclusions.

① Derived judgments

  1. applyConversionsToStoredItem('object_extension', row) now wraps the row as objectExtensions: [row] and replays the full chain (stored.ts L63-85, STORED_ONLY_COLLECTIONS). Verified: PLURAL_TO_SINGULAR and SINGULAR_TO_PLURAL (manifest-collection-spelling.ts) carry neither object_extension nor objectExtensions, so before this line the lookup at L146 found no collection and returned the row by reference. The card's premise holds in the tree. Right.
  2. The shared maps are not widened; the stored-only map is the right home. check-stack-collection-maps.mjs waives objectExtensions as missing on PLURAL_TO_SINGULAR (L537-543: "merge into their target rather than registering as a type") and on four other maps (L498, L569, L616, L716). Adding it to the shared map would advertise a named-item collection to registration and the authoring lint, exactly the [Decision] TranslationItemSchema still declares settings — the item door can override platform settings copy, and it is the stronger of the two doors the batch #132 ruling only half closed #19620 argument for translation. Right. (The waiver's reason text at L539, "there is no singular metadata-type name to map to", is dated since ADR-0146 D10 minted object_extension; the waiver itself stays right. Wording only, outside scope, the dev noted it.)
  3. No plural row spelling is mapped. meta-url-data.generated.ts L51-52 folds object_extensions and objectExtensions to object_extension, and L69 lists object_extension as canonical, so isNonCanonicalStoredType is true for both plurals and the --stored pass reports such a row skipped (protocol.ts ~L16575) before its body is read. The stored.test control pins both plurals pass through by reference. Right.
  4. Every rehydration seam that reads object_extension rows now serves converted bodies. convertStoredItem (protocol-stored-item-conversion.ts L58-73) is called with row.type by the list reads and by resyncStoredObjectExtensions (protocol.ts L2030) before registrableObjectExtension parses the body; database-loader.ts rowToData L878-890 does the same for the DB loader. Consequence: a stored extension carrying a shape the strict FieldSchema refuses at 18 but a retired conversion rewrites (a 12:00Z time default, reference_to, a bare unique: true index, a malformed scale, a currencyConfig.precision) used to fail the resync with INVALID_METADATA and now registers canonical, as an object row already did. This is the fix the card asked for, and it is wire-visible only as the already-published ConversionNotice shape. Right.
  5. Side effects on upgrade, measured against registry.ts. Exactly seven conversions walk objectExtensions[]: field-malformed-scale-precision-removed (L2588), declared-index-unique-scope (L3310), time-default-utc-suffix-dropped (L4531), field-column-lists-canonicalized (L5112), field-reference_to-to-reference (L9054), currency-config-dynamic-default-currency-removed (L14987), currency-config-precision-removed (L15094). All seven are retiredFromLoadPath: true, idempotent, walk fields/indexes by key (never by name, which an extension lacks), and each carries an objectExtensions fixture of its own: they were written for this row and never reached it. No conversion keys on the extension's extend, none dedups a one-element collection, and no EXISTING REFUSED_STORED_SHAPES detector walks objectExtensions (they walk objects, dashboards, views, pages, flows). Nothing fires that should not. The one thing that changes shape downstream is the --stored outcome of an extension row with a refused key: canonical (0 TODOs) becomes skipped with one TODO per key (protocol.ts ~L16740-16745), which is the listing the card asked for.
  6. The detector lists exactly what the door refuses. refusedFieldPatchKeys runs ObjectExtensionSchema.safeParse on the extension and keeps invalid_key issues at path length 2 under fieldPatches. The door is fieldPatches: z.record(FieldNameKeySchema, …, { error }) (object.zod.ts L5057-5069) and FieldNameKeySchema is /^[a-z_][a-z0-9_]*$/ (L2785). The __c refinement (L4902-4912) and the replaced-field refinement (L4929-4940) emit custom issues at the same path and are excluded, as the test pins; a patch's options invalid_key sits at path length 4 and is excluded. The ADR-0010 envelope a loader stamps would add unrecognized_keys issues only, which the filter drops, so a raw stored body judges the same as the save door's envelope-peeled parse. Right.
  7. Public surface. No schema, no export: REFUSED_STORED_SHAPES and STORED_ONLY_COLLECTIONS are module-internal (refused-shapes.ts header: "not re-exported from conversions/index.ts"). The published shapes are the existing ConversionNotice and ConversionTodoNotice; one new TODO conversionId appears, which is additive. Clause-②: no holds. Metadata-protocol: none holds: no door accepts or refuses anything new.

② Semver level

.changeset/29554-stored-object-extension-rows.md: '@objectstack/spec': patch, body carries Clause-②: no (no accept set moves; the stored pass and the --stored listing widen) and Metadata-protocol: none, matching the PR body and the claim. Precedent on main for a listing-only widening of the stored pass is patch (.changeset/29212-dashboard-duplicate-filter-stored-listing.md, .changeset/28163-stored-connector-input-listed.md, and the #25279 fix in packages/spec/CHANGELOG.md under "Patch Changes"). The diff publishes a behaviour fix on an existing primitive and one additive TODO id; patch is the right level. The body's claims were checked against the tree (①4-6) and none is false. Matches.

Clause-②: no (no accept set moves; the stored pass and the --stored listing widen) — confirmed in the tree.

③ Boundary flags

④ Design judgment

packages/spec/src/conversions/stored.ts — holds. One line in STORED_ONLY_COLLECTIONS, reached by the existing lookup (PLURAL_TO_SINGULAR[type] ?? type then SINGULAR_TO_PLURAL[singular] ?? STORED_ONLY_COLLECTIONS[singular]). Tried to break it: (a) a row stored under objectExtensions or object_extensions — neither map resolves it, so it passes by reference, and the --stored pass already reports it skipped by isNonCanonicalStoredType (both fold to object_extension in the URL map); the test pins both. (b) A row whose body is not a dict — the L144 guard returns it. (c) The { [collection]: [item] } wrap with a one-element array — mapCollection indexes it as objectExtensions[0], which is the path every notice and TODO carries; the resync and list reads discard the path and keep the item. (d) The doc comment's claim that the stored-metadata pass reports any other spelling as skipped before reading the body — true at protocol.ts ~L16575. No finding.

packages/spec/src/conversions/refused-shapes.ts — holds. refusedFieldPatchKeys is the door's own predicate (①6) and the detector reports one TODO per refused key at ${path}.fieldPatches.${key} with from = that key's patch only (no credential surface: a display patch), reason naming the target by extend (falls back to "its target" when extend is not a string, which is also the case the parse refuses at extend), the no-lossless-rewrite argument, the sync's INVALID_METADATA consequence (true: registrableObjectExtension throws for the whole body, protocol-object-extension-item.ts L237-254), and closes with the parse's message. The callback returns the item unchanged, so mapCollection leaves the stack untouched (walk.ts L500-505). toMajor: 18 and the id match the registered semantic entry, which the chain test asserts. Tried to break it: (a) a __proto__ key — matches the grammar, refused by refuseRecordProtoKey with a non-invalid_key issue, not listed; the entry's surface is the grammar, so this is outside it, and the guard is #-numbered elsewhere; not a finding. (b) A key with a space — path objectExtensions[0].fieldPatches.Annual Revenue, same rendering the parse's path.join('.') gives and the test pins; consistent with other detectors. (c) Order sensitivity — none: each detector reports independently over the converted stack. No must-fix.

packages/spec/src/migrations/entries/semantic/18.object-extension-field-patch-key-grammar-refused.ts — holds. The header comment and acceptanceCriteria now describe what the tree does: the stored pass wraps the row (stored.ts), the detector of this id lists each refused key "under its row" and "leaves it as stored" (the detector rewrites nothing; --stored reports skipped with the TODO when no conversion also fires, pending when one does, and --apply then fails at the save door, which is the documented behaviour of every refused shape that also converts). The sentence "naming the extended object" is true of reason. The removed manual-check sentence was true on main and is false on this head, so its removal is required, not optional. surface, replacement, reason unchanged. No finding.

Should-fix (follow-up card, not this PR; outside the file surface): at the two rehydration seams the once-per-process notice dedupe keys on data.name (protocol-stored-item-conversion.ts L60-69, database-loader.ts L881-889), and an extension body carries extend, not name. For object_extension rows the warn line reads stored object_extension/ followed by the literal unnamed placeholder and the key ${conversionId}|object_extension| collapses every extension row of a deployment into one warning per conversion. Rows are still converted correctly; only the operator's log loses which extension it was. listSlotNameOf already knows to read extend for this type, so the fix is one call at each seam. Not a FAIL: the card's "Done when" is met, and the affected files are outside the claim's surface.

No must-fix.

Implemented-by: claude/project-thread-e9cv0w
Reviewed-by: session_01JQwxbahe4v7GBD94QeSnir (seat subagent)

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir

维护者速读

席位核对

ACCEPT · domain:spec#3 · head 91fbde288442373ea5a36341ba83dea58172938c · 2026-11-02T23:56Z

  1. Shape: seven files. stored.ts (+16/−1: object_extension → objectExtensions in the stored-only map beside translation), refused-shapes.ts (+52: the fieldPatches key-grammar detector), their two tests, the semantic migration entry 18 (manual-check sentence replaced), the new .changeset/29554-stored-object-extension-rows.md (patch), and a one-line edit to the pending .changeset/29366-field-patches-key-grammar.md. Inside the claim (6586431514); the shared manifest-collection maps are untouched; no protocol.ts file touched.
  2. Premise, measured: the reviewer verified that neither PLURAL_TO_SINGULAR nor SINGULAR_TO_PLURAL carried the type, so the row returned by reference, and traced every rehydration seam that reads object_extension rows. The dev ran an end-to-end probe through the --stored harness: skipped with one TODO, pending when a conversion also fires, and refused at the save door on --apply.
  3. Gates: dev derived 81 at 91fbde28, 80 run, 1 NOT MEASURED (check:dual-build-cjs-loads, left to CI). All exited 0 except check-empty-changeset exit 1 (the deliberate correction below). Two ablations went red (3 failed each) and were restored to equal HEAD. On this head there are 64 check-runs: 54 success, 9 skipped, 1 failure (Check Changeset, by design). mergeable_state is unstable for that one red only.
  4. Accepted deviations: the edit to the pending spec(object-extension fieldPatches): the record key is a bare z.string(), so fieldPatches: { Industry: {…} } parses and is refused only downstream by the target judge #29366 changeset is the DELIBERATE CORRECTION class. Its old sentence names a manual check that is false once this lands, so removing it is required. The class stays red until a person confirms in writing on the PR, which is the decision card in front of Warren.
  5. Out of scope: the reviewer's should-fix, the notice dedupe keying on data.name at the two rehydration seams, is filed as domain:server card metadata: a stored object_extension conversion notice reads data.name, which an extension body does not carry, so every extension warns as "<unnamed>" and one warning per conversion covers them all #29596 (serial after this PR). The dated waiver reason text in check-stack-collection-maps.mjs is wording only.
  6. Landing: not governed (check-governed-merges --pr 29589: NOT governed, 213 changed lines). Contract review at CONTRACT_REVIEW_TIER PASS on this head (6587797860). Check Changeset is not a required context. Gated on Warren's written confirmation of the spec(object-extension fieldPatches): the record key is a bare z.string(), so fieldPatches: { Industry: {…} } parses and is refused only downstream by the target judge #29366 correction. On option A, the seat copies his words onto this PR, then runs pr_ready + automerge_enable; then spec seat 5's spec(conversions): the stored pass does not list a dashboard whose globalFilters entry is keyed dateRange beside a declared dateRange (the #29213 refusal) #29541 and card spec(object-extension fields): the record key is a bare z.string(), so fields: { 'Region Code': … } parses and is refused only downstream #29523 are unblocked. On option B, the card goes back to pm:queue with Resume-from: claude/project-thread-e9cv0w@91fbde28 and a revert-the-sentence note.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir

Maintainer approval · 2026-11-03T00:11Z

Warren approved this PR's open item at 2026-11-03T00:08Z in the project thread "Decide pending spec name approvals". The card asked 「六个命名/措辞全部按推荐批准吗?」 and he picked 「全部批准」. The post above that card listed this PR's item as follows.

This is the written confirmation that the DELIBERATE CORRECTION class of check-empty-changeset asks for: the rewritten sentence stays. Check Changeset remains red by design, and it is not a required context.

With this, the gate the seat ACCEPT named is met. The head is unchanged since the ACCEPT. The seat now readies this PR and arms auto-merge, and needs-user-decision is removed.


Generated by Claude Code

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

1 participant