Skip to content

feat(spec)!: key an object extension's fields by the field map's own key grammar - #29774

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

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-fskrbl

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29523

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here — content/docs/data-modeling/object-extensions.mdx ("Naming the fields you add") gains the key grammar. skills/objectstack-data/SKILL.md and its evals teach extension fields only as region__c, so every taught line stays true and no skill line changes.

维护者速读

  • 改了什么:对象扩展的 fields(扩展新增的字段)的键改用与对象 fields、扩展 fieldPatches 同一个键 schema(小写 snake_case),Region Code、region-code 这类对象自己的字段表不可能容纳的键在解析时就被拒,报一个带示例的 invalid_key。
  • 为什么改:以前任何字符串都能过 schema。实测 main 上 defineStack、os validate 都放行;在 .custom 包里 Region Code__c 被保存门保存并发布、出现在每个对象读取里、启动重读照样注册,还按这个名字建列存了值。
  • 风险与回滚:收窄(BREAKING,spec minor,ADR-0087 语义迁移条目已登记,os migrate meta --stored 会列出存量坏键行)。仓内 examples/skills/docs/模板无一处写过这种键;已存储的带坏键的扩展行升级后整行不再注册(其 fieldPatches 一并),再保存被 422 拒,直到改名。回滚即 revert 本 PR。
  • 先看:packages/spec/src/data/object.zod.ts(ObjectExtensionSchema.fields 的键 schema 与 invalid_key 文案)、packages/spec/src/conversions/refused-shapes.ts(新检测器)、packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/field-customization.test.ts src/conversions/refused-shapes.test.ts,预期 2 个文件全绿(192 条)。
  • 需要你做的:无,席位落地。

What changes

  • packages/spec/src/data/object.zod.ts: ObjectExtensionSchema.fields is keyed by FieldNameKeySchema, the one key schema ObjectSchema.fields and ObjectExtensionSchema.fieldPatches already read (not exported; its doc comment now lists all three readers). The record's error map answers invalid_key with a prescription: the key is the name of a field the extension adds, lowercase snake_case such as region_code (region_code__c in a customization package), never its label. The issue sits at fields.KEY, as the fieldPatches one sits at fieldPatches.KEY. The fields describe names the grammar. __proto__ stays refused on the raw input by refuseRecordProtoKey, unchanged.
  • packages/spec/src/conversions/refused-shapes.ts: one detector, object-extension-field-key-grammar-refused, appended after the fieldPatches one. Its predicate is the refusing door's own: ObjectExtensionSchema.safeParse, keeping each invalid_key issue at fields.KEY (the fieldPatches reader and this one now share one helper, refusedExtensionRecordKeys(extension, slot)). It lists, never rewrites, names the target, shows only the key and the field's type in from, and closes each reason with the parse's own message.
  • packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts: the ADR-0087 semantic entry (refused, with a prescription; no conversion, because the key is the name a field is stored and read under, and a respelling names a different column or lands on a field the target already declares).
  • .changeset/29523-object-extension-field-key-grammar.md: spec minor, BREAKING, adr-0087: registered object-extension-field-key-grammar-refused, with the FROM → TO line.
  • content/docs/data-modeling/object-extensions.mdx: one paragraph in "Naming the fields you add". content/docs/references/data/object.mdx: regenerated (the fields describe moved).
  • No public export is added or removed. No existing test or fixture needed an edit: every extension fields key in examples/**, skills/**, content/docs/**, templates and test fixtures is already snake_case.

Why — the measurement that chose the declaration

Measured on origin/main 1c1a5d40 (spec built from main's object.zod.ts), then on this branch:

Door main this branch
ObjectExtensionSchema / defineObjectExtension (fields: { 'Region Code': … }) parses one invalid_key at fields.Region Code
defineStack({ objectExtensions }), target outside the stack accepted refused, objectExtensions.0.fields.Region Code
defineStack, target inside the stack accepted (the merged object is not re-parsed) refused
os validate (CLI on a scratch defineStack config) no issue on the extension (only an unrelated security-owd-unset on the scratch object) exit 1, both extension keys named
object_extension save + publish door, .custom pair, key Region Code__c stored and published; served on the by-name, list, layered and registry reads; a record insert stored EMEA under Region Code__c; a boot re-read (loadMetaFromDb) registered it again with 0 errors save refused 422 INVALID_METADATA, issue invalid_key at fields.Region Code__c; no row stored; insert refused INVALID_FIELD
A row stored before the upgrade, re-read at boot n/a loadMetaFromDb counts 1 error, the extension is not registered (no extender), and its re-save is refused 422

So every authoring door accepted such a key end to end, which makes this Clause-②: no (narrowing) · Metadata-protocol: narrowed, BREAKING, with the semantic entry and the REFUSED_STORED_SHAPES detector the card asks for (the stored pass reaches object_extension rows since #29589).

Tests

  • packages/spec/src/data/field-customization.test.ts gains the describe "extension fields keys — the field map's own key grammar" (8 cases): Region Code, RegionCode, region-code, 2nd_phone, Region Code__c each refused with exactly one invalid_key at fields.KEY (message names the grammar and an example, no tracker id); CONTROL region_code and region_code__c parse; CONTROL both authored examples of the object-extension guide parse whole; and an agreement test that the extension fields record refuses at the key exactly the keys ObjectSchema.fields refuses at the key, over an 11-key sample that holds keys on both sides (guarded against vacuity).
  • packages/spec/src/conversions/refused-shapes.test.ts: the registered id list gains the new id last; one TODO per refused key at objectExtensions[0].fields.KEY, the row unchanged; the listed keys equal the parse's invalid_key issues and each reason ends with the parse's message; snake_case keys (__c included) are not listed and parse; and a row with both a bad fields key and a bad fieldPatches key is listed once by each detector, never crossed.

Acceptance notes

  • Not changed here: the manifest fold (ObjectQL.registerApp, the objectExtensions branch in packages/objectql/src/engine.ts) copies ext.fields as given and parses nothing; measured on this branch, registerApp with fields: { 'Region Code': … } still registers the field and stores a value. Every authoring path that produces a manifest (defineStack, os validate, os build, the save door) now refuses the key first, so only an artifact built before the upgrade reaches it. That is the fold's trust model for every collection, not this card's; noted, not filed.
  • Not changed here, same family: ObjectSchema.fields refuses constructor and prototype by record refines that sit outside the key schema, so ObjectExtensionSchema.fields: { constructor: … } still parses (as fieldPatches: { constructor: … } does, noted on feat(spec)!: key an object extension's fieldPatches by the field map's own key grammar #29513). Noted, not filed.

Verification

All readings on objectstack-ai/objectstack, head bd50ef25 (merge of origin/main 87956dcc; the delta against main is the eight files listed above).

Door probes (scratch, not committed)

A throwaway copy of the packages/runtime/src/domains/meta-object-extension-item.test.ts rig (better-sqlite3 :memory:, real ObjectQL, real protocol), run once against a spec dist built from main's object.zod.ts and once against this branch's; and os validate on a scratch defineStack config in packages/cli. Readings are the table above. The probe files were deleted before commit.

Ablation: the new tests can fail

node scripts/ablation-replace.mjs replaced z.record(FieldNameKeySchema, FieldSchema, { with z.record(z.string(), FieldSchema, { in object.zod.ts (anchor 1 → 0, blob 06e35bf5 → 09e91a78) and ran src/data/field-customization.test.ts src/conversions/refused-shapes.test.ts: 9 failed | 183 passed (192). The five refusal cases, the agreement test and the three new listing tests went red; every control stayed green. Restored: blob == HEAD, git diff HEAD empty. The spec suite imports ./object.zod from source, so no build sits between the mutation and the run. Taken on 61876fc4; the tested lines did not change in the merge.

Spec and consumer packages (downstream of the contract)

Under scripts/pm/os-verify-lock.sh.

On the final head bd50ef25, after a full turbo run build --concurrency=2 (117/117 tasks):

  • @objectstack/spec check:generated: exit 0. test: 803/803 files, 23807 passed | 1 todo.
  • @objectstack/organizations object-extension-managed-target.wall.test.ts: 1/1, 5 tests. @objectstack/plugin-auth identity-objects-plugin.test.ts: 1/1, 15. @objectstack/plugin-security upgrade-impact-report.wiring.test.ts: 1/1, 5.
  • @objectstack/downstream-contract test: 3/3, 31. @objectstack/example-showcase test: 45/45, 541.

On 61876fc4 (before the main merge; spec built from this branch):

  • @objectstack/spec test: 803/803 files, 23798 passed | 1 todo. typecheck: exit 0. test:repo: 89/89, 1522.
  • @objectstack/metadata-core (full): 23/23, 904. @objectstack/objectql (full): 544/544, 10606. @objectstack/metadata-protocol (full): 308 passed | 4 skipped (312), 22262 passed | 26 skipped.
  • Every other test file outside packages/spec that names objectExtensions, defineObjectExtension, object_extension or ObjectExtensionSchema: @objectstack/cli 9/9 (108), @objectstack/cloud-connection 3/3 (19), @objectstack/lint 7/7 (535), @objectstack/metadata 2/2 (17), @objectstack/platform-objects 2/2 (73), @objectstack/rest 5/5 (81), @objectstack/runtime 4/4 (48).
  • Not run locally: the three ui/packages/app-shell files that name an extension (ResourceListPage.objectExtensionName-29569, DataPillar.baseObjectExtension-28575, DataPillar.draftUnreadable-29148). Their extension bodies are mock-server fixtures keyed custom_note__c, budget_code__c, region__c, tier__c, and none parses them with the spec; Test UI runs them. Studio's add-field derives the name through toFieldNameLoose and extensionFieldName, so it writes snake_case keys.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final head bd50ef25: 104 commands, all exit 0; --ran reconciliation: 104 derived, 104 run, 0 NOT-MEASURED. They ran after the full build, so the built-output gates (check:skill-examples, check:dual-build-cjs-loads) measured, where on 61876fc4 they had exited 3 for want of that build. check-adr-0087-registration (registered, new here) and check-changeset-no-major are among the 104.

Dogfood (narrowing arm, final head)

pnpm --filter @objectstack/dogfood test under the lock on bd50ef25, after the full build in the same locked run: 402 passed | 3 skipped (405) files; 3317 passed | 28 skipped (3345) tests; exit 0, lock VERDICT command-exit 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 passes (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: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE


Generated by Claude Code

…key grammar

ObjectExtensionSchema.fields was keyed by a bare string, so an extension
could add a field under a key (`Region Code`, `region-code`) the target's
own field map refuses. It now reads FieldNameKeySchema, with an
invalid_key prescription at fields.KEY, an ADR-0087 semantic entry, and
a REFUSED_STORED_SHAPES detector that lists such a stored row.

Claude-Session: https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bd50ef25315a838988c2086cd5f2d6733bbc7b08
Local-runs: none
Scope: packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts + packages/spec/src/stack.zod.ts (objectExtensions: z.array(ObjectExtensionSchema), the defineStack and os validate door), packages/metadata-protocol/src/protocol-object-extension-item.ts (OBJECT_EXTENSION_ITEM_SCHEMA, the save, restore, promotion and sync door), packages/metadata-protocol/src/protocol.ts (registerStoredObjectExtension, the boot and heal hydrations), packages/metadata-protocol/src/protocol-extension-field-suffix.ts (the __c judge the changeset's "Unchanged" line names), packages/objectql/src/engine.ts (registerApp's objectExtensions fold), packages/cloud-connection/src/marketplace-install-local-plugin.ts (the install's extension reads), packages/spec/src/conversions/stored.ts (object_extension to objectExtensions), packages/spec/src/migrations/entries/semantic/18.object-extension-field-patch-key-grammar-refused.ts (the sibling entry), packages/spec/src/shared/record-proto-key-guard.ts, .changeset/29523-object-extension-field-key-grammar.md (read for ②, not reviewed)

Inputs: card #29523 (body, triage 6584774842, claim 6593404228, os-dev-report 6595183097), PR #29774 (body, 8-file list, net diff of bd50ef25 against merge-base 87956dcc of main), and the head's check-runs.

① Derived judgments

  • Accept set, ObjectExtensionSchema.fields: the record key moves from z.string() to FieldNameKeySchema (/^[a-z_][a-z0-9_]*$/), the one constant ObjectSchema.fields (object.zod.ts:3212) and ObjectExtensionSchema.fieldPatches (object.zod.ts:5081) already read. Narrowing, correctly judged: Region Code, RegionCode, region-code, 2nd_phone, Region Code__c are refused; region_code and region_code__c still parse. The fields key set is now exactly the key set the target's own map admits, which is the card's "done when". Right.
  • Issue shape: one invalid_key at fields.KEY through the record's error map, the same mechanism and path convention as the fieldPatches reader (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). The message names the grammar, two examples and no tracker number (check:doc-authoring holds). __proto__ stays refused on the raw input by refuseRecordProtoKey, unchanged; constructor and prototype stay admitted on the extension (as on fieldPatches, noted on feat(spec)!: key an object extension's fieldPatches by the field map's own key grammar #29513 and again in the report's out-of-scope list), so the agreement test rightly leaves them out. Right.
  • Public surface: no export added or removed; FieldNameKeySchema stays unexported; the fields describe changes, which is the one content/docs/references/data/object.mdx row the regeneration moved. The published JSON Schema is gitignored and generated at build, so the propertyNames pattern lands there from the same constant. Right.
  • Doors, read in the live tree: defineStack and os validate parse objectExtensions as z.array(ObjectExtensionSchema) (stack.zod.ts:821; cli validate.ts:317 reads ObjectStackDefinitionSchema); the save, restore and draft-promotion doors and every sync that registers a stored row parse the body with OBJECT_EXTENSION_ITEM_SCHEMA = z.preprocess(withoutProtectionEnvelope, ObjectExtensionSchema) (protocol-object-extension-item.ts:75, :244; protocol.ts:2007 registerStoredObjectExtension). So every door that parses an extension refuses the key through the one schema; nothing is spelled twice. Right.
  • Two readers never parsed and still do not: ObjectQL.registerApp (engine.ts:9446) copies ext.fields as given, and the marketplace install (marketplace-install-local-plugin.ts:1842, :3333) reads body.objectExtensions only for the __c judge and the patch-target judge, never through the schema. Both take an artifact os build already parsed, so a bad key reaches them only from a pre-upgrade artifact. The PR body names the fold; the changeset's door list also names "the marketplace install" among the doors that parse an extension, which it is not (see ② should-fix). The narrowing itself is consistent at every parse door.
  • D3 detector object-extension-field-key-grammar-refused: predicate is the refusing door's own (ObjectExtensionSchema.safeParse, invalid_key issues at path length 2 under fields), through the shared refusedExtensionRecordKeys(extension, slot), so it lists exactly the keys the parse refuses as keys and nothing else: a __proto__ key is refused by the guard with a custom code and so is not listed, but that refusal predates this card and is not this narrowing's shape; a malformed field body fails with other codes and is not listed, correctly. It lists, never rewrites (mapCollection returns the entry), names the target, prints only the key and the field's type in from, closes each reason with the parse's message. Appended last in REFUSED_STORED_SHAPES, toMajor: 18 = PROTOCOL_MAJOR, the id is the new semantic entry's id, the stored pass reaches object_extension rows through stored.ts:82. Right.
  • Semantic entry 18: same shape as the sibling object-extension-field-patch-key-grammar-refused entry (id, surface, replacement, reason, acceptanceCriteria; no relevantWhen, as the sibling), filename derived from the id, no other file touched, no tracker number in the runtime strings. Right.
  • Tests (read, not reviewed): refusal cases, controls, the 11-key agreement test guarded against vacuity, and the four detector pins including the no-crossing case between the two detectors; the ablation report says the five refusals, the agreement test and three listing pins go red when the key schema is reverted, with controls green.

② Semver level

.changeset/29523-object-extension-field-key-grammar.md: @objectstack/spec: minor, the only published package the diff moves. Clause-②: no (narrowing) and Metadata-protocol: narrowed on the PR body and the changeset, line-initial, one arm, matching: a value an author could write is now refused, so narrowed, BREAKING, at least minor on the moved package (AGENTS.md Post-Task Checklist 3). The BREAKING banner, the FROM to TO line and the one-line fix are present. The ADR-0087 disposition is one marker, adr-0087: registered object-extension-field-key-grammar-refused, and that id is a new semantic entry in this diff, which is what registered requires. Skills-docs line present; content/docs/data-modeling/object-extensions.mdx gains the paragraph. The level and the declarations match the diff.

Should-fix (changeset prose, not the level): the "at every door that parses an object extension" list names "the marketplace install"; the install does not parse an extension with the schema (it judges __c and patch targets on the built artifact and hands the fold the body as given). Drop it from that list or say the install inherits os build's parse, so the CHANGELOG does not promise a refusal the install door itself does not make.

③ Boundary flags

  • open_questions: empty. Both out-of-scope findings in the report (the registerApp fold copies without parsing; constructor/prototype admitted on the extension) are the fold's trust model and a pre-existing sibling gap, correctly noted and not filed; neither is widened by this diff.
  • Dev flags: none on the diff. Premise-still-valid: true holds against the tree: fields was z.record(z.string(), FieldSchema) at the merge base.
  • Gate verdicts on bd50ef25315a838988c2086cd5f2d6733bbc7b08 (REST, 15 readings one minute apart, 04:10:56Z to 04:25:17Z, and a last read at 04:25Z): check-runs total 0 on every reading; check-suites 5, all queued with no conclusion on every reading (objectstack-fleet, claude, vercel, fly-io, cloudflare-workers-and-pages); workflow runs for the head 0; combined status pending, 0 statuses. No gate has concluded on this head, no run is in progress, and none is claimed green: every derived gate family is still pending. The cause is on the board: the PR reads mergeable: false, mergeable_state: dirty, and git merge-tree origin/main bd50ef25 conflicts in packages/spec/src/conversions/refused-shapes.ts (the spec(conversions): the stored pass does not list a dashboard whose globalFilters entry is keyed dateRange beside a declared dateRange (the #29213 refusal) #29541 dashboard reserved-date-range detector landed on main at the same append site after this branch's merge of 87956dcc). The pull-request workflows do not run on a head GitHub cannot merge. Escalated to the seat as a should-fix before the queue: re-merge main, keep both appended detectors and both ids in the refused-shapes.test.ts list, push, and let the gates run; the re-merged head needs its own record, since this one names bd50ef25.
  • The report's local battery (spec 803/803, dispatch-gates 104/104, dogfood 402 passed) is the dev's reading, not a gate verdict, and is not adopted here.

④ Design judgment

  • packages/spec/src/data/object.zod.ts: holds against its ruling (card spec(object-extension fields): the record key is a bare z.string(), so fields: { 'Region Code': … } parses and is refused only downstream #29523, 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 twin) and its first consumers. The one key constant now has three readers and its comment lists all three; the fields and fieldPatches records are spelled alike (refuseRecordProtoKey(z.record(FieldNameKeySchema, X, { error }))), so the two cannot drift. Tried to break it: a __c key in a vendor package or an unsuffixed key in a .custom pair still parses here and is refused by the doors' one __c judge (protocol-extension-field-suffix.ts), which the schema rightly does not re-spell; a key the target already declares is still the merge's and refuseFieldPatchOfReplacedField's business; the root superRefine and overwrite (materializeTreeSelfReferences) still read extension.fields after the key parse and need no change; Studio's add-field derives its key through toFieldNameLoose and the __c suffix, so it writes a key the grammar admits. No must-fix.
  • packages/spec/src/conversions/refused-shapes.ts: holds against the stored pass and the sibling detector. The shared helper filters on slot, so a row carrying a bad fields key and a bad fieldPatches key is listed once by each and never crossed (pinned). from prints { key: { type } } only, so a field body with a default or help text is not echoed; a non-object field value yields type: undefined without throwing. The fieldPatches detector's from still prints the whole patch, a pre-existing asymmetry the diff does not widen. The detector's parse runs the whole extension twice when both slots are bad, which is a cost the stored preview can bear. No must-fix.
  • packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts: holds against its first consumer, the detector of the same id and the upgrade guide. The surface names objectExtensions[].fields and the defineObjectExtension spelling; the replacement gives the snake_case key with the .custom suffix case; the acceptance criteria state the parse issue, the stored-row fate (the sync counts the row as an error and registers none of it, fieldPatches included; the next save is 422) and the os migrate meta --stored listing, each of which the code read above does. One prose point, no fix owed: the fieldPatches sibling says the sync "names the row (INVALID_METADATA)" where this one says "counts it as an error"; both describe registerStoredObjectExtension throwing into the caller's per-row handling. No must-fix.

Implemented-by: claude/project-thread-fskrbl
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

…-fskrbl

# Conflicts:
#	packages/spec/src/conversions/refused-shapes.ts
… object extension

Claude-Session: https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 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 ObjectExtensionSchema (symbol, a top-level const), fieldPatches (literal, a string literal in refusedExtensionRecordKeys; a string literal in refusedFieldPatchKeys), objectExtensions (literal, a string literal in detect))
  • content/docs/deployment/install-modes.mdx (via objectExtensions (literal, a string literal in detect))
  • content/docs/getting-started/quick-start.mdx (via objectExtensions (literal, a string literal in detect))
  • content/docs/protocol/kernel/i18n-standard.mdx (via objectExtensions (literal, a string literal in detect))
  • content/docs/protocol/objectql/schema.mdx (via objectExtensions (literal, a string literal in detect))

⛔ 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))
  • content/docs/releases/v18/18-0.mdx (via objectExtensions (literal, a string literal in detect))

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

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 2c91a55fc46c19979aba499fbdeea2022d784f35

⚠️ 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 2c91a55fc46c19979aba499fbdeea2022d784f35 → 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#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 649d47eabbc85188b8c8f9576dc7758266ed402e
Local-runs: none
Scope: packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts + packages/spec/src/conversions/refused-shapes.test.ts (the roster pin, read for the conflict resolution), packages/spec/src/migrations/entries/semantic/18.ui-dashboard-global-filter-reserved-date-range-refused.ts (the #29720 entry the merge brought in), .changeset/29523-object-extension-field-key-grammar.md (read for ②, not reviewed), and the cited files of the record on bd50ef25 (stack.zod.ts, protocol-object-extension-item.ts, protocol.ts, protocol-extension-field-suffix.ts, engine.ts, marketplace-install-local-plugin.ts, stored.ts, record-proto-key-guard.ts), unchanged by this delta

This is a DELTA record. It judges 649d47ea against the record of bd50ef25 (card comment 6595440370): two commits since, the merge of origin/main 4bdb73ac (a conflict resolution in refused-shapes.ts and refused-shapes.test.ts, where PR #29720's ui-dashboard-global-filter-reserved-date-range-refused detector landed at the same append site) and 649d47ea, the changeset sentence that answers the earlier record's should-fix 1. Inputs: card #29523 (body and every comment), PR #29774 (body, file list, net diff origin/main...649d47ea, eight files, 289 insertions and 14 deletions, the same eight as before), the head's check-runs.

① Derived judgments

  • The PR's own net change against main is unchanged on five of the eight files: object.zod.ts, the semantic entry, field-customization.test.ts, object-extensions.mdx and the regenerated object.mdx carry byte-identical hunks on bd50ef25 and 649d47ea. Every accept-set and public-surface judgment of the earlier record stands: one key constant, three readers, invalid_key at fields.KEY, no export added or removed, every parse door refusing through the one schema.
  • Conflict resolution, refused-shapes.ts: the added and removed lines are the same as before except the one header-comment line, which now reads after main's reservedDateRangeGlobalFilters clause instead of the duplicateGlobalFilterNames one. The roster keeps every detector main holds and appends the new one last: …, dashboardDuplicateGlobalFilterName, dashboardReservedDateRangeGlobalFilter, objectExtensionFieldPatchKeyGrammar, objectExtensionFieldKeyGrammar. The fieldPatches detector still routes through the shared refusedExtensionRecordKeys(extension, slot) helper; fix(spec): os migrate meta --stored lists a stored dashboard whose global filter reuses the reserved dateRange key #29720's detector is untouched. Right.
  • Conflict resolution, refused-shapes.test.ts: the pinned id list is the roster's order exactly, eleven ids, ui-dashboard-global-filter-reserved-date-range-refused before the two extension ids and the new id last; the spec(object-extension fields): the record key is a bare z.string(), so fields: { 'Region Code': … } parses and is refused only downstream #29523 describe block is appended after spec(conversions): the stored pass maps no object_extension row onto objectExtensions, so no conversion or refused-shape detector ever runs over a stored extension #29554's unchanged. The no-crossing pin (a row with both a bad fields key and a bad fieldPatches key is listed once by each detector) is unchanged. Both 18.ui-dashboard-global-filter-reserved-date-range-refused.ts and 18.object-extension-field-key-grammar-refused.ts exist under entries/semantic/, so the registered-against-the-chain pin has an entry for every roster id. Right.
  • git merge-tree origin/main 649d47ea (with main at e82bf642, past the merge base 2c91a55f) writes a tree with no conflict, so the head is mergeable again.

② Semver level

Unchanged and still matching the diff: @objectstack/spec: minor, Clause-②: no (narrowing), Metadata-protocol: narrowed, BREAKING with FROM to TO and the one adr-0087: registered object-extension-field-key-grammar-refused marker, naming the entry this diff adds. The should-fix of the earlier record is answered: the door sentence now lists defineObjectExtension, defineStack, os validate and os build, and the object_extension save door, and states that the marketplace install does not parse an extension with the schema (it judges a built artifact's __c suffixes and patch targets) and inherits the refusal from os build. That matches the live tree read for the earlier record (marketplace-install-local-plugin.ts:1842 and :3333 call the judges, never the schema; engine.ts:9446 copies ext.fields as given). No should-fix remains on ②.

③ Boundary flags

  • open_questions: still empty; the two noted-not-filed findings (the registerApp fold copies without parsing; constructor/prototype admitted on the extension) are unchanged by the delta.
  • The earlier record's should-fix 2 (re-merge main, keep both appended detectors and both ids in order) is done by 4bdb73ac, as ① reads it.
  • Gate verdicts on 649d47eabbc85188b8c8f9576dc7758266ed402e (REST, 14 readings one minute apart, 04:36:36Z to 04:50:05Z, and a last read at 04:50:18Z): check-runs total 65, completed 65, success 57, skipped 8, failure 0, cancelled 0, pending 0; every GitHub Actions check-suite completed (8 success, 2 skipped); the PR reads mergeable: true, mergeable_state: clean. No run is pending and none is claimed beyond its own conclusion. The five non-Actions check-suites (objectstack-fleet, claude, vercel, fly-io, cloudflare-workers-and-pages) read queued with no conclusion and carry no check-run, exactly as they did on bd50ef25; they are app suites with no gate in them, and no verdict is claimed for them. The 30 runs that were in progress at 04:36Z (Build Core, Test Core 1 to 8, Type Check workspace and consumer gates, Type Check UI, Test UI 1 to 16, Build and E2E UI, Live E2E UI) all concluded success by 04:50Z.

④ Design judgment

  • packages/spec/src/data/object.zod.ts: no hunk changed; the judgment of the record on bd50ef25 stands (holds against the ruling and its first consumers; the __c judge, the replaced-field refinement and the tree-reference overwrite still read the parsed fields after the key parse). No must-fix.
  • packages/spec/src/conversions/refused-shapes.ts: the only delta is the merge placement. The new detector sits after both dashboard detectors and the fieldPatches one, so the stored preview reports in registry order; the two extension detectors filter on slot and cannot cross; fix(spec): os migrate meta --stored lists a stored dashboard whose global filter reuses the reserved dateRange key #29720's detector reads dashboards and shares nothing with the extension readers. Tried to break it: a stack holding a dashboard with a reserved dateRange key and an extension with a bad fields key is listed once by each detector, under its own row, because mapCollection walks each collection on its own. No must-fix.
  • packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts: unchanged; its first consumer, the detector of the same id, is still appended with toMajor: 18. No must-fix.

Implemented-by: claude/project-thread-fskrbl
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

席位核对 — ACCEPT · domain:spec#2 · head 649d47eabbc85188b8c8f9576dc7758266ed402e

  • Shape: ObjectExtensionSchema.fields keys read FieldNameKeySchema, the one constant ObjectSchema.fields and fieldPatches already use; one invalid_key at fields.KEY; one ADR-0087 semantic entry (object-extension-field-key-grammar-refused) and its REFUSED_STORED_SHAPES detector appended after the fieldPatches one; tests; one regenerated reference row; the extension guide. 8 files, +289 −14.
  • Premise measured: the executor measured on a main-built spec that defineStack, os validate, the object_extension save and publish door, every object read, a record insert and the boot re-read all accepted a bad key (os-dev-report 6595183097). The narrowing reading holds.
  • Contract review: record 6595440370 (CONTRACT_REVIEW_TIER, head bd50ef25, PASS, no must-fix) and delta record 6595754343 (head 649d47ea, PASS). Should-fix 1 (the marketplace install named as a parsing door) is adopted in the changeset; should-fix 2 (no CI on a dirty head) is answered by the main merge 4bdb73ac, which kept fix(spec): os migrate meta --stored lists a stored dashboard whose global filter reuses the reserved dateRange key #29720's detector and appended this one last.
  • Declarations: Clause-②: no (narrowing), Metadata-protocol: narrowed, @objectstack/spec minor, BREAKING with FROM → TO and one adr-0087: registered marker.
  • Dogfood pinned to the final head: one locked run read 649d47ea (checked before and after): build 117/117, then the full dogfood battery, 403 files passed, 3 skipped, 0 failed; 3320 tests passed, 28 skipped, 0 failed.
  • Gates: every check-run on the head complete, 57 success, 8 skipped, 0 failure.
  • Landing: not governed (0 paths on the register), 303 changed lines. Ready + auto-merge now.

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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(object-extension fields): the record key is a bare z.string(), so fields: { 'Region Code': … } parses and is refused only downstream

1 participant