Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a standardized “extension” protocol so plugins can attach namespaced custom metadata to core definitions (e.g., Objects/Fields) without modifying base schemas.
Changes:
- Introduces
src/system/extension.zod.ts(extension value/map/definition/registry schemas + helper utilities) and accompanying tests. - Integrates optional
extensionsintoFieldSchemaandObjectSchema, plus tests validating extended metadata. - Adds AI extension reference definitions (field/object) with schemas/tests, and updates generated JSON Schemas + docs + a new example.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/system/index.ts | Exposes the new extension protocol via the System barrel export. |
| packages/spec/src/system/extension.zod.ts | Defines extension schemas and helper utilities for reading/writing extensions. |
| packages/spec/src/system/extension.test.ts | Adds unit tests for extension schemas and helpers. |
| packages/spec/src/data/object.zod.ts | Adds extensions support to Object metadata. |
| packages/spec/src/data/object.test.ts | Tests Objects with/without extensions and complex extension values. |
| packages/spec/src/data/field.zod.ts | Adds extensions support to Field metadata. |
| packages/spec/src/data/field.test.ts | Tests Fields with/without extensions and complex extension values. |
| packages/spec/src/ai/object-extensions.zod.ts | Provides AI-oriented Object extension definitions + schema. |
| packages/spec/src/ai/index.ts | Exports AI extension definitions/schemas from the AI barrel. |
| packages/spec/src/ai/field-extensions.zod.ts | Provides AI-oriented Field extension definitions + schema. |
| packages/spec/src/ai/extensions.test.ts | Tests AI extension definitions and end-to-end schema integration. |
| packages/spec/json-schema/ui/FieldWidgetProps.json | Updates generated UI schema to include extensions on fields. |
| packages/spec/json-schema/system/ExtensionsMap.json | Adds generated JSON schema for ExtensionsMap. |
| packages/spec/json-schema/system/ExtensionValue.json | Adds generated JSON schema for ExtensionValue. |
| packages/spec/json-schema/system/ExtensionRegistry.json | Adds generated JSON schema for ExtensionRegistry. |
| packages/spec/json-schema/system/ExtensionDefinition.json | Adds generated JSON schema for ExtensionDefinition. |
| packages/spec/json-schema/kernel/Manifest.json | Updates generated Manifest schema to include extensions in embedded object/field definitions. |
| packages/spec/json-schema/hub/ComposerResponse.json | Updates generated Hub schema to include extensions in embedded object/field definitions. |
| packages/spec/json-schema/data/Object.json | Updates generated Object schema to include extensions. |
| packages/spec/json-schema/data/Field.json | Updates generated Field schema to include extensions. |
| packages/spec/json-schema/ai/AIObjectExtension.json | Adds generated schema for AI Object extension keys. |
| packages/spec/json-schema/ai/AIFieldExtension.json | Adds generated schema for AI Field extension keys. |
| examples/ai-customer-service/index.ts | Demonstrates adding extensions to an object/fields and validating with schemas. |
| examples/ai-customer-service/README.md | Documents the AI customer service example usage. |
| content/docs/references/system/meta.json | Registers the new System Extension reference section in docs navigation. |
| content/docs/references/system/extension/meta.json | Adds docs metadata for the Extension reference section. |
| content/docs/references/system/extension/ExtensionsMap.mdx | Adds reference doc stub for ExtensionsMap. |
| content/docs/references/system/extension/ExtensionValue.mdx | Adds reference doc stub for ExtensionValue. |
| content/docs/references/system/extension/ExtensionRegistry.mdx | Adds reference doc for ExtensionRegistry. |
| content/docs/references/system/extension/ExtensionDefinition.mdx | Adds reference doc for ExtensionDefinition. |
| content/docs/references/data/object/Object.mdx | Updates Object reference to mention the new extensions property. |
| content/docs/references/data/field/Field.mdx | Updates Field reference to mention the new extensions property. |
| content/docs/references/ai/object-extensions/meta.json | Adds docs metadata for AI Object extensions reference section. |
| content/docs/references/ai/object-extensions/AIObjectExtension.mdx | Adds reference doc for AI Object extension keys. |
| content/docs/references/ai/meta.json | Registers AI extension reference pages in AI docs navigation. |
| content/docs/references/ai/field-extensions/meta.json | Adds docs metadata for AI Field extensions reference section. |
| content/docs/references/ai/field-extensions/AIFieldExtension.mdx | Adds reference doc for AI Field extension keys. |
| content/docs/guides/metadata-extensions.mdx | Adds a full guide describing how to use the extension protocol and helpers. |
| has: (extensions: ExtensionsMap | undefined, key: string): boolean => { | ||
| return extensions ? key in extensions : false; | ||
| }, |
There was a problem hiding this comment.
Extension.has uses the in operator, which returns true for inherited keys like toString even when the extension was never set. Use an own-property check instead (and consider using a null-prototype object for extension maps) to avoid false positives.
| export const ExtensionsMapSchema = z.record( | ||
| z.string().describe('Namespaced extension key (e.g., "plugin_id.property_name")'), | ||
| ExtensionValueSchema.describe('Extension value (string, number, boolean, object, or array)') | ||
| ).optional().describe('Custom extension properties from plugins and modules'); |
There was a problem hiding this comment.
ExtensionsMapSchema is defined as .optional(), which makes the standalone ExtensionsMap type include undefined and produces an awkward top-level JSON Schema (e.g. anyOf with not: {}). Consider making ExtensionsMapSchema the non-optional record, and apply .optional() only at usage sites (Field/Object). Also consider validating keys with a namespaced-key regex (matching ExtensionDefinitionSchema) rather than plain z.string() to enforce the protocol.
| /** | ||
| * Default value if not specified | ||
| */ | ||
| default: z.any().optional().describe('Default value for this extension'), | ||
|
|
There was a problem hiding this comment.
default is z.any(), which allows non-JSON values even though extension values are documented as JSON-compatible. Consider validating default with ExtensionValueSchema (and similarly restricting schema to an object shape if it’s meant to hold JSON Schema).
| ```typescript | ||
| import { ExtensionsMap } from '@objectstack/spec'; | ||
|
|
There was a problem hiding this comment.
The examples in this guide import ExtensionsMap (and later FieldSchema, ObjectSchema, Extension, etc.) directly from @objectstack/spec, but the package root currently only exports namespaces (see packages/spec/src/index.ts). Update the snippets to use the documented namespace import (import { System, Data, AI } from '@objectstack/spec') or direct subpath imports like @objectstack/spec/system / @objectstack/spec/data so they match the actual public API.
|
|
||
| ### Key Concepts | ||
|
|
||
| 1. **Namespaced Keys**: All extensions use dot notation: `plugin_id.property_name` |
There was a problem hiding this comment.
The guide states extension keys use plugin_id.property_name, but elsewhere (and in the schemas) the convention is namespace.property with the property segment in camelCase. Align this description with the intended convention (e.g. plugin_id.propertyName) to avoid encouraging snake_case property segments.
| get: <T = any>(extensions: ExtensionsMap | undefined, key: string, defaultValue?: T): T | undefined => { | ||
| if (!extensions) return defaultValue; | ||
| return (extensions[key] as T) ?? defaultValue; | ||
| }, |
There was a problem hiding this comment.
Extension.get reads extensions[key] directly, which can return inherited prototype properties (e.g. toString) instead of the provided default. Guard with an own-property check (e.g. Object.hasOwn / hasOwnProperty.call) before returning the stored value, otherwise fall back to defaultValue.
| key: z.string() | ||
| .regex(/^[a-z_][a-z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*$/) | ||
| .describe('Fully qualified extension key (e.g., "ai_assistant.vectorIndexed")'), |
There was a problem hiding this comment.
The key regex and nearby docs allow property_name and a property segment starting with uppercase, but the file also states the property should be camelCase. Tighten the regex/examples so the namespace is snake_case and the property segment is lowerCamelCase (and keep docs consistent with that convention).
| appliesTo: z.array(z.enum([ | ||
| 'object', 'field', 'view', 'app', 'dashboard', 'report', 'action', 'workflow' | ||
| ])).describe('Metadata types this extension can be applied to'), |
There was a problem hiding this comment.
appliesTo is required but can currently be an empty array, which makes an ExtensionDefinition effectively meaningless. Consider using a non-empty array constraint (e.g. .nonempty()) so every definition declares at least one applicable target.
| extensions: z.record( | ||
| z.string(), | ||
| ExtensionDefinitionSchema | ||
| ).describe('Registry of available extension definitions'), |
There was a problem hiding this comment.
ExtensionRegistrySchema.extensions allows any string as the record key, so the map key can diverge from ExtensionDefinition.key. Consider constraining the record key with the same key regex and adding a refinement that enforces recordKey === definition.key (and optionally that definition.pluginId matches the namespace prefix).
| * Demonstrates ObjectStack metadata extensions for AI capabilities | ||
| */ | ||
|
|
||
| import { ObjectSchema, Extension } from '@objectstack/spec'; |
There was a problem hiding this comment.
This example imports ObjectSchema and Extension from @objectstack/spec, but the package root entrypoint currently only exports namespaces (e.g. Data, System). Use namespace imports (import { Data, System } from '@objectstack/spec') or subpath imports (@objectstack/spec/data, @objectstack/spec/system) so the example can actually run.
…fused at parse, no longer enforced on writes (objectstack-ai#19909) Fixes objectstack-ai#19629 Clause-②: no (narrowing) Executes maintainer ruling `5791803339` (batch objectstack-ai#215 item 1, letter B, 「215 同意」): `scale` is retired from the `currency` field type. Its refusal remedy and its ADR-0087 migration entry are worded by maintainer ruling 乙 `5805782503` (batch objectstack-ai#218 item 2, 「其他同意」), item 1: > PR objectstack-ai#19909's refusal remedy and ADR-0087 migration entry are reworded: delete `scale` on a currency field; the currency's ISO 4217 minor unit decides its display, and its write allowance stays unconstrained (today's contract). ⛔ No pointer to `currencyConfig.precision`. The consumer half is objectstack-ai/objectui#10221 and is not in this diff: the field designer, the grid summary footer, and, under ruling 乙 item 2, the dashboard `ObjectMetricWidget`. B′ (enforcing a currency width on writes) was not taken and is not here. ## Landing order (ruling 乙 item 2) This PR lands **after** objectstack-ai/objectui#10221 has landed (widened to `ObjectMetricWidget`) **and after** this repository's `.objectui-sha` pin bump has moved past it. That order keeps any zero-decimal window from opening on the grid summary footer or the dashboard metric widget. At this writing the pin is `62597c588072` and objectui#10221 is open with no PR (both re-read in round 3). Holding the landing to that order is the seat's. ## What changes 1. **`@objectstack/spec`, `FieldSchema`** (`packages/spec/src/data/field.zod.ts`): `scale` on a `type: 'currency'` field is refused at parse, with one `custom` issue at `scale` (the file's per-type `superRefine` house pattern). No alias and no grace window. The remedy's first sentence is "`scale` is not valid on a `currency` field — delete the key." It goes on to say that the currency's ISO 4217 minor unit decides how the amount displays, and that the field's write allowance stays unconstrained. It names no other key to carry the value. The `scale` describe names the set the key still applies to: `number`, `percent`, `rating` and `slider`, enforced on writes, and `formula`, rounded. On `currency` it now reads "REFUSED on a `currency` field — delete it there". 2. **`@objectstack/objectql`, record validator** (`packages/objectql/src/validation/record-validator.ts`): the `max_scale` branch no longer reads `def.scale` for `currency`, so the type leaves the enforced set. `min`, `max` and the finite-number check still apply to currency. `percent`, `number`, `rating` and `slider` are unchanged. Nothing else is read in its place: a currency's write allowance stays unconstrained. 3. **ADR-0087 semantic entry** `field-currency-scale-refused` (`packages/spec/src/migrations/entries/semantic/18.field-currency-scale-refused.ts`). Its replacement is "DELETE the key — that is the whole migration", and it says nothing replaces the key. It is deliberately not a mechanical conversion: a conversion that dropped the key would accept it on every load, which is the grace window ruling B refused. `registry.ts` is regenerated with `gen:migration-registry`, never hand-edited. 4. **Shipped examples** that the refusal would reject. `scale: 2` was deleted from each, and nothing was added: - `examples/app-crm/src/objects/account.object.ts`, `opportunity.object.ts`, `opportunity-line-item.object.ts` (×2) - `examples/app-showcase/src/data/objects/account.object.ts`, `client-brief.object.ts`, `expense-report.object.ts`, `external/customer.object.ts`, `external/order.object.ts`, `field-zoo.object.ts`, `invoice.object.ts` (×3), `project.object.ts` (×2) 5. **Documentation examples.** `scale: 2` was deleted from 13 currency examples in `content/docs/concepts/architecture.mdx` (×2), `concepts/metadata-driven.mdx` (×2), `data-modeling/fields.mdx`, `data-modeling/objects.mdx`, `data-modeling/schema-design.mdx`, `protocol/kernel/plugin-spec.mdx`, `protocol/objectql/schema.mdx` (×4) and `protocol/objectql/types.mdx`. The `schema.mdx` property table's `scale` row no longer lists `currency`. It now says "Refused on `currency` — delete it there". 6. **Generated**, never hand-edited: `content/docs/references/data/field.mdx`, `content/docs/references/data/object.mdx` and `content/docs/references/system/migration.mdx` (the `scale` describe). `authorable-surface`, `api-surface`, `json-schema.manifest`, `spec-changes.json` and the upgrade guide did not move, because no key or export was added or removed. The nine `*.metadata-forms.generated.ts` i18n bundles did not move either: round 3 changes a `visibleWhen`, not a label or help text, and those bundles carry only label and help text. 7. **`packages/spec/src/data/field-scale.ts`**: the docblock's `currency` bullet records the retirement and ruling 乙's remedy. It no longer names `CurrencyConfigSchema.precision` as the money faces' override; that claim was false at the pin. 8. **`packages/spec/src/data/object.form.ts`** (round 3): the object designer's quick-add fields grid, `objectForm`, is the form `METADATA_FORM_REGISTRY` serves for the `object` metadata type. Its `scale` row's `visibleWhen` was `"data.type in ['number','currency','percent']"` and is now `"data.type in ['number','percent']"`, with a one-line comment citing this card and ruling `5791803339` B. The `precision`, `min` and `max` rows are unchanged. ## Measured first, on `origin/main` `1f89ba0d70` (built `dist`) - A currency field with `scale: 2` **parsed**: `FieldSchema.safeParse` succeeded. Control: the same with `type: 'number'` also succeeded. - The record validator **refused** a currency write over `scale`: `scale: 2`, write `1.234` → `max_scale {"scale":2,"actual":3}`. A currency field without `scale` accepted `1.23456`. - `max_scale` read `def.scale` on **all four** controls. Each was given `scale: 2` and the write `1.23456`. `number`, `rating` and `slider` answered `{scale:2, actual:5}`. `percent` answered `{scale:4, actual:5}`, because of the fraction-storage derivation. - **Population census**, a TypeScript AST sweep over every tracked `.ts`/`.tsx`/`.mjs`/`.js` file (7,036 files): **15 example sites**, all listed above, and **7 test fixtures**, listed below. A JSON walk over 556 tracked `.json` files found 0 currency-typed objects with `scale`. A windowed scan of 831 `.md`/`.mdx` files found the 13 documentation examples. The one surviving AST hit is `packages/spec/src/data/inline-related-columns.test.ts:103`. It is `InlineGridColumnSchema.scale`, a different schema that the ruling does not cover. ## Readings the round measured, and where each went 1. **`currencyConfig.precision` has no reader.** No face in the pinned console (`.objectui-sha` `62597c588072`) reads it, and no non-test code in this repository does. The cell uses the currency's ISO 4217 minor unit, and the edit widget reads the **field-level** `precision`. → Ruling 乙 item 1: the remedy and the migration entry no longer point to that key. Two findings follow from ruling 乙 item 3: `currencyConfig.precision` is declared and read by nothing, and objectui's `CurrencyField` reads the field-level `precision` as decimal places. Both are filed by the `domain:spec` seat, as objectstack-ai#19992 and objectstack-ai/objectui#10276, ⛔ not riders here. 2. **Ruling B's control reading undercounted the examples.** Fifteen `Field.currency` declarations in `examples/` declared `scale: 2`. → Corrected on the card by `5805294161`. 3. **A fourth face.** The dashboard `ObjectMetricWidget`'s currency arm reads `valueFieldDef.scale ?? 0`. → Ruling 乙 item 2: objectui#10221 is widened to it and lands first (see the landing order above). 4. **Direction of the validator half.** It moves writes from refused to accepted. → Ruling 乙 item 4: `Clause-②: no (narrowing)` stays as ruled. The widening reading from the objectstack-ai#19320 precedent is noted and ⛔ not re-declared. ## Round 3, on top of `40ef043003` **Why.** The at-tier contract-review record `5818558748` on this PR (head `40ef043003`) read **VERDICT: FAIL** with one required fix: `packages/spec/src/data/object.form.ts:190` still offered `scale` on a currency row of the object designer's fields grid, the form registered in `packages/spec/src/system/metadata-form-registry.ts`. After this PR an author using that form is shown a `scale` input on a currency field and is refused at publish, which is the offer-vs-door shape this retirement removes. - **`d7740c324e`**: the fix exactly as the record states it. The `scale` row's `visibleWhen` is now `"data.type in ['number','percent']"`, with the one-line comment. The row was not widened to any other type, and the `precision`, `min` and `max` rows were not touched. The same commit adds the offer-side pin and one changeset sentence (both below). `origin/main` was not merged: the PR reads `mergeable_state: clean`, and no file main changed since the merge base `c8399867b8` is a file this branch changes (`comm` of the two name lists is empty), so CI's merge ref needs nothing from a merge here. **Sweep of `packages/spec/src/**` for any other surface offering `scale` on `currency`.** All 17 `*.form.ts` files were read for `scale` rows, and every non-test source file naming both `scale` and `currency` was read at the hit. | file:line (HEAD `d7740c324e`) | what it is | changed? | |:--|:--|:--| | `data/object.form.ts:191` | the `objectForm` fields-grid `scale` row (it was `:190`) | **yes**, the record's fix | | `data/field.form.ts:79` | the `fieldForm` `scale` row, `"data.type == 'number'"` | no; it does not offer the key on currency (the record's own control) | | `data/field.zod.ts:1235-1236` | `FieldSchema.scale` and its describe, "REFUSED on a `currency` field" | no; that is the door, from round 2 | | `data/field.zod.ts:1025` | `FieldSchema` aliases `decimals` / `decimalPlaces` → `scale` | no; on currency the renamed key lands on the same refusal (acceptance note below) | | `data/field.zod.ts:420` | `CurrencyConfigSchema` alias `scale` → `precision`, inside `currencyConfig` | no; a different schema, pre-existing, and the surface of objectstack-ai#19992 (acceptance note below) | | `data/field.zod.ts:931` | `InlineGridColumnSchema.scale` | no; a different schema, left alone as instructed | | `ui/view.zod.ts:3338` | `FormFieldSchema.scale`, a form-view row's widget override with no type gate by design (a form row usually omits `type`) | no; a different schema, and its describe names no field type | | `ui/view.zod.ts:1600` | the timeline view's `scale` (`hour` … `year`) | no; unrelated | | `ui/bulk-action.zod.ts:157` | `scale` in `BULK_PARAM_WIDGET_CONFIG_KEYS`, a list of keys bulk params REFUSE | no; a refusal list, not an offer | | `data/field-scale.ts:101` | `ABSENT_SCALE_BY_TYPE`, whose one row is `percent` | no; there is no currency row | | `data/numeric-column-representation.ts:49-52` | a dated measurement docblock that names a `currency` field with `scale: 2` refused at the write seam | no; a record of a reading, not an offer (acceptance note below) | | `studio/**` | designer hints | 0 `currency` hits | | `packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts` (outside `packages/spec/src`) | the generated form bundles; `fields.scale` carries only its label and help text (`en` `:147-150`) | no; no predicate lives there, and `check:i18n` reads all nine packages' bundles in sync | A quoted-exact `git grep -n -F "['number','currency','percent']"` over the whole tree now answers one line, the `precision` row at `object.form.ts:189`, which the record says to leave. Before the fix it answered that row and the `scale` row. **The new pin** (`packages/spec/src/data/field-currency-scale-refused.test.ts`, final block). It evaluates each row's `visibleWhen` the way `form-delete-behavior-options.test.ts` already does: a fail-closed reader of the `data.type` spellings these forms use (`==` and `in [...]`, joined by `||`) that throws on anything else, so a predicate it cannot read fails the test instead of being assumed visible or hidden. - **CONTROLS** (lit): the walk over every form in `METADATA_FORM_REGISTRY` finds exactly two `scale` rows, `field:scale` and `object:fields.scale`. `METADATA_FORM_REGISTRY.object` is `objectForm`. The reader lights on a row that does name `currency`. - **Firing case**: the `objectForm` `scale` row is not offered when `data.type` is `currency`. - **Dark controls**: it is still offered for `number` and `percent`, and the door agrees on both types (`FieldSchema.safeParse` with `scale: 2` succeeds). - **Class guard**: no registered form offers `scale` on a currency field. **Old-row pins.** Nothing pinned the old row: before the fix, the quoted-exact `git grep` above found the predicate only in `object.form.ts` itself, and none of the tests that read `objectForm` or `METADATA_FORM_REGISTRY` mention `scale`. There was nothing to reverse. ## Round 2, on top of `7c0a33c6ad` - **`07c9be36bf`**: a merge of `origin/main` `c8399867b8`, run through `scripts/pm/os-regen-merge.sh`. `registry.ts` text-merged, and `gen:migration-registry` then wrote no change. `check:generated` read "All 15 generated artifacts are up to date". - Main changed none of the three reference pages this branch changes, so the script kept the branch's bytes for them. The three reference pages main did change (`api/package-api.mdx`, `security/permission.mdx`, `system/translation.mdx`) are byte-identical to `origin/main` (`git diff` empty). - Quoted-exact `git grep` survival checks: - Each of the 7 semantic entries main added since the merge base appears exactly once in both `origin/main`'s and HEAD's `registry.ts`: `admin-scope-business-unit-blank-refused`, `filter-equality-array-comparand-refused`, `flow-predicate-slot-blank-string-refused`, `package-api-contracts-unmounted-entries-retired`, `rls-predicate-array-comparand-refused`, `translation-per-app-settings-platform-only` and `view-filter-rule-absent-value-refused`. - Main's added reference line "Required and non-blank: an empty or whitespace-only value names no business unit" appears 2 times in `permission.mdx` on both sides. - Main's deleted line "POST /api/v1/packages/upgrade" appears 0 times on both sides. - **`e2c5077421`**: the rewording to ruling 乙, across the refusal message, the describe, the migration entry (with `registry.ts` regenerated), the validator's comments, the `field-scale.ts` docblock, the `schema.mdx` row, the changeset and the spec pins. What is refused and what is accepted did not change. - **`40ef043003`**: `gen:docs` regenerated the three reference pages. A later `gen:schema && gen:docs` produced no further diff, and `check:docs` read "225 generated files in sync with packages/spec". ## Tests, on HEAD `d7740c324e` Every command went through `scripts/pm/os-verify-lock.sh`. Each count is the runner's own summary line. - Build: `turbo run build` over `./packages/*`, `./packages/*/*` and `./examples/*` gave **73 successful, 73 total**. - `@objectstack/spec`: `vitest run --project local` gave **532 files, 15667 passed, 2 todo** (round 2: 15663; the 4 new offer-side tests). `--project repo` gave **35 files, 602 passed**. - `field-currency-scale-refused.test.ts` alone: **12 passed** (8 from round 2, 4 new). - `@objectstack/spec` `typecheck` exited 0. `check:test-typecheck` held 53 files, 255 errors and 142 pinned signatures, the same as rounds 1 and 2. `check:type-check-coverage` exited 0, so the package's `tsconfig` reaches the edited test file. - Consumers that read the registered forms: `@objectstack/platform-objects` translation suites gave 6 files, 137 passed; `@objectstack/lint` `validate-predicate-path-refs.test.ts` gave 1 file, 54 passed; `@objectstack/metadata-protocol` `protocol.meta-types-*` gave 3 files, 38 passed. - NOT re-run this round: `@objectstack/objectql` (on `40ef043003`: 309 files, 5196 passed; 1 file, 5 passed; `typecheck` exit 0 with 40 files, 234 errors and 65 pinned signatures held), `@objectstack/example-crm`, `@objectstack/example-showcase`, and round 1's `platform-objects`, `metadata-core`, `driver-sql` and `service-automation` suites. No file in those packages changed since `40ef043003`. They are declared to CI. ## Reverse verification All legs are one-shot proofs run from the committed state, each with a trap on EXIT/INT/TERM. No rebuild was needed, because the test file resolves `./object.form`, `./field.zod` and `../system/metadata-form-registry` through relative `src` imports. **Round 3, the offer.** `scripts/ablation-replace.mjs` restored the old row, `visibleWhen: "data.type in ['number','currency','percent']"`, on `object.form.ts`. The landing was proved on disk: new-row anchor 1 → 0, old row 0 → 1, blob `6e87b93283` → `295afbe69f`. Then `field-currency-scale-refused.test.ts` ran: **2 failed | 10 passed**. The two failures are the firing case ("expected true to be false") and the class guard (it received `object:fields.scale` where it expected none). The lit CONTROLS, the `number` and `percent` dark controls and all 8 round-2 tests stayed green. Restored: blob `6e87b93283` == HEAD, `git diff HEAD` empty, and `git status --porcelain` empty. Round 2's two legs, on `40ef043003`: - **The ruled wording.** `field.zod.ts` was restored to its round-1 blob `0c24baa382`, which carries the old remedy naming the other key. Main changed nothing in this file across the merge, so that blob differs from HEAD only by the round-2 rewording. The landing was proved by blob hash, with the old-remedy anchor ×1 and the new one ×0. Then `field-currency-scale-refused.test.ts` ran: **4 failed | 4 passed**. The three message pins went red on the first-sentence assertion, and the describe pin went red on its new clause. The firing control (every declared value refused) and the three dark controls stayed green. Restored: blob `d6ada97894` == HEAD, and `git diff HEAD` is empty. - **The absence half.** `scripts/ablation-replace.mjs` appended "Move the value to `currencyConfig.precision` instead." to the new message. Anchor 1 → 0, blob `d6ada97894` → `017fdba420`. Result: **3 failed | 5 passed**. All three failures read "not to match /currencyConfig|precision/", and the describe pin and every control stayed green. Restored: blob == HEAD, and `git diff HEAD` is empty. ## Test pins of the old remedy, reversed A repo-wide grep finds the old remedy pinned in one file only, `packages/spec/src/data/field-currency-scale-refused.test.ts`. It had a message `toContain` on the other key and a control named "the remedy target parses". Here is what replaces them: - One helper asserts the message's first sentence verbatim, the two ruled clauses, and that the message names neither `currencyConfig` nor `precision`. It runs on all three firing paths: the designer shape, the shape beside a `currencyConfig` block, and the `Field.currency()` + `ObjectSchema` path. - The control is now "following the remedy parses". It takes each refused shape, deletes `scale`, adds nothing, and the result is accepted. - The describe pin asserts the new clause and the absence of `currencyConfig`. - `packages/spec/src/data/currency-precision-iso4217.test.ts:293` names that key only as `CurrencyConfigSchema`'s ISO-contradiction issue path. It is not a remedy pin and is unchanged. ## `currencyConfig` hits that remain (`git grep` on HEAD `40ef043003`, unchanged at `d7740c324e`) - **In the branch's added lines** (`c8399867b8..HEAD`): 7 hits. None names that key as a remedy or as a decimal-places carrier. - The changeset's FROM → TO row 2 shows a field that declares a `currencyConfig` block. Its fix is still the deletion. - `field-zoo.object.ts:53` is in the diff only because `scale: 2` was deleted from that line. Its `currencyConfig: { …, precision: 2 }` is `origin/main`'s, unchanged. - `field-currency-scale-refused.test.ts` has 5 hits: two absence assertions, one test title, and two fixtures with a `currencyConfig` block and no `precision` (the firing case and the remedy-follow control). Round 3 added none. - **In the touched files, pre-existing** (the same count on `c8399867b8` and HEAD): - `fields.mdx` 1, `types.mdx` 2 and the three reference pages 2 each. The reference hits are generated from `CurrencyConfigSchema`. - Showcase `account.object.ts` 2, `field-zoo.object.ts` 1. - `field.test.ts` 34 and `field.zod.ts` 10: the `CurrencyConfigSchema` definition and tests, the ISO check, and the `currency`-is-not-a-field-key refusal. - `registry.ts` 1, in another entry. - These stay because this PR did not write them. The key's own disposition is ruling 乙 item 3's first finding, for the `domain:spec` seat. ## Test fixtures re-judged (round 1) These fixtures pinned a currency field with `scale`, or `max_scale` on currency: - `packages/spec/src/data/field.test.ts`: "should accept **number** field with precision and scale" spelled `type: 'currency'`. It now spells `number`, a spelling change only. - `packages/spec/src/data/object.test.ts`: the account fixture's `annual_revenue` lost `scale: 2`. - `packages/spec/src/data/field-scale.test.ts`: the resolver's "declared scale wins" example moved from `currency` to `slider`. - `packages/objectql/src/validation/record-validator.test.ts`: the objectstack-ai#19320 CONTROLS loop dropped `currency`. A new objectstack-ai#19629 block holds the three ruled pins, and its header now cites ruling 乙 for the unconstrained write allowance. - `packages/services/service-automation/src/flow-field-expression-scale.integration.test.ts`: the objectstack-ai#11060 oracle needs a live `scale` gate, so its `total` field moved from `currency` to `number`. - `packages/drivers/driver-sql/src/sql-driver-external-unprovisioned-sort-anchor.test.ts` and `packages/metadata-core/test/injected-column-provenance.test.ts` mirror the showcase external customer. Both lost `scale: 2`. ## Changeset `.changeset/19629-currency-scale-retired.md` sets `@objectstack/spec: minor` and `@objectstack/objectql: minor`, with a **BREAKING** banner. Per AGENTS.md, `Clause-②: no (narrowing)` is breaking, and `check-changeset-no-major` refuses `major`. Round 2 reworded it to ruling 乙: - Its remedy sentence and FROM → TO table now prescribe deletion only. - Its write sentence says the allowance stays unconstrained. - Its console bullet says both faces derive from the currency in the console this release bundles, because of the landing order above. Round 3 added one sentence to its `@objectstack/spec` paragraph, because the form change is visible in Studio: "Studio's object editor no longer offers `scale` on a currency field: the fields grid of the `objectForm` this package registers in `METADATA_FORM_REGISTRY` now shows it only for `number` and `percent`." It names no key to carry the value, so it holds under ruling 乙. It carries the ADR-0087 marker `registered field-currency-scale-refused`. `check-adr-0087-registration` reads "`[BREAKING+bang+clause-②-narrowing]` registered field-currency-scale-refused". The test-only packages (`driver-sql`, `metadata-core`, `service-automation`) ship nothing that changed, and the examples are private. ## Acceptance notes - `docs/qa/platform-checklist/areas/records-forms.json`: the field-zoo fixture line still reads "f_currency scale 2". Its `knownGaps` text, which says `scale` is not enforced on write, has been stale since objectstack-ai#7501. This is checklist-owned text and is left for that seat. - Four documentation YAML examples keep `precision: 18` on a currency field (`schema.mdx` ×3, `types.mdx`). They are meant as DECIMAL(18,2) total digits, but the pinned edit widget reads field-level `precision` on currency as fraction digits. This predates the PR. It is the surface of ruling 乙 item 3's second finding. - `data-modeling/fields.mdx` ("Currency Configuration") and `protocol/objectql/types.mdx` show `currencyConfig: { precision: 2, … }` as a currency's configuration. This predates the PR and is the surface of ruling 乙 item 3's first finding. - `FieldSchema`'s alias table renames `decimals` / `decimalPlaces` to `scale`. On a currency field that rename now leads to this refusal, and its remedy tells the author to delete the key. - hotcrm declares `scale: 2` on a currency quote field (hotcrm#1206, cited by the flow oracle). That becomes a refused parse downstream, and the migration entry prescribes the deletion. - objectui#10221's title still says "derive the summary footer's fraction digits from `currencyConfig.precision` / ISO minor units", which is ruling B's wording. Under ruling 乙 only the ISO half holds. Rewording that card belongs to the `domain:ui` seat. - Round 3 sweep: `CurrencyConfigSchema` (`field.zod.ts:420`) accepts `currencyConfig: { scale: … }` and renames it to `currencyConfig.precision`, the key objectstack-ai#19992 records as read by nothing. This predates the PR and is not `FieldSchema.scale`; it goes wherever objectstack-ai#19992's enforce-or-remove decision takes that key. - Round 3 sweep: the field editor form's `precision` row (`field.form.ts:78`) is shown on currency with the help text "Decimal places (e.g., 2 for $10.50)", while `FieldSchema.precision`'s describe (`field.zod.ts:1211`) says "Total digits" and `objectForm`'s own `precision` row says "Total digits". That is a `precision` row, which this PR does not touch; it is the spec-side neighbour of objectstack-ai/objectui#10276. - Round 3 sweep: `numeric-column-representation.ts:49-52` records a write-seam measurement in which a `currency` field declaring `scale: 2` was refused. After this PR the seam no longer reads `scale` on currency, so the docblock describes a past reading. It is an internal comment, not an offer, and it is left alone. ## Gates, on HEAD `d7740c324e` - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived **120** families from this worktree. Every one was run with its exit code captured before any pipe, and **all 120 exited 0**. The `--ran` reconciliation read: "120 derived famil(ies) accounted for — 120 run, 0 NOT-MEASURED (a DERIVED zero — all 120 recorded an exit code and none of them is 3)". The derivation warned of a stale tree: `origin/main` `e8f163fc3a` is 23 commits past the merge base, and 18 derivation inputs changed in that range (among them `.github/workflows/lint.yml` and `package.json`). CI runs the PR's merge ref with the current copies. - `pnpm --filter @objectstack/spec check:generated`: "All 15 generated artifacts are up to date". - `check:i18n`: "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)". `check:nul-bytes`: OK, no raw ASCII control bytes. - `check-changeset-no-major`: no `major` bump. The level axis reads this PR body, so it is NOT MEASURED locally. - CI on `d7740c324e`, read at 2026-09-24T18:25Z: 35 check-runs, 33 `success` and 2 `skipped` (`Console Pin Gate`, `Packed-tarball smoke (opt-in)`), none failed. All seven required contexts and `Check Changeset` read `success`. - Lint was narrowed to the diff and was not run repo-wide. `eslint --no-inline-config --format json` over the 25 changed `.ts`/`.mjs` files (`c8399867b8..HEAD`, round 3 adds `object.form.ts`) reported 25 files, 0 errors, 0 warnings and no ignore notices. The narrowing holds because `eslint.config.mjs` never enables type-aware linting (its own comment at lines 326-328: no `parserOptions.project`, no typed rules), so this diff cannot move any untouched file's verdict. The full `pnpm lint` belongs to CI. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… dataset-*, hook-* and metadata-* migration entries states each lesson in words, not tracker numbers (stage 5) (objectstack-ai#20522) Part of objectstack-ai#20233 Stage 5: the field-, export-, api-, dataset-, hook- and metadata- families. Clause-②: no **Stage 5 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. One `surface` moves, under ruling A of the stage-1 ACCEPT (`5858839916`): it carried two tracker numbers. ## What this does `os migrate meta` prints every ADR-0087 semantic entry it crosses as one block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's `reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's runtime-string rule applies to all of it: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (`pnpm check:doc-authoring`): the lesson goes into the text.」 Form **D** of ruling C+D on objectstack-ai#19123 (`5749154545`) sets the shape: the lesson in words, and no number, dead or alive; a cross-repo number is still a tracker number. This stage covers the next six families, `field-`, `export-`, `api-`, `dataset-`, `hook-` and `metadata-`: **110 sites → 0** in the three prose fields and **2 → 0** in `surface`, across 25 entry files. Each site now says what the cited ruling, measurement or fix decided. ADR ids stay. `registry.ts`, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated from the entries (`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`), never hand-edited. The pin now holds seventeen families. ## Census — tracker ids in the author-shown fields **Instrument.** Stage 4's TypeScript-AST census, the same script: for each entry object literal under `packages/spec/src/migrations/entries/**` it evaluates `replacement`, `reason`, `acceptanceCriteria` and (separately) `surface`, joining string literals with `+`, and counts `#` followed by 4 or 5 digits at a word boundary. On base `9e9bb464` it reads the whole tree at **571** sites / 7 `surface` / 61 short, which is stage 4's recorded after-count. Unevaluable fields: 0. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), before and after. - **Dark (comment lines):** 823 `//` / docblock lines in entry files carry a tracker id, and none is counted; 823 before and after. Comment lines are objectstack-ai#20234's surface, and this PR touches none (proved below). **Base `9e9bb464`:** `field-` 10 entries, **28** sites (5 / 20 / 3); `export-` 3, **21** (0 / 21 / 0); `hook-` 4, **17** (0 / 17 / 0); `api-` 5, **16** (0 / 14 / 2); `metadata-` 7, **16** (1 / 15 / 0); `dataset-` 3, **12** (2 / 8 / 2), plus **2** in `surface`. **110** sites (8 / 95 / 7) in 24 of the 32 entries; 75 distinct ids (71 bare, 1 spelled `framework#`, 3 `objectui#`). Short numbers: 14. **After this PR:** all six families **0**, `surface` 0; the eleven earlier families still 0; whole tree **571 → 461**, `surface` **7 → 5**, short **61 → 50**. The PM's rough line count (126 sites, 25 files) is a wider instrument; the AST reading is 110 in 24 files, and the 25th file carries only short decision-batch numbers and ruling-record ids. | entry | sites (replacement / reason / acceptanceCriteria) | surface | short numbers | |---|---|---|---| | `17.api-runtime-create-withdrawn` | 9 (0 / 7 / 2) | | | | `17.export-axis-opt-in` | 7 (0 / 7 / 0) | | | | `17.export-field-meta-constraints-retired` | 8 (0 / 8 / 0) | | | | `17.field-runtime-create-withdrawn` | 9 (0 / 6 / 3) | | | | `17.hook-context-session-roles-retired` | 6 (0 / 6 / 0) | | | | `17.hook-register-empty-object-target-refused` | 8 (0 / 8 / 0) | | | | `18.api-assembled-entry-split` | 1 (0 / 1 / 0) | | 1 | | `18.api-error-retry-after-unit-in-key` | 3 (0 / 3 / 0) | | 1 | | `18.api-runtime-config-durations-unit-in-key` | 3 (0 / 3 / 0) | | 1 | | `18.dataset-filter-nested-relation-equality-array-refused-at-save` | 2 (0 / 2 / 0) | | | | `18.dataset-measure-aggregate-field-type-refused` | 5 (1 / 2 / 2) | 2 | 2 (1 kept) | | `18.dataset-measure-selecting-aggregate-field-type-refused` | 5 (1 / 4 / 0) | | 3 (1 kept) | | `18.export-job-family-retired` | 6 (0 / 6 / 0) | | 2 | | `18.field-currency-scale-refused` | 0 | | 2 | | `18.field-max-length-malformed-or-misplaced-refused` | 8 (3 / 5 / 0) | | | | `18.field-min-length-malformed-or-misplaced-refused` | 3 (1 / 2 / 0) | | | | `18.field-multiple-non-capable-type-refused` | 4 (0 / 4 / 0) | | 1 | | `18.field-predicate-reference-traversal-refused` | 2 (0 / 2 / 0) | | | | `18.field-scale-precision-integer-refused` | 2 (1 / 1 / 0) | | 1 (kept) | | `18.hook-register-undispatched-lifecycle-event-refused` | 3 (0 / 3 / 0) | | | | `18.metadata-customization-protocol-retired` | 4 (0 / 4 / 0) | | | | `18.metadata-endpoints-switch-radius-repartitioned` | 3 (0 / 3 / 0) | | | | `18.metadata-manager-config-cache-ttl-unit-in-key` | 3 (1 / 2 / 0) | | | | `18.metadata-manager-config-inert-cache-keys-retired` | 3 (0 / 3 / 0) | | | | `18.metadata-plugin-additional-types-retired` | 3 (0 / 3 / 0) | | | | **total, 25 entries** | **110 (8 / 95 / 7)** | **2** | **14 (3 kept)** | The seven entries of these families that carried no number are untouched: `api-endpoint-cache-ttl-unit-in-key`, `field-inline-and-related-list-columns-closed`, `field-master-detail-set-null-refused`, `field-reference-to-spelling-retired`, `hook-timeout-unit-in-key`, `metadata-changed-event-payload-retired`, `metadata-item-name-grammar-enforced`. ## Text only — proved by a base-vs-head AST comparison For every entry file this PR changes, both versions (`9e9bb464` and the head) are parsed and compared: every import declaration; every property other than the three prose fields, by evaluated value (so `id`, `from` / `to` and any matcher); every comment token in the file; and the code skeleton, token by token with each run of joined string literals collapsed to one. `surface` is allowed to differ only where the base value carried a tracker id and the head value carries none. **25 files compared, 0 with a non-prose change**; one note, the ruling-A `surface` of `18.dataset-measure-aggregate-field-type-refused`. The instrument is shown able to fail first: on an in-memory copy it reports DETECTED for a mutated `id`, a mutated comment, a mutated `surface` whose base carried no tracker id, a mutated code token and a mutated import, and stays dark on a prose-only mutation. So none of objectstack-ai#20234's comment lines moved, and no entry's identity or matching moved. ## Every citation read, and what the text now says Each cited id was read with a single-card REST read (body plus the ruling, measurement or landing comments), resolved against the repository its sentence names: 72 in this repository (one spelled `framework#`, the repository's old directory name) and 3 in `objectstack-ai/objectui`. Ids are in code spans so this body posts no cross-references. **4 ids answer 404** on both the issues and the pulls endpoint (re-probed with a 200 control, `14478`); those sentences are rewritten from what `main` records, listed in Acceptance notes. **`api-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `5488` | Maintainer, 2026-08-07: flip `api` to `allowRuntimeCreate: false` and refuse at the write inlet (remove, not converge the read path); re-entry only with a real consumption path. | the measurement and the ruling were already in the sentence; the id is dropped | | `5040` | The declarative endpoint executor project; its acceptance step moved showcase's endpoints to the artifact route, live. | "showcase uses the artifact route, and its declared endpoints serve live" | | `4052` | `BatchOptions.validateOnly`, retired the same way (a runtime verdict, no D2). | named by the key, which the sentence already carried | | `5279` (PR), `5189`, `5203` (PR) | The publish gate for `api` drafts; `publishPackage` and load-time `buildEndpointIndex` running the endpoint gate. | named by the functions the sentence already carried | | `2657` | Studio metadata coverage: Part B asks which un-typed concepts (`apis` among them) become registered types. | "if the Studio metadata-coverage work promotes `apis` to a registered type WITH A REAL CONSUMPTION PATH" | | `5311` | The direct-active `saveMetaItem` write was a third path past the namespace and duplicate gates; closed as subsumed by the `5488` ruling. | "The same refusal closes the direct-active write too, which had been a third path past the endpoint namespace and duplicate-path gates." | | `18576` | Maintainer, 2026-09-17, option B: narrow the `./api` entry, rather than add a bundle-weight rule (A) or accept the weight (C). | "Maintainer ruling of 2026-09-17, option B (narrow the entry, rather than add a bundle-weight rule to the browser-reachability ledger or accept the weight as it stood)" | | `14478`, `15677` | Maintainer ruling B of 2026-09-02: a duration's unit lives in the key name, no offender grandfathered; the `api/` stack of that ruling. | "Maintainer ruling B of 2026-09-02 on duration-shaped keys"; trailing ids dropped | **`export-` (15 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `6350` | The stock reconciliation of the v17 train's breaking changesets against the ledger, which backfilled this entry. | "Registered (backfilled) by the stock reconciliation that compared the breaking changesets already on the v17 release train against this ledger" | | `3544`, `3710` | The user-level export axis, and its extension to the CSV attachments scheduled reports mail out. | "the export axis, and its extension to the CSV attachments scheduled reports mail out" | | `6148` | **404** — see Acceptance notes. | "the gate that makes a breaking changeset state its ADR-0087 disposition" | | `3956` (spelled `framework#`) | The import dry run skipped the field-level validation the real write ran; the hand-copied pre-check mirror was added to close it. | "added when the dry run was found skipping the field-level validation the real write ran" | | `4633`, `6532` (PR) | Maintainer, 2026-08-06, ruling D: a validate-only protocol operation, so the dry run's prediction is the engine's verdict; the mirror retired. | "the maintainer's 2026-08-06 ruling D (a validate-only protocol operation, so the dry run's prediction is the engine's verdict by construction)" | | `4484`, `5540`, `6011` | The `findStream`, `IStorageService.list` and `actor-user-roles-to-positions` retirements. | named by their surfaces, which the sentence already carried | | `6536` | The eight keys left read by nothing after the mirror retired, deferred to their own sweep. | "this is the removal the dry-run change deliberately deferred to a sweep of its own" | | `17158` | Maintainer, 2026-09-12, ruling A: retire the family, `IExportService` and `ScheduleExportInput`; `ScheduleState` with it unless a live consumer is measured. Landing route A, 2026-09-24; scope note, 2026-09-25. | "maintainer ruling A of 2026-09-12 (…), the landing route the maintainer ruled on 2026-09-24 (route A: …), and a scope note the maintainer agreed on 2026-09-25" | | `objectui#10247` | The console retires its own async-export path first. | "objectui retires its own side of the unimplemented async-export path first"; "which carries objectui's own retirement" | | `19543` | Three sibling list doors declared `limit` / `cursor` and never read them; the export-job list's door folded into this retirement. | "one of three sibling list doors found declaring them and never reading them" | | `16320` | The seven cron-typed positions nothing read, retired (three on these defs). | "once the retirement of the cron-typed positions nothing read had deleted theirs"; "Those earlier cron-position deletions" | **`field-` (20 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `7893` | Maintainer, 2026-08-12: retire the runtime `field` write channel rather than build a read path. | the measurement and the ruling were already in the sentence; the id is dropped | | `5488`, `4052` | The `api` and `validateOnly` withdrawals. | "the `api` withdrawal's rationale reused (`api-runtime-create-withdrawn`)"; named by key | | `7743`, `7894` | The field overlay lock (`NOT_OVERRIDABLE`); the plural `/meta/fields/` spelling folded onto the singular. | "The field overlay refusal"; the plural door named by its path | | `8169` | The `_diagnostics` envelope asserts well-formedness only; it has no "in effect" axis. | "(the envelope has no "in effect" axis)" | | `11566`, `11989` (PR), `11950` | Maintainer, 2026-08-24: tighten both halves of `maxLength` (value shape and applicable types); shipped on 17.x; registered in a follow-up. | "Maintainer ruling of 2026-08-24, tightening both halves — the value's shape and the types the key applies to"; "registration was deferred to a follow-up" | | `11875` | Maintainer, 2026-08-25, option 1: the write seam enforces `maxLength` for `signature` / `qrcode`, then both join the bounded-string set. | "which joined once the write seam enforced a declared bound on them" | | `11431` | The SQL driver stops reading a malformed bound as authoritative (the `varchar(0)` plan). | "until it was taught to stop reading a malformed bound as authoritative" | | `8321` | `scale` / `precision` refused as non-integer or negative — the house pattern. | "the house pattern the `precision`/`scale` integer refusal set" | | `11949` | Maintainer, 2026-08-25, option B: `minLength` is `int().min(1)`, zero refused, the `maxLength` template in full. | "Maintainer ruling of 2026-08-25 (option B, the lower bound at 1) … the defect pair the 2026-08-24 ruling closed for `maxLength`" | | `17469`, `11437` | Maintainer, 2026-09-13, option 1′: `multiple: true` refused outside the multi-capable types — the earlier `radio` rule generalised; the driver derives its JSON column from the spec predicate. | "Maintainer ruling of 2026-09-13, option 1′ (the earlier rule refusing an authored `radio` with `multiple: true`, generalised)" | | `objectui#8886`, `objectui#8937` | The console's related list shaped its parent filter from the spec predicate; its follow-up recorded the driver half as owed. | "the console's related list pinned the divergence on the consumer side when it began shaping that filter from the spec predicate, and its follow-up recorded the driver half as owed and not filed" | | `20078` | Triage, 2026-09-25, remedy A: refuse the traversal at authoring with a prescription; hydrating the field level is a capability of its own. | "Triage routed this on 2026-09-25 to remedy A: refuse the traversal at authoring, with a prescription." | | `18682` | A validation rule or visibility predicate reads one hop through a lookup. | "is served, one hop deep, and stays accepted" | | `7501` | `scale` enforced at write time: an over-scale write refused, never rounded. | "when `scale` was made enforced at write time (an over-scale write refused, never rounded)"; "the write-time `scale` enforcement" | **`hook-` (12 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4839`, `5049` (PR) | Both `session.roles` admin exemptions removed; the record lock and the delegation guard back on the one permission vocabulary. | "An earlier fix removed both readers, returning the record lock and the delegation guard to the one permission vocabulary" | | `4579`, `4657` | The `openApi31` and `activationEvents` retirements. | named by their surfaces, which the sentence already carried | | `3733` | Measured: a key removed from a non-strict schema parses clean and is silently dropped. | "as a removed field key was measured to be" | | `5050` | This retirement's own card. | trailing id dropped | | `4281` (PR) | An empty hook target is not "no target": closed at the two metadata doors. | "An earlier breaking fix established that an empty hook target is not "no target""; "that fix's headline failure mode" | | `5928` | The `excludeObjects` face (global except named objects); it declined to change the matcher's read in passing. | "The later `excludeObjects` face (a hook global except for the objects it names)"; "the `excludeObjects` change declined to do it in passing" | | `6573` | **404** — see Acceptance notes. | trailing id dropped; the entry states the change | | `4001` | The unknown-key strictness campaign (ADR-0078). | trailing id dropped (ADR-0078 kept) | | `3195` | The hook taxonomy collapsed from 18 events to the 8 dispatched; a registration guard warns on the rest. | "the change that collapsed the hook taxonomy to the eight dispatched events made this branch a warn" | | `17713` | This refusal's own card. | trailing id dropped | **`metadata-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `12057` | Maintainer, 2026-08-29: retirement adopted, re-scope rejected (the card's ruling comment is no longer on it; `main` records it). | "the maintainer's ruling of 2026-08-29 adopted retirement and rejected a re-scope" | | `13135` | **404** — see Acceptance notes. | "executed widened to the full coupling set the fork report on that ruling measured" | | `11513` | **404** — see Acceptance notes. | "the 2026-08-24 lock-and-clone ruling (lock the packaged base, customize a clone) left deliberately unchartered" | | `15542`, `15854` | Maintainer, 2026-09-06, ruled together (2 + A): every `endpoints.*` switch gates exactly the face its name states; the whole-store family gets its own key. | "The maintainer ruled the two together on 2026-09-06 as one principle: …" | | `15543` | No shipped boot path constructs a `RestServerConfig`. | the measurement was already in the sentence; the id is dropped | | `15624` | The outer cache keys read by nothing, retired on their own (the owning seat's ruling). | "(the owning seat's ruling, conditioned on the measurement below and re-taken on the merged ref)"; "retired on its own under ADR-0049" | | `14478` | Maintainer ruling B of 2026-09-02 on duration units. | "Maintainer ruling B of 2026-09-02 on duration-shaped keys"; "the duration-unit rename" | | `8586`, `8421` | Maintainer, 2026-08-14, jointly: remove `additionalTypes`, and refuse unknown `/meta` types by the static registry. | "maintainer ruling of 2026-08-14: remove the key, jointly with refusing unknown types at the `/meta` boundary by the static registry" | | `4212` | Four of five declared plugin lifecycle hooks were never invoked, `onInstall` among them. | "the plugin lifecycle's `onInstall` (a documented hook with no invocation site)" | **`dataset-` (9 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `19889` | Ruling A of 2026-09-24: the schema door refuses what the compile face refuses; a field spec with no `$` key stays undescended. | "ruling A of 2026-09-24, which made the schema door refuse what the compile face refuses, drew the line there" | | `20080` | Triage, 2026-09-25, remedy A: refine the two analytics carriers; remedy B (stop the analytics door descending) changes what a nested list means. | "Triage on 2026-09-25 routed the fix to the two analytics carriers instead, rather than stop the analytics door descending, which would change what a nested list means" | | `16737`, `16099` | The measured defect: `AVG()` over a datetime is an average year on SQLite and an error on Postgres. | the measurement was already in the sentence; the ids are dropped | | `16353` | The aggregate × field-type table, declared in the spec. | named by the table, which the sentence already carried | | `16099` (in `surface` and `acceptanceCriteria`) | The widening of the refusal to `sum` / `avg` over every field class, registered `not-required` against this entry. | "followed in a later change"; "widened by a later change" | | `17560` | Director ruling B of 2026-09-13: the compile door enforces the table for every aggregate. | "Director ruling B of 2026-09-13: …"; the sibling entry named by id | | `15768`, `16236` | `measureResultType` typing `min` / `max` over strings and over a `formula` return type. | the sentence already states both | | `17513` | Closed as a duplicate with zero rulings on it. | "the card it cited is closed as a duplicate with zero rulings on it" | ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened `COVERED_PREFIXES` gains `field-`, `export-`, `api-`, `dataset-`, `hook-` and `metadata-` (17 prefixes; `data-` still selects neither `datasource-` nor `dataset-`, and `api-` does not select `apimethod-`: the match is `startsWith`). The `REWRITTEN` floor rises from **88 to 113** ids: the 25 entries this stage rewrote. The three `it` blocks are textually unchanged. The file keeps its stage-1 name; the header lists the seventeen covered families. ## Ablation — the widened pin can fail on a new-family block From committed state, HEAD `d24a253bd0`, in one lock turn, with `scripts/ablation-replace.mjs` in wrap mode (it owns the mutation's restore; the leg script adds its own `trap … EXIT INT TERM` that restores `registry.ts` from `HEAD` by absolute path and checks the blob) and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is built from the generated `registry.ts`, so that is the file mutated. - **Mutation.** In `registry.ts`, the `reason` of `hook-register-undispatched-lifecycle-event-refused`: anchor `made this branch a warn — ` → `made this branch a warn (objectstack-ai#3195) — `. The tool read anchor 1 → 0 and replacement 0 → 1, blob `94bc4938` → `b045dbfd`. - **Mutate leg.** Spec build exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `hook-register-undispatched-lifecycle-event-refused: the printed guidance cites a tracker id: expected 'objectstack-ai#3195' to be undefined`. - **Restore.** Tool-proven: blob `94bc4938` == HEAD, `git diff HEAD` empty. - **Restore leg.** Spec build exit 0. The `--absent` preflight found the marker in none of 224 built files, with the working tree clean against HEAD. Pin: **green**, `3 passed`. Whole tree afterwards: 0 dirty paths. ## Verification Final head **`d24a253bd0`**; every reading below was taken there. Every heavy run went through `scripts/pm/os-verify-lock.sh`, with per-step exit codes recorded separately. - **Build:** `pnpm exec turbo run build --concurrency=2 --filter='@objectstack/cli^...'` gives `Tasks: 58 successful, 58 total`; the ten packages outside that closure (for `check:dual-build-cjs-loads`) give `Tasks: 68 successful, 68 total`. - **Pin with its neighbour:** `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`, `Tests 10 passed | 1 skipped` (the skip is the default-range file's own `skipIf`). - **CLI unit:** `test/vitest-tiers-partition.test.ts` and `src/utils/spec-release-changes.test.ts` give `Test Files 2 passed`, `Tests 28 passed`. - **Spec, the whole `local` project:** `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` gives `Test Files 573 passed (573)`, `Tests 16801 passed | 1 todo`. **The `repo` project:** `Test Files 38 passed`, `Tests 690 passed`. - **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0 (test layer: 53 files / 251 errors held in its ledger); `pnpm --filter @objectstack/cli typecheck` exits 0 (3 files / 28 errors held). - **Gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives **89** families. `--ran` over the recorded exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:doc-authoring` ("16735 customer-facing string(s) across 1168 spec sources clean"), `check:issue-citations`, `check:migration-registry` ("registry.ts is current (311 semantic, 230 retired-key, 206 retired-def)"), `check:spec-changes`, `check:upgrade-guide`, `check:generated` ("All 15 generated artifacts are up to date"), `check:org-identifier` ("no removed session.tenantId alias"), `check:nul-bytes`, `check:dual-build-cjs-loads` (104 require entry points across 66 packages load), `check:type-check-debt`, `check:adr-0087-registration` and `check:changeset-no-major`. `check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT MET`: ten packages had no `dist/`); after the ten were built it exits 0, and that is the reading recorded. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 27 changed `.ts` files reports 27 files, 0 errors, 0 warnings. - The population is read from `eslint.config.mjs`: `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 27 are in it (no file-ignored notice). - Invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules), so a text edit cannot move the verdict on a file it does not touch. - **Mergeability:** `main` moved one commit past the base, to `0bbe4005`: the landed `20511` (a `qa-` entry, a retired key and `registry.ts`). A driver-free bare-clone `merge-tree --write-tree` of `d24a253bd0` against `0bbe4005` exits 0 with no conflicted path, and the census over that merged tree reads 0 sites in all six families (whole tree 461), so `main` was not merged in; CI's merge ref runs the registry gates on the merged tree. ## Acceptance notes - **Four dead ids, rewritten from what `main` records.** Each answers 404 on both the issues and the pulls endpoint, with a 200 control (`14478`). - `6148`: `scripts/check-adr-0087-registration.mjs`'s header (a declared-breaking changeset must state its ADR-0087 disposition in writing) — the same reading stage 4 used. - `6573`: the objectql CHANGELOG entry "`engine.registerHook` refuses an empty `object` target and a scope whose two faces cancel out", and `engine.ts`'s refusal docblocks. It is this entry's own change, so the trailing id is dropped; the entry already states the change. - `13135`: `packages/metadata/CHANGELOG.md` ("retire the paper metadata-customization protocol with its full coupling set … re-charter of" the `12057` ruling, "the maintainer adopted retirement … 2026-08-29 … and" it "charters the full coupling set the fork report measured"). - `11513`: ADR-0126 names it the lock-and-clone ruling of 2026-08-24 (Salesforce-style: lock the packaged base, customize a clone), and its section 9 records the per-field overlay layer as explicitly not chartered. - **One ruling read from `main`, not from its card.** `12057` answers 200 but carries only its triage comment; the 2026-08-29 ruling the entry names is recorded in `packages/metadata/CHANGELOG.md`, and the sentence says only what that record says. - **Short numbers, ruling-record ids and acknowledgements went too (invisible to the regex).** Eleven decision-batch numbers (`objectstack-ai#43` ×2, `objectstack-ai#59` ×2, `objectstack-ai#122`, `objectstack-ai#127`, `objectstack-ai#128`, `objectstack-ai#145`, `objectstack-ai#215`, `objectstack-ai#218`, `objectstack-ai#221`) and five ruling-record comment ids (in `field-currency-scale-refused`, `field-predicate-reference-traversal-refused` and `dataset-filter-nested-relation-equality-array-refused-at-save`) are numbers an author is shown and cannot follow, so each is dropped. So are five maintainer acknowledgements (「同意,其他也同意」 in `api-assembled-entry-split`, 「同意」 ×3 in `export-job-family-retired`, 「同意」 in `metadata-customization-protocol-retired`): they record only that a batch was approved, and each sentence now states the ruling's date and content instead. The two quotes that carry the lesson stay verbatim: "both legs, table in spec" and 「`min`/`max` numeric plus `date`/`datetime`; everything else refused」. - **Three `Prime Directive objectstack-ai#12` / `PD objectstack-ai#12` spellings are kept.** They name a rule in this repository's AGENTS.md, not a tracker item, like the ADR ids; the earlier stages kept the same spellings in the `ui-`, `plugin-` and `system-` families. - **`surface`, per ruling A.** `18.dataset-measure-aggregate-field-type-refused` was the only entry of these families whose `surface` carried tracker ids (two). Its header now names the later widening and the sibling entry in words, and the AST comparison shows nothing else in it moved. No test or tool reads that `surface`: outside the migration tree, the pin and the generated projections, the id appears only in three earlier changesets' ADR-0087 disposition markers (and this PR's changeset). - **"issue NNNN" / "PR NNNN" spellings, checked by hand.** A scan of the six families' evaluated prose for any run of three or more digits and for `issue` / `card` / `PR` / `batch` / `record` / `summon` / `item` plus a number now finds only HTTP statuses, ports, byte counts, durations, dates, commit shas, SQLSTATE and TS error codes, ADR ids and example values. - **No test pinned a removed tracker number of these entries.** A search of test files for the 25 entry ids finds only the pin, `export-job-family-retirement.test.ts` (it asserts `not a D2 conversion` and the backtick-free `surface`, both unchanged) and comment lines; a search for the 75 cited numbers in `toMatch` / `toContain` assertions finds only unrelated digit runs and runtime strings outside this card (`api-endpoint-step.test.ts` and `endpoint-executor.test.ts` assert a `5040` hint, which is objectstack-ai#20513's surface). - **No open PR touches these six families.** Read twice: at the start of this stage, 10 open PRs and 634 file rows; again just before opening this one, 13 open PRs and 633 rows (the Version Packages PR `17076` included both times). None carries a `migrations/entries/semantic/NN.(field|export|api|dataset|hook|metadata)-*` file. PR `20512` adds a retired-key file `18.api__RestApiConfig__documentation.version.ts`; a retired key has no id and is not in `step.semantic`, so the widened `api-` prefix does not select it. PRs `20512`, `20504`, `20460` and `20458` add entries in other families (`rest-`, `turso-`, `stack-`, `cube-`) and regenerate `registry.ts`: ordinary concurrency, regenerate on merge. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1–4; only the protocol-17 entries appear in them. - **What later stages pick up** (whole tree at this head, same instrument): **461** prose-field sites in the other families, **50** short numbers, **5** `surface` sites. ## Line budget Entry files: **265 changed lines** (+147 / −118) across 25 files, against the stage-1 ≈400 budget. The whole diff is **631 lines** (+372 / −259) in 30 files. Of the rest, `registry.ts` is 265, the two projections are 44 (`spec-changes.json` 24, the upgrade guide 20), the widened pin is 29 and the changeset 28. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…-*, package-*, object-*, sharing-*, audit-*, flow-* and http-* migration entries states each lesson in words, not tracker numbers (stage 6) (objectstack-ai#20536) Part of objectstack-ai#20233 Stage 6: the rest-, analytics-, view-, package-, object-, sharing-, audit-, flow- and http- families. Clause-②: no **Stage 6 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. One `surface` moves, under ruling A of the stage-1 ACCEPT (`5858839916`): it carried two tracker numbers. ## What this does `os migrate meta` prints every ADR-0087 semantic entry it crosses as one block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's `reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's runtime-string rule applies to all of it: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (`pnpm check:doc-authoring`): the lesson goes into the text.」 Form **D** of ruling C+D on card `19123` (`5749154545`) sets the shape: the lesson in words, and no number, dead or alive; a cross-repo number is still a tracker number. This stage covers the nine families `rest-`, `analytics-`, `view-`, `package-`, `object-`, `sharing-`, `audit-`, `flow-` and `http-`: **122 sites → 0** in the three prose fields and **2 → 0** in `surface`, across 33 entry files. It also takes the two carry-overs the stage-5 record (`5880299859`) named: - `18.api-error-retry-after-unit-in-key`: the clause on the ~16 runtime-emitted measurements and `ApiError.retryAfter` now dates the ruling that decided it — "its 2026-09-05 population ruling" — instead of reading under ruling B's 2026-09-02 date alone. - `18.inline-grid-column-currency-scale-refused`: the two ruling-record ids and the batch / item numbers are replaced by the rulings' dates and options, the same rewrite stage 5 gave its `field-` sibling. Each site now says what the cited ruling, measurement or fix decided. ADR ids stay, and so does `Prime Directive objectstack-ai#10` in `18.package-manifest-version-grammar-enforced` (a rule in AGENTS.md, as stages 2–5 kept `objectstack-ai#10` / `objectstack-ai#12`). `registry.ts`, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated from the entries (`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`), never hand-edited. The pin now holds twenty-seven families. ## Census — tracker ids in the author-shown fields **Instrument.** The stage-4 / stage-5 TypeScript-AST census, the same script: for each entry object literal under `packages/spec/src/migrations/entries/**` it evaluates `replacement`, `reason`, `acceptanceCriteria` and (separately) `surface`, joining string literals with `+`, and counts `#` followed by 4 or 5 digits at a word boundary. On base `fb386074` it reads the whole tree at **461** sites / 5 `surface` / 50 short, which is the stage-5 record's after-count. Unevaluable fields: 0. 313 semantic entries. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), before and after. - **Dark (comment lines):** 832 `//` / docblock lines in entry files carry a tracker id, and none is counted; 832 before and after. Comment lines are the sibling card's surface (the one that owns every comment and docblock line), and this PR touches none (proved below). **Base `fb386074`:** `rest-` 6 entries, **17** sites (0 / 17 / 0); `view-` 10, **15** (0 / 13 / 2); `analytics-` 6, **14** (2 / 12 / 0); `audit-` 2, **14** (2 / 12 / 0); `sharing-` 2, **14** (1 / 13 / 0); `http-` 2, **13** (0 / 13 / 0); `object-` 7, **13** (1 / 11 / 1); `flow-` 6, **11** (0 / 11 / 0) plus **2** in `surface`; `package-` 6, **11** (0 / 10 / 1). **122** sites (6 / 112 / 4) in 31 of the 47 entries; 91 distinct ids read (82 in this repository, 8 in objectui, 1 `hotcrm#`), plus five ruling-record comment ids. Short numbers in the nine families: 9 (one kept, the Prime Directive). **After this PR:** all nine families **0**, `surface` 0; the seventeen earlier families still 0; whole tree **461 → 339**, `surface` **5 → 3**, short **50 → 40** (the two `inline-` batch numbers included). The PM's rough line count (about 133 sites, about 30 files) is a wider line instrument; the AST reading is 122 in 31 files. | entry | sites (replacement / reason / acceptanceCriteria) | surface | short numbers | record ids | |---|---|---|---|---| | `17.analytics-query-request-envelope-retired` | 1 (0 / 1 / 0) | | | | | `17.audit-log-action-enum-retired` | 4 (0 / 4 / 0) | | | | | `17.audit-log-action-restore-retired` | 10 (2 / 8 / 0) | | | | | `17.flow-retry-max-retries-required` | 1 (0 / 1 / 0) | | | | | `17.http-request-errors-total-retired` | 7 (0 / 7 / 0) | | | | | `17.http-server-runtime-vocabulary-retired` | 6 (0 / 6 / 0) | | | | | `17.package-uninstall-explicit-all-tenants` | 3 (0 / 2 / 1) | | 1 | | | `17.rest-server-openapi31-block-removed` | 2 (0 / 2 / 0) | | | | | `17.sharing-execution-context-retired` | 11 (1 / 10 / 0) | | | | | `17.sharing-rule-recipient-reconcile` | 3 (0 / 3 / 0) | | | | | `17.view-filter-rule-value-shaped-by-operator` | 4 (0 / 3 / 1) | | | | | `17.view-management-protocol-retired` | 3 (0 / 2 / 1) | | | | | `18.analytics-authorable-unknown-keys-refused` | 3 (0 / 3 / 0) | | | | | `18.analytics-date-range-array-two-bounds-required` | 7 (2 / 5 / 0) | | 1 | | | `18.analytics-time-dimension-date-range-vocabulary-closed` | 3 (0 / 3 / 0) | | 1 | | | `18.api-error-retry-after-unit-in-key` | 0 | | | | | `18.flow-decision-branch-expression-absent-refused` | 1 (0 / 1 / 0) | | | | | `18.flow-decision-edge-branching-first-match` | 1 (0 / 1 / 0) | | | | | `18.flow-edge-condition-evaluated-slot-source-required` | 4 (0 / 4 / 0) | 2 | | 1 | | `18.flow-predicate-slot-blank-string-refused` | 4 (0 / 4 / 0) | | | 1 | | `18.inline-grid-column-currency-scale-refused` | 0 | | 2 | 2 | | `18.object-block-sort-item-array` | 3 (0 / 3 / 0) | | 1 | | | `18.object-grid-data-view-data-converged` | 4 (0 / 3 / 1) | | | | | `18.object-grid-default-filters-rule-array` | 2 (0 / 2 / 0) | | | | | `18.object-index-unknown-keys-refused` | 4 (1 / 3 / 0) | | | | | `18.package-api-contracts-unmounted-entries-retired` | 3 (0 / 3 / 0) | | 1 | | | `18.package-install-request-unknown-keys-refused` | 0 | | 1 | 1 | | `18.package-rollback-response-retired` | 5 (0 / 5 / 0) | | | | | `18.rest-api-endpoint-handler-status-retired` | 7 (0 / 7 / 0) | | 1 | | | `18.rest-api-plugin-durations-unit-in-key` | 3 (0 / 3 / 0) | | 1 | | | `18.rest-server-config-dead-keys-retired` | 5 (0 / 5 / 0) | | | | | `18.view-filter-rule-absent-value-refused` | 2 (0 / 2 / 0) | | | | | `18.view-filter-rule-scalar-operator-array-refused` | 4 (0 / 4 / 0) | | | | | `18.view-overlay-options-bag-judged` | 0 | | | | | `18.view-pagination-page-size-default-50` | 2 (0 / 2 / 0) | | | | | **total, 35 entries** | **122 (6 / 112 / 4)** | **2** | **10 (0 kept)** | **5** | The fourteen entries of these families that carried no number are untouched: `analytics-cube-public-default-visible-enforced`, `analytics-query-request-format-retired`, `flow-node-config-required-keys-refused`, `object-grid-default-sort-retired`, `object-kanban-quick-add-retired`, `object-tenancy-organization-field-retired`, `package-manifest-version-grammar-enforced` (its one short number is the kept Prime Directive), `package-version-row-semver-2-0-0`, `rest-api-config-dead-keys-retired`, `rest-api-documentation-version-retired` (the new entry from the landed `rest-` change: its prose was read and is clean), `view-filter-rule-operator-input-canonical`, `view-item-owner-hidden-retired`, `view-overlay-judged-by-viewkind-arm`, `view-overlay-owner-hidden-retired`. Four of the 35 changed entries carried no four- or five-digit id: `view-overlay-options-bag-judged` (a provenance-only acknowledgement quote), `package-install-request-unknown-keys-refused` (a batch number and a ruling-record id) and the two carry-overs. ## Text only — proved by a base-vs-head AST comparison For every entry file this PR changes, both versions (`fb386074` and the head) are parsed and compared: every import declaration; every property other than the three prose fields, by evaluated value (so `id`, `from` / `to` and any matcher); every comment token in the file; and the code skeleton, token by token with each run of joined string literals collapsed to one. `surface` is allowed to differ only where the base value carried a tracker id and the head value carries none. **35 files compared, 0 with a non-prose change**; one note, the ruling-A `surface` of `18.flow-edge-condition-evaluated-slot-source-required`. The instrument is shown able to fail first: on an in-memory copy it reports DETECTED for a mutated `id`, a mutated comment, a mutated `surface` whose base carried no tracker id, a mutated code token and a mutated import, and stays dark on a prose-only mutation. So none of the sibling card's comment lines moved, and no entry's identity or matching moved. `registry.ts`, compared the same way: comment tokens, imports and code skeleton identical; all 1,572 non-prose properties identical by value (container literals compared through their children); exactly the 35 changed ids differ; and all 313 head registry entries equal the head entry files on `surface` / `replacement` / `reason` / `acceptanceCriteria`. ## Every citation read, and what the text now says Each cited id was read with a single-card REST read (body plus the ruling, measurement or landing comments), resolved against the repository its sentence names: 82 in this repository and 8 in `objectstack-ai/objectui` (one bare id resolves there: the sort ruling's consumer change `8758` is "objectui PR" in its sentence). The five ruling-record comment ids were read by id. `hotcrm#1555` answers 403 to this session and is rewritten from what `main` records. Ids are in code spans so this body posts no cross-references. **8 of this repository's ids answer 404** on the issues endpoint and again on the pulls endpoint (`6206`, `6239`, `6511`, `6523`, `10004`, `14369`, `14691`, `17124`; control `6209` answers 200); their sentences are rewritten from what `main` records, listed in Acceptance notes. **`rest-` (14 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `3197` | An audit: several event / subscription / connector webhook enums are schema-only, declared with no runtime consumer. | "the declared-but-unconsumed shape an earlier audit found in the connector webhook and event enums one layer up" | | `4579` | This retirement's own card (`openApi31` declared, never enforced). | trailing id dropped; "(the `openApi31` precedent)" | | `13823` | Maintainer, 2026-09-01: remove `handlerStatus` with a tombstone; enforce excluded; the class direction recorded for two sibling cards. | "maintainer ruling of 2026-09-01 on this key: remove it with a tombstone; enforce excluded"; trailing id dropped | | `5384` | `ApiEndpointSchema` was still an open object after `api` became a registered metadata type; closed strictly. | "the same endpoint vocabulary whose ApiEndpointSchema had already been closed strictly once `api` became a registered metadata type" | | `13808` (PR) | A factual sweep of the automation skill; one corrected sentence taught `handlerStatus` as working machinery. | "this finding came out of correcting that skill sentence, in a factual sweep of the automation skill" | | `3950` (PR) | The precedent that an exported value schema with no consumer reads as a capability, so it leaves with its key. | "an exported value schema with no consumer reads as a capability, so it leaves with its key" | | `13612`, `13613` | The unbound branded identifier schemas; `EventNameSchema`'s binding schemas with no runtime consumer. | "two sibling ADR-0049 findings (the unbound branded identifier schemas and the event-name schema no runtime reads; not ruled by it)" | | `14478`, `15677` | Maintainer ruling B on duration units (2026-09-02), its population widened 2026-09-05; the `api/` stack of that ruling. | the stage-3 wording, naming both dates; trailing ids dropped | | `14369` | **404** — see Acceptance notes. | "The liveness census that enrolled the four `RestServerConfig` sub-objects" | | `11984` | `normalizeConfig` cast the four sub-objects instead of parsing them; it parses them since. | "(which parses them, rather than casting them, since an earlier fix)" | | `14796` | A closed-set sweep of the cloud repository for the 15 dead keys: zero hits. | "A closed-set sweep of the cloud repository at 9b6abe0f2fd5: zero hits" | | `14691` | **404** — this retirement's own card. | trailing id dropped | **`analytics-` (10 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `3891` | The degraded analytics shim (the fallback serving `/analytics/query` with no analytics service installed) dropped the caller's identity and the contract's `where` filter at its door. | "the retired degraded analytics shim (the fallback that answered /analytics/query when no analytics service was installed, and dropped the caller's identity and its `where` filter at the door)" | | `4001` | The unknown-key strictness campaign: silent stripping of undeclared keys ends as the default, schema family by schema family. | "The unknown-key strictness campaign (the sweep that ended silent stripping of undeclared keys as the default, one schema family at a time), its data/ batch" | | `3878` | One URL, two request bodies (the shim's envelope vs the bare query); the envelope dialect was retired. | "strict since the degraded shim's envelope dialect was retired (one URL, one request body)" | | `10414` | `MetricSchema.filters` was authorable with zero consumers; removed. | "removed later in this major, because nothing ever read it: `metric-filters-removed`" | | `17598` | Maintainer ruling A, 2026-09-12, re-affirmed 2026-09-13: the array arm refuses anything but exactly two string bounds. | "Maintainer ruling A of 2026-09-12, re-affirmed 2026-09-13, which tightened the array arm to exactly two string bounds" | | `16322` | The driver half of the closed preset vocabulary; its migration table is the vocabulary entry's, unchanged (single day as the same date twice). | "the shipped migration table for the closed preset vocabulary"; "in a driver change of their own" | | `17593` (PR) | All four analytics faces read the array arm through one rule and refuse the rest with the ADR-0112 envelope. | "since the fix that made them read the array arm one way"; "The fix that followed made all four faces refuse it"; "Since that fix" | | `17124` | **404** — see Acceptance notes. | "a measurement of one authored document on each face found what that bought" | | `16041` | Maintainer, 2026-09-06, option A (contract first): the string arm closes to the declared preset vocabulary. | "Maintainer ruling of 2026-09-06 on the analytics date-range string (option A — contract first)" | | `4614` | The preset list, once three copies (spec, objectui, docs), became one source of truth. | "since the dashboard date filter's three copies of the list were folded into it" | **`view-` (10 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `5869`, `6209` (PR) | A scalar comparand on `in` / `not_in` answered 500; now a named 400 INVALID_FILTER. | "An earlier fix closed the RUNTIME half"; the trailing `(5869)` in `acceptanceCriteria` dropped | | `5685` | The ordering operators' comparand was widened to the strings the platform itself produces (a schema stricter than the runtime was the wrong side). | "an earlier fix already settled the opposite error (the ordering operators' comparand widened to the strings the platform itself produces)" | | `5948` | The issue asking what `GET /ui/view/:object/:type` answers, and its 2026-08-07 ruling, both read `GetViewResponseSchema`. | "The issue asking what `GET /ui/view/:object/:type` answers AND its 2026-08-07 maintainer ruling"; "the shapes that ruling meant" | | `6239` | **404** — this removal's own change (recorded in the client and spec CHANGELOGs). | trailing id dropped | | `19751`, `19514` | The absent-value and scalar-array findings (this and its sibling entry's own cards). | leading ids dropped | | `6227` | `ViewFilterRuleSchema.value` shaped by its operator (the `view-filter-rule-value-shaped-by-operator` entry). | "since the value was first shaped by its operator"; "from then until this change" | | `objectui#9050` | Maintainer ruling C′, 2026-09-20: the protocol is the only refusal set; render time never throws on a protocol-valid document. | "the maintainer's ruling C-prime of 2026-09-20 on objectui's render-time filter converter — the protocol is the only refusal set, so a document it accepts never throws at render time"; the lesson quote 「the differences are the protocol's to close」 kept verbatim | | `objectui#9853` | Maintainer, 2026-09-24: the display default page size is 50, declared once in the protocol; objectui reads the spec default and never hardcodes it. | "the maintainer's ruling of 2026-09-24 set the platform display page size to 50, declared once in the protocol"; "(an earlier ruling on the grid's page size, which the page-size ruling restated)" | | (no id) `view-overlay-options-bag-judged` | Maintainer, 2026-09-24 (objectui's overlay-options card), option A: judge each `options.KIND` at the door. | 「其他同意」 replaced by "the maintainer's ruling of 2026-09-24" | **`package-` (8 ids and one ruling record)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `7705`, `7780` | Uninstall left orphaned rows (repaired); measured there: an org-less uninstall deleted every organization's rows. | "measured at 5 of 5 deleted, including a foreign org's, while uninstall's orphaned-row defect was being repaired"; "exactly as the orphaned-row repair left it" | | `objectstack-ai#12` (short) | Not a tracker id: `rest-requireauth-default-flip` lived in protocol 12. | "(protocol 12)", the spelling four sibling entries use | | `19116` | Maintainer, 2026-09-23, option A: retire the three contract-map entries naming paths nothing mounts. | "Maintainer ruling of 2026-09-23 (option A: retire the three contract-map entries that name paths nothing mounts)" | | `18604` | The measurement on one `HttpDispatcher` over a real `SchemaRegistry`. | the measurement was already in the sentence; the id is dropped | | `18058` | `installPackage` rebound onto the serving `POST /api/v1/packages`. | "rebound by an earlier fix onto the serving POST /api/v1/packages" | | `5856869656` (record) | Maintainer, 2026-09-27, option A: the wrapped install form refuses an unknown top-level key by name. | "the maintainer's ruling of 2026-09-27, option A: the wrapped form refuses an unknown top-level key by name" | | `12038` | Maintainer, 2026-08-27, sub-question 3A: retire the false rollback declaration first, then author the true one. | "Maintainer ruling of 2026-08-27 on the client SDK's unbound response contracts, sub-question 3A: retire this false declaration first, then author the true one"; "the ruling's own survey" | | `11925` | Typed the SDK's un-annotated return values, keeping compile-time guards against a wrong-contract substitution. | "the change that typed the SDK's un-annotated return values left a compile-time guard"; "that negative guard" | | `3877` | Response bodies are never checked against the schemas that declare them. | "the hazard of response bodies never checked against the schemas that declare them, realised in the opposite direction" | **`object-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `objectui#8221` | Maintainer, 2026-09-07, option B: the legacy string `sort` clause retired, one spelling (the array); its fourth item routes the `ComponentPropsMap` pull-back here. | "the maintainer's ruling of 2026-09-07 (option B) retired the legacy string `sort` clause"; "One item of that ruling"; the item's quote kept verbatim | | `8758` (objectui PR) | The consumer half: the string arm dropped from `convertSortToQueryParams`. | "the objectui change that drops the string arm from `convertSortToQueryParams`" | | `7751` | Maintainer, 2026-08-12, direction A: the `object-*` block family's props schemas enter `ComponentPropsMap`. | "a read-point record from the change that brought the `object-*` blocks into `ComponentPropsMap` (the maintainer's ruling of 2026-08-12)" | | `objectui#6207` | Found by objectui's declared-arm parity gate: two spec authorities disagreed on `data`'s kind. Ruled 2026-08-25, option A. | "(contract-vs-contract, found by objectui's declared-arm parity gate)"; "The maintainer's ruling of 2026-08-25 (option A)"; "closes the objectui finding that the two authorities disagreed" | | `objectui#5090` | The grid's registry declaration of `data` was aligned to `ViewData`. | "the authority objectui aligned the grid's registry declaration to" | | `objectui#4648` | Its deprecated-alias carve-out: object-grid's deprecated spellings (`staticData` among them) are not published as authoring surface. | "the deprecated `staticData` shortcut that objectui's deprecated-alias carve-out already refuses to publish as authoring surface" | | `19514`, `objectui#9050` | As in `view-`. | as in `view-` | | `objectui#4772` | The console's index fallback editor converged onto `IndexSchema`, dropping `where`. | "removed when objectui converged that editor onto `IndexSchema`"; "objectui then converged that editor" | | `4001` | As in `analytics-`. | "The unknown-key strictness campaign held this site open" (its internal batch and site numbers dropped) | | `5114` | The console's filter save answered 422: a strict schema refused a key the console itself writes. | "a measured risk, the kind that had already made a console save answer 422 (a strict schema refusing a key the console itself writes)" | **`sharing-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `6206` | **404** — see Acceptance notes. | "completing the maintainer's ruling of 2026-08-07 on the share-link context (enforcement adjudicates on the WHOLE envelope, never a per-site subset)" | | `6430`, `6511` | The share-link enforcement path moved onto the full context under that ruling (`6511` answers 404). | "the share-link twin, which that same ruling moved onto the whole context" | | `6523`, `7068` (PR) | The sharing, approval and report contracts converged onto the full envelope (`6523` answers 404). | "the envelope the sharing, approval and report contracts have declared since they converged onto it"; "One change converged the contracts" | | `7140` (PR), `7206` (PR), `7070` | The four implementations re-annotated (sharing and audit, then approvals and reports); the split that deferred the type's deletion. | "two more re-annotated the four implementations (sharing and audit, then approvals and reports)"; "the deletion that split had deferred" | | `7218` | This retirement's own card. | trailing ids dropped | | `1878` | The metadata property liveness audit: security properties parsed but never enforced. | "The change came out of the metadata property liveness audit, which found security properties parsed but never enforced" | | `6350` | The stock reconciliation of the v17 train's breaking changesets, which backfilled this entry. | "registered late, by the stock reconciliation that compared the breaking changesets already on the v17 release train against this ledger" | **`audit-` (8 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `7675` | Maintainer, 2026-08-12: two halves — build the cheap writers, retire the enum values with no feature; principle recorded 「空 widget + 永远查不到东西的过滤器是可见产品缺陷;审计面宁窄勿谎」 (kept verbatim, it is the lesson). | "Maintainer ruling 2026-08-12 on the audit log's writerless actions"; "that ruling's own survey" | | `8144`, `8145` | The `login` / `logout` writers on the auth session hooks; `config_change` from the settings service. | "(`login` / `logout` on the auth session hooks, `config_change` from the settings service)" | | `8147`, `8315` | The two retirements' own cards. | trailing ids dropped; "(triage 2026-08-13)" kept | | `1883`, `3146` | The undelete / purge permission lifecycle and the soft-delete recycle bin: both open, held, not declined. | "(an undelete / purge permission lifecycle and a soft-delete recycle bin, neither built yet)"; "both held open, not declined" | | `8011` | A credential-storage audit had to re-measure two "hashed at rest" comments; resolved by making the declaration cite its mechanism. | "(the shape a credential-storage audit had to settle by re-measuring two "hashed at rest" comments)" | **`flow-` (11 ids and two ruling records)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4247` | `maxRetries` had two defaults (schema 0, engine 3). | the measurement was already in the sentence; the id is dropped | | `19961` | This refusal's own card. | leading id dropped | | `hotcrm#1555` | **403** — see Acceptance notes. | "a CRM application's lead-conversion flow rendered a refusal screen AND ran the conversion in one execution" | | `17322`, `17495` (in `surface`) | The node slot rebound to the edge door's rule at `registerFlow`, then at `objectstack validate`. | "The node slot joined this entry with the two later changes that rebound AutomationEngine.registerFlow and objectstack validate to the edge door's own rule" | | `15807`, `15430`, `15662` | The evaluated-slot rule: an `ast`-only envelope no engine can evaluate refused; a non-string node predicate refused at registration rather than answered a silent `false`; carried to the edge. | "The evaluated-slot rule, carried to the edge condition — the line that first refused an `ast`-only envelope no engine can evaluate, and refused a non-string node predicate at registration instead of letting the evaluator answer it a silent `false`" | | `5550509137` (record) | 2026-09-05: an `ast`-only envelope driven through `AutomationEngine.evaluateCondition` answers `false`. | "(measured on 2026-09-05 by driving an `ast`-only envelope through `AutomationEngine.evaluateCondition` directly)" | | `17493`, `5651023407` (record) | Maintainer ruling A, 2026-09-13: the two sibling predicate slots refuse a blank string at authoring. | "Maintainer ruling A of 2026-09-13: the two sibling predicate slots refuse a blank string at authoring" | | `15572` | Its pin had held the blank admission correct because parser and evaluator agreed. | "An earlier fix had pinned that admission as correct" | | `17322`, `15811` | The structural `config.condition` and every evaluated `source` refuse a blank. | "after the structural `config.condition` and a blank evaluated `source`" | **`http-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `9650`, `9835`, `9834`, `10004` | The request counter, then the latency histogram, moved to the transport through the response-observing hook (`10004` answers 404). | "(the request counter, then the latency histogram, both through the response-observing hook the transport was given)" | | `5122`, `3977` | The origin cards of the two named sibling entries. | named by those entries' ids, which the sentence already carried | | `9834`, `5295` | This retirement's own cards. | trailing ids dropped | | `4938` | The CONFIG half of `system/http-server.zod.ts` removed. | "The first removed the CONFIG half"; "the config half's removal in this very file" | | `4834`, `4988`, `5055` | The dynamic plugin-loading family, the `ui/` interaction configs, the widget / i18n shapes — each removed by route 3. | "the earlier removals of the dynamic plugin-loading family, the `ui/` interaction configs and the widget / i18n shapes" | **Beyond the four- and five-digit regex.** Nine decision-batch numbers (`objectstack-ai#27`, `objectstack-ai#43`, `objectstack-ai#57`, `objectstack-ai#77`, `objectstack-ai#117`, `objectstack-ai#215`, `objectstack-ai#217`, `objectstack-ai#218`, `objectstack-ai#227`), their item numbers, the campaign's internal batch and site numbers, `batch adjudication batch 4`, 「五问一批」 and "C′ item 1" were dropped; each sentence now carries the ruling's date and content. The provenance-only acknowledgements 「同意」 (×2), 「其他同意」 (×2), 「217 同意」, 「其他接受」 and 「9853 默认页大小改为50」 (the ruling's content, 50, is stated in words; the quote itself carried a tracker number) were replaced by the ruling's date and content. The lesson-bearing quotes are kept verbatim: 「the differences are the protocol's to close」 (×2), the sort ruling's fourth item, 「every other operator takes a scalar」, 「lowers to a deep-equality comparand」, 「`persistViewPatch` 只存 patch,不存 merged base」 and the audit ruling's 原则记录. ## Pin — widened, not weakened `packages/cli/test/migrate-meta-engine-guidance.test.ts`: `COVERED_PREFIXES` 17 → **27** (`rest-`, `analytics-`, `view-`, `package-`, `object-`, `sharing-`, `audit-`, `flow-`, `http-`, and `inline-` for the carry-over entry, the `inline-` family's only entry, now tracker-free); `package-` selects no `packages-` entry. `REWRITTEN` 113 → **147**: the 34 ids this stage rewrote for the first time (the 33 nine-family entries plus `inline-grid-column-currency-scale-refused`; `api-error-retry-after-unit-in-key` was already listed). The header comment names the new families; the three `it` blocks and every `expect` are textually unchanged. **Ablation, from committed HEAD `472ee29b82`, one lock turn** (`scripts/ablation-replace.mjs` wrap mode, a restore trap by absolute path with a blob check, `scripts/ablation-dist-preflight.mjs`): `registry.ts`, `http-server-runtime-vocabulary-retired`'s `reason`, anchor `behind it, vocabulary second. ADR-0049.` → `behind it, vocabulary second. ADR-0049, objectstack-ai#5295.` (anchor 1 → 0, replacement 0 → 1, blob `06c06741` → `702e7370`). Mutate leg: spec build exit 0; the marker in 4 `dist` files; the pin **RED**, 1 failed | 2 passed: `http-server-runtime-vocabulary-retired: the printed guidance cites a tracker id: expected 'objectstack-ai#5295' to be undefined`. Restore proven by the tool (blob == HEAD `06c06741`, `git diff HEAD` empty). Restore leg: spec build exit 0; the marker absent from all 224 `dist` files with the tree clean; the pin **GREEN**, 3 passed; whole tree 0 dirty paths. No other test pins a removed number: on `main`, the tests naming any of the 35 entry ids (`sys-audit-log-retired-actions`, `plugin-rest-api.handler-status-retirement`, `rest-api-config-dead-keys-retirement`, `inline-grid-column-currency-scale-refused`, `component-object-grid-default-filters.pin`, `view-overlay-owner-hidden-retirement`, and two that name them in passing) assert ids, surfaces and prescriptions this PR does not move, not the prose it rewrites. ## Generated artifacts - `registry.ts`: 313 semantic, 232 retired-key, 206 retired-def; exactly the 35 ids differ (see the AST comparison above). - `spec-changes.json` and `docs/protocol-upgrade-guide.md`: only the protocol-17 entries appear in them, as the generators project; every changed line is a fragment of a changed entry's field. - `.changeset/20233-rest-analytics-view-package-object-sharing-audit-flow-http-migration-guidance-tracker-free.md`: `'@objectstack/spec': patch`, `Clause-②: no`. ## Verification (head `472ee29b82`) Every heavy run through `scripts/pm/os-verify-lock.sh`, each exit code written to disk before it was read. - **Build:** `turbo run build --concurrency=2 --filter='@objectstack/cli^...'` → Tasks: 58 successful, 58 total (VERDICT command-exit 0); plus the 10 packages outside that closure for the dual-build gate → Tasks: 68 successful, 68 total. - **Pin + neighbour:** `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts test/migrate-meta-default-range.test.ts` → Test Files 2 passed (2), Tests 10 passed | 1 skipped (the default-range file's own `skipIf`). - **CLI unit (the tests that read the registry or `spec-changes.json`):** `spec-release-changes`, `meta.stored-flags`, `doctor-deprecation-hint-commands`, `vitest-tiers-partition` → Test Files 4 passed (4), Tests 48 passed (48). - **Spec:** `--project local` → Test Files 573 passed (573), Tests 16835 passed | 1 todo; `--project repo` → Test Files 39 passed (39), Tests 701 passed (701); `src/migrations/migrations.test.ts` alone → Tests 150 passed (150). - **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exit 0 (test layer 53 files / 251 errors held); `pnpm --filter @objectstack/cli typecheck` exit 0 (3 files / 28 errors held). - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 89 families over this diff; all 89 run, all exit 0; `--ran` → "89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN". Among them: `check:doc-authoring` ("16759 customer-facing string(s) across 1172 spec sources clean"), `check:generated` ("All 15 generated artifacts are up to date"), `check:migration-registry` ("registry.ts is current (313 semantic, 232 retired-key, 206 retired-def)"), `check:spec-changes`, `check:upgrade-guide`, `check:issue-citations` ("no issue citations added"), `check:org-identifier`, `check:nul-bytes`, `check:api-surface`, `check:authorable-surface`, `check:dual-build-cjs-loads` (104 require entry points across 66 packages load), `check:type-check-debt`, `check:adr-0087-registration`, `check:changeset-no-major`, `check:empty-changeset`. - **Lint, a proven narrowing:** `eslint --no-inline-config --format json` over the 37 changed `.ts` files → 37 files, 0 errors, 0 warnings, no file-ignored notice. Population read from `eslint.config.mjs` (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`); invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move any untouched file's verdict. The full `pnpm lint` is CI's. - **Mergeability:** `origin/main` is `1378ec7c`, three commits past the base, none touching the migration ledger or its projections; a driver-free bare-clone `merge-tree --write-tree` of the head against it exits 0 with no conflicted path. `registry.ts` is shared with open PRs `20504`, `20460` and `20458`, ordinary concurrency; no open PR touches any of the 35 entry files or the pin. ## Acceptance notes **404 and 403 ids, rewritten from what `main` records.** - `6206` (the share-link ruling of 2026-08-07): `packages/core/src/security/assemble-execution-context.ts` (the share-link copies omitted `accessible_org_ids`; both surfaces converted to pass the whole envelope) and `packages/plugins/plugin-approvals/CHANGELOG.md` ("applying the ... ruling — enforcement adjudicates on the whole envelope, never a per-site subset"). - `6523` / `6511`: the same CHANGELOG ("converged 36 contract signatures onto the complete `resolveAuthzContext` envelope"); `6430` (200) names the share-link half. - `6239`: `packages/client/CHANGELOG.md` records it as this removal itself (retire `ViewProtocol`'s five viewId-addressed methods); dropped. - `8758` as a bare id: its sentence names objectui, and `objectui#8758` answers 200 (the string `sort` clause retired). - `10004`: `packages/observability/src/semconv.ts` and the observability CHANGELOG pair it with `9834` as the histogram's move to the transport seam. - `14369`: `docs/qa/platform-checklist/areas/api-backend.json` ("the liveness census that found the block read by nothing") and the `14640` changeset (it enrolled four of the five sub-objects). `14691`: the same file records it as this retirement; dropped. - `17124`: `.changeset/17124-daterange-array-arm-arity.md` records the measurement (one authored document, four faces, three readings). - `hotcrm#1555` (403 cross-repo): `.changeset/15429-decision-edge-branching-first-match.md` records the case and the node (`lead_conversion.decision_duplicate`). **Observations, not filed.** - Carrier: this card's later stages · 339 prose-field sites, 40 short numbers and 3 `surface` sites remain in the other families (the largest: `actor-` 13, `hot-` 12, `external-` 11, `query-` 11, `delete-` 10, `stack-` 10); `stack-` and `turso-` have entries in open PRs. The pin file keeps its stage-1 name while holding twenty-seven families. - Carrier: the sibling card for comment and docblock lines · 832 comment lines in entry files still cite tracker ids (unchanged), including the header comments of `18.view-pagination-page-size-default-50.ts`, `18.view-overlay-options-bag-judged.ts`, `18.flow-decision-edge-branching-first-match.ts` (`hotcrm#1555`) and `18.analytics-authorable-unknown-keys-refused.ts`. Implemented-by: `claude/issue-20233-migrate-meta-tracker-free-stage-6` · `domain:spec` seat 4 dispatch, session `session_01ARcDurZ5j34RdqsGgc4jgH`. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Plugins need a standard way to extend core metadata (Objects, Fields) with custom properties without modifying base schemas.
Changes
Extension Protocol (
src/system/extension.zod.ts)plugin_id.property_name)get,set,has,remove)Schema Integration
extensionsfield toFieldSchemaandObjectSchemaAI Extensions (Reference Implementation)
Usage
Documentation
content/docs/guides/metadata-extensions.mdxexamples/ai-customer-service/(complete AI-powered support system)Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.