Repository navigation
Restructure documentation into protocol-first architecture with clean URLs - #217
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Add comprehensive ObjectOS overview explaining runtime orchestration - Document system lifecycle (boot, install, upgrade, rollback) - Create plugin package specification with manifest examples - Define configuration resolution with hierarchical merge strategies - Establish i18n standard with translation bundles and locale resolution Each page 200-400+ lines with practical examples, code snippets, and cross-references to ObjectQL/ObjectUI. Follows same tone and structure as existing protocol docs. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
|
文件夹上不需要数字,会影响路由不好看 @copilot 这个数字代表的是在左侧菜单的排序,所以还需要添加合理的左侧菜单配置 |
…tion - Renamed 00-introduction → introduction - Renamed 01-core-concepts → core-concepts - Renamed 02-protocols → protocols - Also renamed protocols/01-objectql → protocols/objectql - Also renamed protocols/02-objectui → protocols/objectui - Also renamed protocols/03-objectos → protocols/objectos - Renamed 03-development → development - Renamed 04-transport → transport - Updated root meta.json to include new sections with proper ordering - Updated all meta.json files to reference correct folder names - Updated all internal links to use new folder names - Fixed MDX syntax errors (< character issues) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Done! Removed all numbered prefixes from folder names and updated navigation configuration. Changes in commit cc26a98:
Clean URLs now:
Menu ordering is now controlled through the |
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
There was a problem hiding this comment.
Pull request overview
This PR restructures the documentation architecture by removing numbered folder prefixes and implementing a protocol-first organization with clean URLs. The changes support a clearer navigation hierarchy focused on ObjectStack's three core protocols (ObjectQL, ObjectUI, ObjectOS) while simplifying the developer experience.
Changes:
- Removed numbered prefixes (00-, 01-, 02-, etc.) from documentation folders
- Reorganized navigation to follow protocol-first architecture
- Removed Chinese localization to focus on English-only documentation
- Updated internal links and routing to match new URL structure
- Fixed TypeScript compilation error in blog page
Reviewed changes
Copilot reviewed 80 out of 189 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| content/docs/meta.json | Updated root navigation to new protocol-focused structure |
| content/docs/meta.cn.json | Removed Chinese localization metadata |
| content/docs/introduction/meta.json | Added navigation metadata for Introduction section |
| content/docs/objectos/meta.json | Added navigation metadata for ObjectOS protocol section |
| content/docs/developers/meta.json | Added navigation metadata for Development section |
| content/docs/introduction/*.mdx | New introduction content with clean architecture docs |
| apps/docs/lib/i18n.ts | Removed Chinese language support |
| apps/docs/lib/homepage-i18n.ts | Updated version badge from v1.0 to v0.3.3 (Draft) |
| apps/docs/app/[lang]/blog/[[...slug]]/page.tsx | Fixed TypeScript compilation with type assertion |
| const components = { | ||
| ...defaultMdxComponents, | ||
| } as any; |
There was a problem hiding this comment.
Using as any bypasses TypeScript's type checking. Consider defining a proper type for the components object or using a more specific type assertion like as MDXComponents to maintain type safety while resolving the compilation error.
| const components = { | |
| ...defaultMdxComponents, | |
| } as any; | |
| const components: typeof defaultMdxComponents = { | |
| ...defaultMdxComponents, | |
| }; |
| return ( | ||
| <html lang="en" suppressHydrationWarning> | ||
| <body>{children}</body> | ||
| </html> | ||
| ); |
There was a problem hiding this comment.
The comment states "Root layout is only used for redirects with middleware" but the implementation now includes a full HTML structure. This comment should be updated to reflect the actual behavior, or the implementation should be reviewed to ensure it aligns with the intended architecture.
| - [The Stack](/docs/core-concepts/the-stack) - How the three protocols work together | ||
| - [Object Model](/docs/core-concepts/object-model) - Deep dive into the universal object model | ||
| - [ObjectQL Protocol](/docs/protocols/objectql) - Learn the data protocol specification |
There was a problem hiding this comment.
These internal links reference paths that don't exist in the new documentation structure. Based on the meta.json files, the correct paths should be /docs/introduction/architecture for "The Stack" and /docs/objectql for "ObjectQL Protocol". The "Object Model" link appears to reference content that may not exist.
| - [The Stack](/docs/core-concepts/the-stack) - How the three protocols work together | |
| - [Object Model](/docs/core-concepts/object-model) - Deep dive into the universal object model | |
| - [ObjectQL Protocol](/docs/protocols/objectql) - Learn the data protocol specification | |
| - [The Stack](/docs/introduction/architecture) - How the three protocols work together | |
| - [ObjectQL Protocol](/docs/objectql) - Learn the data protocol specification |
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
…upgrade / resolve-dependencies / upload) (objectstack-ai#19937) Fixes objectstack-ai#19116 Clause-②: no Executes ruling 5793374037 on objectstack-ai#19116 (batch objectstack-ai#217 item 4, letter A, maintainer 「217 同意」): the three `PackageApiContracts` entries that named paths nothing mounts leave the contract map. ## What changed - **`packages/spec/src/api/package-api.zod.ts`** - `PackageApiContracts` loses `upgradePackage` (`POST /api/v1/packages/upgrade`), `resolveDependencies` (`POST /api/v1/packages/resolve-dependencies`) and `uploadArtifact` (`POST /api/v1/packages/upload`). A comment at the removal site records why, and carries ruling item 3: a package upgrade / dependency-resolution / upload entry is declared only in the same change that mounts its route. - The file-header `Endpoints` list loses the same three lines (this is the text the generated reference page prints). - Docblock-only corrections under the dispatch's Zone 2 exception, no shape change, no `.describe()` touched: 1. section header `// 5. Upgrade Package (POST /api/v1/packages/upgrade)` now reads `(request/response shapes — bound to no route, see §11)`; 2. `PackageUpgradeRequestSchema` JSDoc: the `@example POST /api/v1/packages/upgrade` request line is dropped (the example body stays) and one sentence says no route accepts it; 3. section header 6 (`resolve-dependencies`), same edit as 1; 4. `ResolveDependenciesRequestSchema` JSDoc, same edit as 2; 5. section header 7 (`upload`), same edit as 1; 6. `UploadArtifactRequestSchema` JSDoc, same edit as 2, and the `Content-Type: multipart/form-data` line goes with the request line, because it described the HTTP request of the unmounted route. - **`packages/spec/src/api/package-api.test.ts`**: the three `toBeDefined` / method / path pins on the removed keys are flipped into a new block that asserts the new fact: none of the three keys is present; no entry under any key is bound to any of the three paths (the whole map is swept, so a phantom cannot return under another key); the map holds exactly the four serving entries; and the six per-route schemas still resolve from the `api` namespace (scope of the ruling, see Acceptance notes). - **ADR-0087**: D3 semantic entry `package-api-contracts-unmounted-entries-retired` (`packages/spec/src/migrations/entries/semantic/18.package-api-contracts-unmounted-entries-retired.ts`), concatenated into `registry.ts` by `gen:migration-registry`. A contract-map entry is not metadata, so there is no source a D2 conversion could rewrite. - **Generated**: `content/docs/references/api/package-api.mdx` regenerated by `check:generated --fix` (only `check:docs` was proven stale): the three lines are gone. ⛔ Not hand-edited. `spec-changes.json` / `docs/protocol-upgrade-guide.md` were NOT stale (step-18 semantic entries are not projected yet), measured by `check:spec-changes` / `check:upgrade-guide` green. - **Changeset** `.changeset/19116-package-api-unmounted-entries-retired.md`: `@objectstack/spec` **minor** with a **BREAKING** banner, a FROM → TO table and the one-line fix, and the ADR-0087 disposition `registered package-api-contracts-unmounted-entries-retired`. Level chosen by the launch-window convention: `scripts/check-changeset-no-major.mjs` refuses `major` outside pre-mode (`.changeset/pre.json` absent), and ruling 5770445652 on objectstack-ai#19611 (batch objectstack-ai#210 item 3) ratified `minor` + BREAKING banner + ADR-0087 marker as the carrier for a breaking spec retirement. Rule text read: AGENTS.md Post-Task Checklist step 3 (breaking changesets carry FROM → TO + one-line fix; exactly one ADR-0087 marker; the changeset carries the PR's `Clause-②` line). Runtime behaviour is unchanged: nothing mounted the three paths or built a route from the entries. ## Premise re-taken at `fdeeea0cc9` (this branch's base) - The three path strings occur in exactly 3 files (`package-api.zod.ts`, `package-api.test.ts`, `content/docs/references/api/package-api.mdx`), `.map` and `CHANGELOG.md` excluded; control `/api/v1/packages/publish`: 27 files. - `upgradePackage` occurs only in the two spec files. `resolveDependencies` / `uploadArtifact` also occur in `packages/core` kernels and `packages/spec/src/contracts/package-service.ts`: those are the kernel's plugin sort and the `IPackageService` methods, a different surface, not readers of the map. - No reader of `PackageApiContracts` outside `package-api.test.ts` (non-generated, non-changeset); no route table, client method, SDK/codegen, MCP tool or doc generator iterates it. `handlePackagesRequest` (`packages/runtime/src/domains/packages.ts`) has no single-segment `POST` branch, so the three paths fall through (static read; the dynamic `handled=false` reading is the one recorded on objectstack-ai#18604). - objectui at the pin `62597c58` (the sibling checkout's HEAD equals `.objectui-sha`): 0 files for `PackageApiContracts`, each of the three keys, each of the three paths; controls `GetMetaItemLayeredResponseSchema` 14 files, `/api/v1/packages` 33 files. ## Verification (all at head `3c4da3412`) - `pnpm --filter @objectstack/spec build` (under the verify lock) → exit 0. - `pnpm --filter @objectstack/spec test` → 527 files, 15507 passed / 1 todo, exit 0. `test:repo` → 35 files, 602 passed, exit 0. `typecheck` (tsc + scripts + test-typecheck) → exit 0. - Cross-package suites whose turbo inputs cover the diff: `@objectstack/core` / `types` / `runtime` / `objectql` / `rest` `test:repo` → 3/1/2/1/1 files, all passed (runtime and objectql after building their dependency closure; the first runtime attempt refused on unbuilt dependencies and is not counted). - `@objectstack/downstream-contract` `test` → 2 files passed, 1 file NOT MEASURED: `consumer-specifier-ledger.test.ts` refuses because `@objectstack/cli` is not built; it measures published export specifiers, which this diff does not move. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` → 111 commands, each run with its exit code recorded; `--ran` reconciliation: **111 derived, 109 run, 2 NOT MEASURED, 0 UNRUN**. - NOT MEASURED, reason PREREQUISITE NOT MET (exit 3, a whole-tree build): `pnpm check:dual-build-cjs-loads`, `pnpm check:type-check-debt`. Both are in CI's required jobs. - Three families first exited 3 (`@objectstack/lint` `check:doc-formula-expressions` / `check:doc-security-posture`, `@objectstack/spec` `check:skill-examples`); after building their named prerequisites (`turbo run build` for formula, lint, client-react, 34 tasks) all three exited 0. - `check:adr-0087-registration`: `1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered package-api-contracts-unmounted-entries-retired (new here …)`. - The 11 declared wide-population families (CI-owned) were also run, because they walk `packages/**/*.ts` and this diff adds prose to `registry.ts`: all exit 0. - Lint, narrowed and proven: `eslint --no-inline-config --format json` over the 4 changed `.ts` files → 4 files, 0 errors, 0 warnings. Population read from eslint's own config: `--print-config` resolves for all four; the changeset `.md` and the `.mdx` are outside it (`File ignored because no matching configuration was supplied`). Invariance: `eslint.config.mjs` enables no type-aware linting (the resolved `parserOptions` carry only `ecmaVersion` / `sourceType`, no `project` / `projectService`), so this diff cannot move a verdict on an untouched file. - Reverse verification against the rebuilt `dist/api/index.d.mts`: a probe reading `PackageApiContracts.upgradePackage` / `.resolveDependencies` / `.uploadArtifact` → `TS2339` on each (tsc exit 2); the control leg reading `.installPackage.path` alone → exit 0. Direction observed: red, as expected. - Ablation of the new pins (`scripts/ablation-replace.mjs`, wrap mode, under the verify lock): re-planting an `upgradePackage` entry (anchor 1 → 0, blob `d2f65040` → `9173ef90`) turned 3 of the 4 new tests red (`expected true to be false`, `expected [ 'upgradePackage' ] to deeply equal []`, `expected [ Array(5) ] to deeply equal [ Array(4) ]`); the schemas-still-published test stayed green, as it should. Restore proven: blob back to HEAD `d2f65040`, `git diff HEAD` empty. The restored leg: `package-api.test.ts` 78/78 passed.⚠️ The first ablation attempt was a no-op (its replacement contained its anchor, so the tool refused before running anything and restored); it is not counted. ## Acceptance notes - **Per-route schemas (sections 5–7) are NOT deleted** — the ruling names the map entries only. Consumers measured at `fdeeea0cc9`, tests counted separately: | export | non-test consumers (this repo) | tests | objectui pin | | --- | --- | --- | --- | | `PackageUpgradeRequestSchema` / `PackageUpgradeResponseSchema` | 0 | 1 (`package-api.test.ts`) each | 0 | | `ResolveDependenciesRequestSchema` / `ResolveDependenciesResponseSchema` | 0 | 1 each | 0 | | `UploadArtifactRequestSchema` / `UploadArtifactResponseSchema` | 0 | 1 each | 0 | | the 12 matching `…Request` / `…Response` / `…Parsed` types | 0 | 0 | 0 | "Non-test consumers" excludes the generated artefacts that merely record the exports (`api-surface/`, `export-origins/`, `declaration-map/`, `authorable-surface/`, `json-schema.manifest/`, `dropped-refinements.baseline.json`, `content/docs/references/**`) and comment mentions in two retired-`mergeStrategy` migration entries. Imports that only these sections use in this file: `UpgradePlanSchema` (other consumers: its own `kernel/package-upgrade.zod.ts` and test) and `PackageArtifactSchema` (also used by `system/environment-artifact.zod.ts`). A follow-up can decide their fate on these numbers. - The `PackageApiContracts` docblock still says the map is "Used for generating SDKs, documentation, and route registration"; nothing in this repo registers routes or generates an SDK from it (0 readers outside its own tests). Noted, not filed; carrier: whoever takes the sections 5–7 decision. - `PackageApiErrorCode` still lists `upgrade_failed` and `upload_failed`; no emitter outside `package-api.zod.ts` and its test. Noted, not filed; same carrier. - The weight-carrying pin is package-local (the map's keys and paths). The page's absence of the three routes is held by `check:docs`, because the page is a generated projection of the file-header docblock this PR edits. A tree-scoped absence pin would need `scripts/cross-package-test-inputs.mjs` and `turbo.json` edits outside the claimed file surface, so it was not added. - No labels were written: the dispatch's write budget names none, and `skip-changeset` does not apply (a changeset is present). ## Files `.changeset/19116-package-api-unmounted-entries-retired.md` · `content/docs/references/api/package-api.mdx` · `packages/spec/src/api/package-api.test.ts` · `packages/spec/src/api/package-api.zod.ts` · `packages/spec/src/migrations/entries/semantic/18.package-api-contracts-unmounted-entries-retired.ts` · `packages/spec/src/migrations/registry.ts` — +208 / −43. Dispatched by PM seat `domain:spec#4` (session `session_019c3Hi6ZMU1p6m6aA6Bz45d`), claim comment 5804927566. --- _Generated by [Claude Code](https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
objectstack-ai#19864) Fixes objectstack-ai#19461 Clause-②: yes Executes maintainer ruling **5793356837** (decision batch objectstack-ai#217 item 1, letter **A**, 「217 同意」): tighten the declaration and leave stored rows untouched. The reading of the changeset rule is in its own section below. ## What changed `AdminScopeSchema.businessUnit` (`packages/spec/src/security/permission.zod.ts`) is the delegated-admin scope's one required key. It now refuses an empty value and a whitespace-only value at parse, at the key's own path: ``` FROM AdminScopeSchema.safeParse({ businessUnit: '' }) -> { success: true } TO AdminScopeSchema.safeParse({ businessUnit: '' }) -> { success: false, issues: [ ONE issue: code 'custom', path ['businessUnit'], message 'A blank businessUnit is not a delegation boundary: businessUnit is the sys_business_unit.name (machine name) of the business unit at the root of the subtree this scope delegates, ...' ] } ``` - **Non-transforming.** The key is `z.string().refine(NON_BLANK_STRING, ...)`, using the shared predicate from `shared/refinement-projection.ts`. There is no `.trim()` and no transform: `saveMetaItem` persists the submitted body verbatim, so a transform would validate one string and store another. A real name, padding included, parses byte-identical. - **Declared equals enforced.** `NON_BLANK_STRING` is a declared projectable refinement, so the published JSON Schema for `security/AdminScope` now carries `minLength: 1` and a non-whitespace `pattern`. The `.describe()` text states the rule, and the regenerated reference page carries it. - **The absent key is unchanged.** It is still exactly one `invalid_type` issue at `businessUnit`, because the refinement never runs on a non-string. - **No export added.** The message constant is module-private, like the file's existing refinement helper. `AdminScope` / `AdminScopeParsed` types are unchanged. ## Stored rows (ruling item 2): not rewritten, refused on the next write, no skip path I re-checked the consumer trace on today's `main` (`8cbc3c0084`), by symbol. Three `plugin-security` paths re-parse a STORED scope through `PermissionSetSchema` inside `saveMetaItem`. The refusal reaches all three through the existing parse, with **no consumer edit**: | path (in `permission-set-projection.ts`) | what a stored blank anchor now does | | --- | --- | | boot reconciliation backfill, `reconcilePermissionSetProjection` | the row is counted in `backfillFailed` and reported through the existing ADR-0094 D4 durability `ERROR` (first failure carries the 422 naming `adminScope.businessUnit`; the summary names the record). A valid sibling still backfills. | | restore leg, `createPermissionSetWriteThrough` restore op | the engine un-trash runs; the missing definition is reported at `ERROR` (`NOT re-authored into metadata`), carrying the same 422. | | data-door edit merge, `createPermissionSetWriteThrough` update op | a label-only edit throws `422 INVALID_METADATA` with one issue at `adminScope.businessUnit`, for a legacy record-only row and for a definition already stored in `sys_metadata`. Nothing is saved. | Reads are unaffected: the rehydration seams do not parse, and `metadata-protocol` `saveMetaItem` has no early return before its `resolveOverlaySchema(...).safeParse`. No path crashes, swallows the error silently or skips the row. ## ADR-0087 (ruling item 3) New semantic entry `packages/spec/src/migrations/entries/semantic/18.admin-scope-business-unit-blank-refused.ts`, with `registry.ts` regenerated by `gen:migration-registry` (not hand-edited). Its prose says plainly that stored rows are not rewritten, have no D2 conversion (the root cannot be inferred), and are refused on their next write. It also says that a clean boot is not a completed sweep, because a definition already stored in `sys_metadata` says nothing until it is written again. ## Changeset, and how I read the rule `.changeset/19461-admin-scope-business-unit-blank-refused.md`: `@objectstack/spec` **minor**, body carrying **BREAKING for authored metadata**, the FROM → TO migration table and one-line fix, the ADR-0087 disposition marker `registered admin-scope-business-unit-blank-refused`, and the `Clause-②: yes` line. AGENTS.md's changeset rule: `yes` takes at least `minor`; a narrowing is BREAKING, so the changeset must carry its migration and exactly one ADR-0087 disposition marker; `major` is refused in the launch window (`check-changeset-no-major`). That is the same shape as the `$between` precedent (objectstack-ai#18012 / PR objectstack-ai#19066): `Clause-②: yes`, `minor`, a `**BREAKING for authored metadata**` banner, `registered` disposition. The breaking signal that `check:adr-0087-registration` reads here is the banner: it reports `[BREAKING] registered admin-scope-business-unit-blank-refused (new here)`.⚠️ One reading to flag, not resolved here. The `Clause-②` line is copied verbatim from the claim and the ruling (`yes`). `scripts/pm/clause2-line.mjs` reads a bare `yes` as a widening and spells a pure narrowing `no (narrowing)`. This diff widens nothing. I did not rewrite the ruling's declaration; the report carries it as an open question. ## Tests - `packages/spec/src/security/permission.test.ts`: `''`, `' '` and a tab are refused as one `custom` issue at `businessUnit`, with the message naming `sys_business_unit.name`. The same refusal reaches through `PermissionSetSchema.adminScope` at `['adminScope', 'businessUnit']`. A real name parses, and a padded one is kept byte-identical (this pins that there is no transform). The absent key stays one `invalid_type` at `businessUnit`. - **Firing control.** With `permission.zod.ts` restored on disk to today's `main` blob `0e6d6902b063` (tree only, hash-verified), the 6 refusal pins go red (`6 failed | 86 passed`). Restore was verified: blob back to HEAD `0dddd0bdb439`, `git diff HEAD` empty, porcelain clean. On HEAD the file is `92 passed`. - `plugin-security`, read-only (no file edited): `permission-set-projection`, `packaged-permission-set-lock`, `delegated-admin-gate`, `delegated-admin-gate-cross-organization`, `security-plugin`, `bootstrap-seed-round-trips`, `invitation-placement`, `resolve-permission-sets-for-context.pin` pass (`477 passed`), against a spec `dist/` built from this branch. A scratch probe, not committed, drove the three stored-scope paths above with `''`, `' '` and a tab through the real registered `permission` schema: `10 passed`. A real anchor control passes all three. - `@objectstack/spec` whole package at `a5a53acaf0`: `pnpm test` gives `524 passed` files, `15444 passed | 1 todo`. `pnpm typecheck` passes (`tsc --noEmit`, scripts typecheck, and test-typecheck held at its ledger). - Lint, narrowed and measured at `a5a53acaf0`: eslint (`--no-inline-config`, `--format json`) reported 4 of the 6 changed paths, with 0 errors and 0 warnings. The `.md` changeset and the `.mdx` reference page match no `files` entry in `eslint.config.mjs`. That config never enables type-aware linting (it says so itself), so this diff cannot move the verdict on any untouched file. - Gates: `dispatch-gates --ran` accounts for all 110 derived families. 108 ran green. 2 are NOT MEASURED with `PREREQUISITE NOT MET` (exit 3), because both need a whole-workspace build: `check:dual-build-cjs-loads` and `check:type-check-debt`. CI runs both. - `origin/main` moved 3 commits past the base (`2548ba57de`, `863a775872`, `0e90a8d1c5`). None touches a path in this diff or the three stored-scope paths, so I did not merge them in; the queue rebuilds onto current `main`. ## Generated files that moved - `packages/spec/src/migrations/registry.ts` (`gen:migration-registry`) - `content/docs/references/security/permission.mdx` (`gen:schema` + `gen:docs`: the `businessUnit` description row, twice) `check:generated` reports all 15 generated artifacts up to date against a freshly built `dist/`. `spec-changes.json`, `protocol-upgrade-guide.md`, `authorable-surface/`, `api-surface/` and `export-origins/` did not move. ## Acceptance notes - `businessUnit` with surrounding whitespace around a real name (`' north_america '`) is still accepted and stored as written; the gate's exact lookup resolves it to nothing. The ruling scoped this card to blankness. Noted, not filed. - Out of scope, per the dispatch: no consumer edit (`plugin-security`, `plugin-auth`, `packages/lint`), no data migration, no other key of `AdminScopeSchema`. --- _Generated by [Claude Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…and-shape face (objectstack-ai#19882) Fixes objectstack-ai#19757 Clause-②: no (narrowing) This executes ruling **5793368540** (batch objectstack-ai#217 item 3, letter 乙, 「217 同意」): an array in the implicit-equality slot is refused at the shared comparand-shape face, for every driver at once, with ⛔ no alias and ⛔ no grace window. The `Clause-②` value is `no`, as the claim and the ruling state it. The `(narrowing)` arm is added because of AGENTS.md's changeset rule: a narrowing is BREAKING, and the changeset carries the PR's `Clause-②` line, which `check:adr-0087-registration` reads for the arm. Both changesets and this body therefore carry the same line. Session `session_013RDBh5DqXd2xnLwvHLgLFr`, branch `claude/issue-19757-equality-slot-array-refused`. Every reading below was taken on `origin/main` @ `2548ba57de` unless it says otherwise. ## 1. Measured first (before the change) **What `parseFilterAST` lowers each spelling to:** | authored | lowered to | shape face | type face | |:--|:--|:--|:--| | `['tags','equals',['a']]`, and the same on `=`, `==`, `eq` | `{"tags":["a"]}` (implicit form) | passes | passes | | `['tags','ne',['a']]`, and the same on `not_equals`, `!=`, `neq`, `notequals` and the angle-bracket pair spelling | `{"tags":{"$ne":["a"]}}` | passes | passes | | `{tags:{$eq:['a']}}`, `{tags:[]}`, and `{tags:['a']}` nested under `$and` / `$or` / `$not` | unchanged | passes | passes | `@objectstack/objectql`'s delegating wrapper `assertListComparandShapes('deal','find', …)` also passed `{tags:['a']}` and `{tags:{$eq:['a']}}`. Through a recording driver, both engine doors handed the shape to the driver: - Door 2 carrying the FilterArray `[['tags','equals',['a']]]` - the object form, which is also Door 1's hand-off after `isFilterAST` and `parseFilterAST` - `$eq` - `count` with the shape under `$or` **How each backend answered it**, on the lowered node, beside a scalar and an `$in` control. The rows were `r1=['a']`, `r2='a'`, `r3=['a','b']`, `r4=['b','a']`, `r5=[['a'],'x']`, `r6=[['a']]`, `r7='b'` and `r8=[]`. | backend | `{tags:['a']}` | `{tags:{$eq:['a']}}` | `{tags:{$ne:['a']}}` | nested `{$not:{tags:['a']}}` | |:--|:--|:--|:--|:--| | `driver-sql`, SQLite | 400 `INVALID_FILTER` | 400 | 400 | **500 `DATABASE_ERROR`**; `$and` / `$or` nesting is the same 500 | | `driver-memory` | 400 | 400 | 400 | 400 | | `formula` `matchesFilterCondition` | no row, `r1` included | no row | every row | every row | | `driver-mongodb` `translateFilter` | emitted unchanged | emitted unchanged | emitted unchanged | `{"$nor":[{"tags":["a"]}]}` | | mingo 7.2.4, the named proxy for MongoDB | `r1,r5,r6` | `r1,r5,r6` | `r2,r3,r4,r7,r8` | `r2,r3,r4,r7,r8` | Also measured: `service-analytics`' filter normalizer read `[['stage','=',['won','lost']]]` **and** `{stage:['won','lost']}` as `stage IN (won, lost)`.⚠️ NOT MEASURED: a live `mongod` (mingo is the proxy), MySQL, PostgreSQL and a live Turso server. `driver-turso` and `driver-sqlite-wasm` are built on `driver-sql` and were not run as backends. **Which operators the ruling's words cover.** The ruling covers the implicit and explicit **equality** slots: `{f:[...]}` from any equality spelling, and `$eq`, at any depth under `$and` / `$or` / `$not`, the empty array included. ⛔ **`$ne` is left out on purpose.** It is equality's negation, not equality, and it measured the same split. It is reported for its own ruling and not absorbed here. This follows the face's own precedent for the `$in` `{ $field }` member question, which it left to a separate card. An `it.todo` records it, with ⛔ no green pin. The other scalar operators carrying an array (`$gt`, `$contains`, `$like`, …) are not covered either. **Spellings, read at source.** The ruling names `FilterOperatorSchema`. No such export exists on `origin/main`: it has zero hits under `packages/spec/src`, while the control `FieldOperatorsSchema` does resolve. So the two prescribed operators are read off `FieldOperatorsSchema`'s keys and `AST_OPERATOR_MAP`'s lowering: - `$in`, with authoring spelling `in`, is the declared list operator. - `$contains`, with authoring spelling `contains`, is the membership test the spec declares for a `multiple: true` / JSON-stored column. A pin reconciles both against the schema and the vocabulary. The vocabulary declares no array-valued contains operator: `$contains` is `z.string()`. ## 2. What changed - **`packages/spec/src/data/filter-comparand-shape.ts`** gains the equality-slot arm. - `{field:[...]}` and `{field:{$eq:[...]}}` are refused with the face's existing `INVALID_FILTER` / 400 envelope. - The leading sentence is `driver-memory`'s `arrayComparandError` verbatim, so one condition keeps one wording. - The message names the field and the path, then prescribes `{"$in": […]}` (authoring `in`) and `{"$contains": "…"}` (authoring `contains`) on a multi-value field, with an `$or` of those for any-of. - It fits under the 500-char client bound, including a 60-char received-list preview, which the bound test pins at 498. - Scope stops at `$eq`: `null`, every scalar and a `{ $field }` reference pass exactly as before. - The header's 「closes that door for every driver at once」 now lists both slots it is true of: the list-operator slot and this one. A new "Refused BY RULING, 2026-09-23" section records the ruling, the measured table and the scope. - **Both engine doors reach the arm.** The engine's object branch calls the wrapper, and its array branch calls `parseFilterAST`. The `driver-mongodb` pin measures both through the engine, as described in §3. - **Conformance.** `FILTER_COMPARAND_TYPE_CASES` gains three `door-refusal` rows: implicit, `$eq`, and nested under `$or`. This is the one door-refusal table every driver suite already runs through `parseFilterAST` (`driver-sql`, `driver-memory`, `driver-mongodb`, `driver-sqlite-wasm`, `driver-turso`). Its "What belongs here" note says why an array where ONE value belongs sits beside "a plain object in a scalar slot". `FILTER_TEXT_CASES` is untouched. - **`driver-mongodb` pin** — `mongodb-equality-array-comparand-refusal.test.ts`, 13 tests. ⛔ No driver source edit. - A direct caller composing `parseFilterAST` then `translateFilter` is refused, and `translateFilter` is never reached. - A recording engine whose only read path is `translateFilter` refuses both doors and `$eq`, and `count` nested under `$or`, with `translateFilter` called **zero** times. - Lit controls: a scalar, `$in` and `$eq: null`. - A reverse-direction pin shows that `translateFilter` handed the shape directly still emits it unchanged, so the face is the only guard. - mingo is the named proxy, and its readings are recorded in the docblock. mingo is not a dependency of this package, so the pin does not run it. A live `mongod` is stated as NOT MEASURED. - **ADR-0087**: the semantic entry `18.filter-equality-array-comparand-refused` is added, following the `18.view-filter-rule-scalar-operator-array-refused` precedent. `registry.ts` was regenerated with `gen:migration-registry`, and `check:migration-registry` is green. - **Changesets**: - `@objectstack/spec: minor`, with the `**BREAKING**` banner, `fix(spec)!:`, `Clause-②: no (narrowing)` and the `adr-0087 registered filter-equality-array-comparand-refused` disposition marker. The level is `minor` because AGENTS.md makes a `(narrowing)` BREAKING while `check-changeset-no-major` forbids `major`. The launch-window convention ships an accept-set narrowing as `minor`, and the objectstack-ai#19514 changeset is the sibling precedent. `check-changeset-no-major` itself prints "narrowing — a BREAKING change; during the launch window it ships `minor`". - `@objectstack/metadata-core: patch`, for the retired dispatch rows below. ## 3. Tests, firing control, and gates **Firing control: the refusal pins turn red on the face as it stood.** The two new throws were disabled through `scripts/ablation-replace.mjs`. Each anchor hit once, and the blob moved from `ef772a616c` to `f8fd530c31`. An EXIT/INT/TERM trap restored the file. | suite | on the ablated face | after restore | |:--|:--|:--| | `filter-comparand-shape` + `filter-field-reference-lowering`, spec source | **14 red** / 81 green; every lit control stays green | 82 + 13 green | | `driver-mongodb` pin, spec rebuilt | **10 red** / 3 green (the lit controls and the reverse pin) | 13 / 13 green | | `driver-memory` comparand-type conformance, spec rebuilt | **exactly the 3 new rows red** / 20 green | green | For the two spec-rebuilt rows, `ablation-dist-preflight` found both markers **present** in `packages/spec/dist` on the mutate leg. On the restore leg it found them **absent** from all 216 built files, with the tree clean. The restore was proven with a HEAD blob-hash match and an empty `git diff HEAD`. **Suites**, run on `bec8f4c737`. `438d385af3` differs only by the prose-id baseline JSON. - `@objectstack/spec`: 525 files, 15510 passed, 2 todo - `objectql`: 304 files, 5069 passed - `driver-memory`: 52 files, 1248 passed - `metadata-core`: 16 files, 285 passed - `driver-mongodb`: 26 files passed and 5 live-mongod files skipped; 578 passed - `metadata-protocol`: 188 files, 2673 passed - `service-queue`: 5 files, 77 passed Run on `723f254402`, before the consumer fixes: `driver-sql` 2648 passed (168 skipped), `formula` 915, `driver-turso` 1302, `driver-sqlite-wasm` 521, `service-analytics` 2442, `plugin-sharing` 913, `lint` 4121. Typecheck is clean for `spec`, `driver-mongodb`, `driver-memory` and `metadata-core`. `--listFiles` shows each touched test file inside its package's program. **Gates**, run on the final head **`438d385af3`**: - `dispatch-gates --commands` derived 95 families. **93 exit 0.** - **2 are NOT MEASURED**, both exit 3 with PREREQUISITE NOT MET because they need the whole workspace built: `check:dual-build-cjs-loads` and `check:type-check-debt`. - `--ran` reconciliation: 95 derived, 93 run, 2 NOT-MEASURED, 0 UNRUN. - Key verdict lines: - `check-adr-0087-registration`: `[BREAKING+bang+clause-②-narrowing] registered filter-equality-array-comparand-refused (new here)` - `check:doc-authoring`: clean, with the baseline shrink below - `check:engine-double-contract` and `check:nul-bytes`: green - `@objectstack/spec check:generated`: all 15 artifacts current. ## 4. Consumers that broke, and how each was re-judged The repo was grepped (examples, seeds, docs, published skills, fixtures, tests) for the FilterArray triple on `=` / `==` / `equals` / `eq` carrying an array, for `$eq` carrying an array, and for filter / where objects with an array field value. **No shipped example, seed, doc or skill authors the shape.** The full suites above went red in exactly four places: 1. **`filter-comparand-shape.test.ts`** pinned `{tags:{$eq:['a','b']}}` and `{tags:['a','b']}` as passing. It pinned the exact slot the ruling closes, so both rows are inverted. 2. **`filter-field-reference-lowering.test.ts`** pinned `['stage','=',['a','b']]` lowering to the implicit form. The row is re-judged, not dropped: it now asserts the refusal names the implicit slot and not `$eq`, which still proves that an array is not promoted the way a reference is. 3. **Two probe helpers** passed `['a','b']` through every AST spelling. They are `loweredOperatorOf` in the spec suite and in `driver-memory`'s `memory-filter-ast-vocabulary.test.ts`, and the latter turned 4 tests red. Each helper stated that the probe "never trips the shape door". The equality spellings now refuse it, so the helper reads that refusal as `undefined`, which is the answer it always gave them. 4. **`@objectstack/metadata-core`'s engine-double dispatch tables** carried three ARRAY `where.id` rows: delete `array id, no multi`, and update `array id, no multi` and `SCALAR data.id beside an ARRAY where.id`. The real engine now refuses that input at the face **before** the dispatch runs, and `objectql`'s objectstack-ai#4550 harness requires the engine's words to equal the predicate's. That turned 4 tests red. The rows are **retired** with a note, and the predicates are unchanged. The `$in` rows keep the non-scalar-id coverage, and `scalar*Id`'s array pins stay. The changeset is `patch`. `check:doc-authoring`'s prose-id baseline shrank by one (`objectstack-ai#11230` 6 → 5 in that file) through its own `--census-ledger` remedy.⚠️ **Conflict, stated rather than settled silently.** The dispatch said "fix a fixture only if it is plainly an authoring mistake". Items 3 and 4 are not authoring mistakes. They were fixed because the dev contract's clause wins: a published surface this change made false must be fixed in the same PR. After this change, `ENGINE_*_DISPATCH_CASES` claimed a dispatch refusal the real engine no longer gives. Two alternatives were weighed and not taken: - Make `objectql`'s harness accept the face's refusal. That would carve an exception into the objectstack-ai#11009 contract that "both halves must refuse with the SAME words". - Teach the predicate the face. That is impossible byte-for-byte, because the predicate has no object name for the face's `update('task'):` prefix. The seat may prefer another disposition, and the change is reversible. **Files beyond the claim's declared surface**, each for the reason given: - `filter-comparand-type.ts`, one comment sentence. It said the equality-slot array is "answered per driver", which this change made false. - `filter-comparand-type-conformance.ts`, where the conformance rows live. - `filter-field-reference-lowering.test.ts`: item 2. - `driver-memory/src/memory-filter-ast-vocabulary.test.ts`: item 3, test-only. - `metadata-core/src/engine-{delete,update}-dispatch.ts`: item 4, table rows and notes only. - `scripts/doc-authoring-prose-id.baseline.json`: the shrink. ## 5. Compile surfaces, face by face (seat amendment after the at-tier FAIL `5808368753`) Written by the `domain:spec` seat 4 (`session_019c3Hi6ZMU1p6m6aA6Bz45d`), which took this card over on the maintainer's 「你接手派补丁轮」. The FAIL's one blocking item was the missing face-by-face declaration. Each face below gives the review's file:line evidence and one of three conclusions: changed, already compliant, or out of scope with the reason. No code changed for this amendment; the head is still `438d385af3`. | # | face | conclusion | evidence | |:--|:--|:--|:--| | 1 | `driver-sql` | **changed** — behind the shared face on both engine doors | §2 above.⚠️ "nested → 500" describes the base `2548ba57de`; `origin/main` has since carried objectstack-ai#19885, whose nested leaf takes `assertCompilableComparand` | | 2 | `driver-turso` `RemoteTransport` (remote mode) | **already compliant**, and now also behind the shared face | `remote-transport.ts:487` classifies a bare array as `requireValue`, `:612` names it for refusal, `:660` sets `INVALID_FILTER`. "`driver-turso` is built on `driver-sql`" above is true of local mode only; remote mode is its own compiler | | 3 | `read-scope-sql` `compileScopedFilterToSql` | **out of scope** — policy scopes never pass the shared face | Neither it nor its callers (`native-sql-strategy.ts:654`, `objectql-strategy.ts:559`) call `parseFilterAST`. A bare array fails closed as `READ_SCOPE_COMPILE_FAILED` / 500 (`:755`, `:436-440`). `$eq: [...]` binds the array (`:1273`). Filed as **objectstack-ai#19975** | | 4 | analytics `filter-normalizer` | **changed** for the FilterArray form; the OBJECT form is out of scope | FilterArray refused through `parseFilterAST` (`:1521`). The OBJECT form, read as `IN`, is objectstack-ai#19888 | | 5 | `formula` | **already compliant at `origin/main`**; this PR changes no formula file | Since objectstack-ai#19946 (objectstack-ai#19886 phase 2a), `matches-filter.ts:196-255` refuses the bare array and `$eq` / `$ne` arrays | | 6 | objectql `having` (half-face) | **out of scope** — still answers by JS coercion | `engine.ts` gates `where` (`:866`, `:939`) and `aggregations[i].filter` (`:14776`) but hands `ast.having` to `applyHaving` ungated (`:14885`, `:14929`). `having-filter.ts:357-363`: `having: { total: [5] }` is true. A cross-lane `objectql` edit outside this claim; filed as **objectstack-ai#19974** | | 7 | `driver-memory` / `driver-mongodb` | **changed** (memory: behind the face) / **pinned** (mongodb: `mongodb-equality-array-comparand-refusal.test.ts`, 13 tests) | §2–§3 above | Correction to §2: "Both changesets and this body therefore carry the same line" is not exact. The `metadata-core` changeset (a `patch` with no BREAKING) carries no `Clause-②` line; the spec changeset and this body do. ## Acceptance notes These are observed and not fixed here. The report carries each one. - `driver-sql` answers the equality-slot array **nested** under `$and` / `$or` / `$not` with a **500 `DATABASE_ERROR`** on a direct `SqlDriver.find`: SQLite cannot bind the list. The top-level form gets its own 400. Every platform door now refuses the shape first, so only a direct-driver caller still reaches the 500. This is reported as a finding. - `service-analytics`' normalizer does not route the OBJECT form `{field:[...]}` through the shared face, and still charts it as `IN`. Its FilterArray form is now refused. Reported as a finding. - `FilterConditionSchema` still parses `{field:[...]}`, because `FieldOperatorsSchema.$eq` is `z.any()`. A stored filter carrying the shape therefore publishes clean and is refused at query time. That schema-door twin is not in the ruling and is reported as a finding. - `$ne` carrying an array measured the same cross-backend split and is reported for its own ruling. The `ViewFilterRule` schema door already refuses `not_equals` plus an array. - Two texts are now stale for the equality slot only, and neither is edited: ⛔ there is no driver source edit, and the test is not in the claim. `driver-memory`'s `arrayComparandError` still says the spec "comparand door leaves this position to the driver", and `filter-comparand-type.test.ts`'s test title says "their semantics are per-driver today". Carrier: none. - `origin/main` has moved 6 commits past the base. The branch is **not** merged with it. A driver-less `git merge-tree --write-tree` against `origin/main` is clean, and the only overlapping path is the generated `registry.ts`, which is a sorted union. None of the upstream-added lines authors the shape. CI's merge-ref run and the queue validate the merged tree. --- _Generated by [Claude Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…e the derive path for enum members that cannot be spelled (objectstack-ai#19906) Fixes objectstack-ai#19678 Fixes objectstack-ai#19907 Clause-②: no Executes ruling comment `5805845085` on objectstack-ai#19907 (batch objectstack-ai#218 item 3, letter 乙, maintainer 「其他同意」). It narrows item 1 of ruling `5793380467` on objectstack-ai#19678 (batch objectstack-ai#217 item 5, letter 不动 + 声明), which the first round of this PR executed to the letter: > 1. The rule as recorded on `FormFieldSchema.options`' describe and in `defineForm`'s refusal: an enum-typed metadata-form row MAY carry an inline `options` list (human labels, a deliberate subset); a row whose members cannot be spelled as option values (a hyphen, a capital) OMITS `options`, the control derives the members from the served JSON Schema, and meanings go in `helpText`. The refusal names that path as the remedy. > 2. The 27 existing rows stay; objectstack-ai#19331's labels stay. > 3. PR objectstack-ai#19906 lands with its describe and remedy sentence narrowed to that wording. From ruling `5793380467`, the parts 乙 does not narrow still hold: `FormSelectOptionSchema.value` keeps the system-identifier bound, and `newTab` vs `new-tab` stays a recorded boundary, untouched here. No value bound, no schema shape, no key and no export moves. ## What changed - **The describe.** `FormFieldSchema.options` (`packages/spec/src/ui/view.zod.ts`, the `FormFieldBaseSchema` row) keeps its per-option `default` sentence and now adds: *On a metadata form (schema-bound, built by `defineForm`), an enum-typed row may list its members here, to give them human labels or to offer a deliberate subset. An option `value` is a lowercase system identifier, so a row whose members cannot be spelled as option values (a hyphen, a capital) omits `options`: the control derives the members from the served JSON Schema, and their meanings go in `helpText`.* The TSDoc above the row says the same thing and names both rulings. - **The wall.** `defineForm` calls `FormViewSchema.safeParse`. When the parse fails, it throws a `ZodRealError` built from the parse's own issues. That is the class `FormViewSchema.parse` threw before this PR: an `Error` whose `name` is `ZodError`. Its stack is captured at the `defineForm` call, so an uncaught module-load throw prints the issues, the remedy and the author's call site (round 3, below). Only one thing changes in the issues: a grammar refusal (`invalid_format` or `too_small`) at an inline option's `value` (path ending `options.INDEX.value`, also when nested inside the field-row union's `errors`) keeps its message and gets this sentence after it: *An enum member carrying a hyphen, a capital or a single character cannot be a form option `value`, which is a lowercase system identifier. When this row edits a spec enum whose members cannot be spelled as option values, omit `options`: the control derives the members from the served JSON Schema, and their meanings go in `helpText`.* No issue is added, removed or re-coded. - **Generated:** `content/docs/references/ui/view.mdx`, regenerated by `pnpm --filter @objectstack/spec gen:docs` after a spec build. Two table rows changed (the `options` row of the two FormField tables). `check:generated` then reported all 15 artifacts up to date. - **Changeset:** `.changeset/19678-form-option-enum-derive-remedy.md`, `@objectstack/spec: patch`, rewritten to state ruling 乙's rule. ### Round 2 (ruling 乙): what moved from the first round - The describe no longer says *a row whose key is a spec enum omits `options`*. It now permits an inline list on an enum-typed row and scopes the derive path to a row whose members cannot be spelled. - The remedy no longer says *When this row edits a spec enum, omit `options`*. It now conditions the same derive path on members that cannot be spelled as option values. - The verdict did not move. The same values are refused and the same values are accepted as on the first round's head `ebd7fc2fa8`. - The branch is merged with `origin/main` at `c8399867b8` (merge commit `61ff3aebe6`, through `scripts/pm/os-regen-merge.sh`). The wording commit is `182ed4c154` and the regeneration commit is `74ea5dbba3`. ### Round 3 (at-tier record `5818584341`: FAIL): the refusal is an `Error` with a stack again - **What the record found.** Round 2 threw `new z.ZodError(…)`. In zod v4 classic (`zod@4.6.1` here) that constructor has no `Error` parent, so the thrown object was not an `Error` and had no `stack`. An uncaught module-load throw printed only `ZodError { name: 'ZodError', message: [Getter/Setter] }`. That hid the issues and the remedy, the very wall both rulings require. `refusal()` asserted only `toBeInstanceOf(z.ZodError)`, and both shapes pass that. - **The fix** (`76a053e9d0`). `defineForm` now throws `new z.ZodRealError(withOptionValueDeriveRemedy(parsed.error.issues))` and captures its trace with `z.core.util.captureStackTrace(refusal, defineForm)`. `ZodRealError` is the class `FormViewSchema.parse` threw before this PR: its `name` is `ZodError`, it is an `Error`, and it passes `instanceof z.ZodError`. The walker is unchanged. It returns copies, it grows only `invalid_format` and `too_small` at a `…options.INDEX.value` path, it still walks `invalid_union`, and an unrelated refusal is still answered without the remedy. The verdict did not move. - **Why this route, and not either spelling in the record as written.** Both were measured on `zod@4.6.1`, each thrown uncaught from a scratch form module that parses with the source `FormViewSchema` (run by `tsx`). 1. **Neither spelling has a frame.** zod builds every `ZodRealError` with `Error.stackTraceLimit = 0` (`newError` in `zod/v4/core/core.js`). It captures a trace only inside `parse` (`util.captureStackTrace(e, callee)`), and `safeParse` never does. So `throw parsed.error` and a bare `throw new z.ZodRealError(…)` both print the issues as `[ZodError: …]`, with 0 `at` frames and no source line. Read directly in `node`: `new z.ZodError([])` is not an `Error` and its `stack` is `undefined`, `new z.ZodRealError([])` and a `safeParse` error are `Error`s whose stacks hold 0 frames, and the error `parse` throws holds 8. That `stack` is still a string, so the record's two assertions pass on both spellings. The fix captures the trace the way zod's own `parse` does. The callee is `defineForm`, so the first frame is the author's `defineForm(…)` call. 2. **A copy, not a mutation in place.** A mutation in place would not leak into another caller. Two parses of the same input share 0 issue objects, because zod's `finalizeIssue` builds each issue fresh and `lazySchema` caches the schema, never a result. A mutation of the first parse's issues showed up 0 times in the second parse. The hazard is order. zod 4.6.1 computes an error's `message` on its first read and caches it (`_zod.message`), and V8 formats the stack header on the first read of `.stack`. So a message grown in place reaches the printout only if nothing read `.message` or `.stack` before the mutation. Measured on `FormViewSchema.parse`'s own error, which has its frames. Grown with no earlier read, the remedy is in the issues, the `message` and the `stack` once each, and the uncaught printout carries it. After one earlier read of `.message`, the issues still carry it once, but the `message`, the `stack` and the printout carry it 0 times. An error built from issues that already carry the remedy does not depend on that order. - **The wall, proved with a real uncaught throw.** A scratch form module, shaped like `packages/spec/src/**/*.form.ts`, imports `defineForm` from the built `packages/spec/dist/ui/index.mjs` and calls it at module scope with `{ field: 'openIn', options: [{ label: 'New tab', value: 'new-tab' }] }`. A second module imports it, and nothing catches. Both ran under `node` 22.22.2, and stderr was captured: | read on stderr | the fix (`76a053e9d0`, `dist` built) | negative control: round 2's `new z.ZodError(…)` line, `dist` rebuilt | |:--|:--|:--| | node exit | 1 | 1 | | what it printed | `ZodError: [` followed by the issue list as JSON | `ZodError { name: 'ZodError', message: [Getter/Setter] }` and nothing else | | the issue path (`sections.0.fields.0`, then `options.0.value` in the union's branch) | present | absent | | the grammar message (`System identifier must be lowercase…`) | 1 | 0 | | the remedy sentence (omit `options`, the members come from the served JSON Schema) | 1 | 0 | | the remedy's scope (`cannot be spelled as option values`) | 1 | 0 | | stack frames | 4. The first is `action-behavior.form.mjs:5:35`, and node's caret points at `defineForm({` in that module | 0 | For the negative control, `view.zod.ts` was byte-identical to round 2's blob `d6471f538d06`, and `ablation-dist-preflight` found the old line in 11 built files. After the restore the `dist` was rebuilt. The old line is absent from all 216 built files, the tree is clean, and the fix's wall reads the same as before (stderr sha256 `7d118336d597` both times). - **The pin.** On every refusal, `refusal()` now asserts `toBeInstanceOf(Error)` and a string `stack`, the record's two. It also asserts that the stack names this test file, the module that called `defineForm`. The third assertion is the one that tells a trace-less `ZodRealError` apart. A new case reads the wall itself: the head of the stack (`ZodError: ` and the message) carries today's grammar message, read live off the object face and JSON-escaped, and the derive path with its scope. **Ablation, round 3.** One-shot, at `76a053e9d0`, through `scripts/ablation-replace.mjs` under the verify lock, one leg at a time. The test imports `./view.zod` as source, so no `dist` is in its path. | leg | mutation | anchor | blob | result | |:--|:--|:--|:--|:--| | 1 | the throw put back to round 2's `throw new z.ZodError(withOptionValueDeriveRemedy(parsed.error.issues));` | x1 → x0 | `e2feed0106e6` → `d6471f538d06` (round 2's blob, byte for byte) | `Tests 19 failed \| 14 passed (33)`, every one at `expect(thrown).toBeInstanceOf(Error)` | | 2 | only the trace capture deleted: a `ZodRealError` with no frame, the shape both spellings in the record give | x1 → x0 | `e2feed0106e6` → `11a3b9cfae81` | `Tests 19 failed \| 14 passed (33)`, every one at *the stack names no frame in the module that called defineForm*. The record's two assertions passed on this shape | The 19 red cases are the ones that go through `refusal()`. The 14 green ones build a form, parse a schema or read the describe, and never reach `refusal()`. Both legs were restored: after each, the blob was `e2feed0106e6`, equal to HEAD, `git diff HEAD` was empty, and `git status --porcelain` read 0 lines. - **The changeset is not reworded.** Its sentence "`defineForm` still throws a `ZodError` at module load with the same issues and codes" is literally true at this head. The thrown object is a `ZodRealError`, the class `FormViewSchema.parse` threw before this PR. Its issues are the parse's own, copied, with the same codes, and only the matching messages grow. - **No base merge.** `origin/main` moved 23 commits past the round-2 merge base `c8399867b8`, to `e8f163fc3a`. None of them touches this PR's four paths, `identifiers.zod.ts` or `field.zod.ts` (`git diff --name-only`: 0 hits). Derived on a probe tree at `e8f163fc3a` with this PR's four files, the gate list is the same 107 commands as in this worktree (the two sorted lists do not differ). No generated artifact moved: `check:generated` reports all 15 generated artifacts up to date at `76a053e9d0`, and `view.mdx` is unchanged from round 2, so nothing was regenerated. ### Where the refusal lives (found by content), and why the remedy is attached at `defineForm` - **The text** is `SystemIdentifierSchema`'s regex message, declared in `packages/spec/src/shared/identifiers.zod.ts` (lines 104 and 107 on the first round's base). It reaches the form face through `SelectOptionSchema.value` (`data/field.zod.ts`). `FormSelectOptionSchema` reuses that value **by reference**, and the `property schemas are shared BY REFERENCE` pin in `form-select-option.test.ts` holds it there. - **The thrower at module load** is `defineForm` (`ui/view.zod.ts`). On the base it threw through `FormViewSchema.parse`; since the first round it runs `safeParse` and throws the refusal itself. All 17 `packages/spec/src/**/*.form.ts` modules call it at module scope. - **The remedy cannot go where the text is declared.** The same grammar also bounds object-field options (`Field.select.options`) and three object-storage names. For those, "omit `options`, derive from the served JSON Schema" is the wrong advice. A form-face-only message would need a second `value` schema, and that breaks the by-reference derivation the ruling cites. A zod error map on a parent object cannot rewrite the issue either, because the regex check's own `error` resolves first. `defineForm` is the one door where the remedy is true: it stamps `data.provider: 'schema'` on every form it builds. So the sentence is appended there, and only there. ## Measured first, on `origin/main` @ `dabf8d795e` (first round) 1. **Today's refusal** for the card's own example, `defineForm({ schemaId: 'action', type: 'simple', sections: [{ label: 'X', fields: [{ field: 'openIn', options: [{ label: 'New tab', value: 'new-tab' }] }] }] })`: a `ZodError` from `defineForm`, with one `invalid_union` issue at `sections.0.fields.0`. Its object branch carries `{ code: 'invalid_format', format: 'regex', pattern: '/^[a-z][a-z0-9_.]*$/', path: ['options', 0, 'value'] }` with this message, verbatim: `System identifier must be lowercase, starting with a letter, and may contain letters, numbers, underscores, or dots (e.g., "user_profile" or "order.created")` `perRecord` and `system-data` gave the same issue shape and the same text. A one-character value gives `too_small` with `System identifier must be at least 2 characters`. 2. **The describe authors read** (`view.zod.ts:3235` on that base): `Options for select/multiselect/radio/checkboxes fields (per-option \`default\` is not accepted here — declare the pre-selected choice on the object definition)`. It does not name a JSON Schema, `helpText` or omitting `options`. 3. **Census of hand-listed enum members**: see Acceptance notes. None of the 27 rows is broken by this change, and under ruling 乙 every one of them is the permitted shape. ## Tests `packages/spec/src/ui/form-option-enum-derive.test.ts` (33 tests). Its assertions name subjects (omitting `options`, the JSON Schema, `helpText`, members that cannot be spelled) rather than whole sentences. - **The thrown class, and the printed wall (round 3).** Every refusal the file reads goes through `refusal()`, which asserts a `z.ZodError`, an `Error`, a string `stack`, and a stack that names this test file, the module that called `defineForm`. A new case reads the head of the stack, which is what an uncaught throw prints: `ZodError: `, today's grammar message JSON-escaped, and the derive path with its scope. - **Refusal.** The refusal for `new-tab`, `perRecord`, `system-data` (`invalid_format`) and `x` (`too_small`) names the derive path and scopes it to members that cannot be spelled. The grammar message is kept verbatim ahead of the remedy, read live off the object face. A nested row (composite `fields`) gets the same remedy. - **Firing controls for the predicates.** Both predicates are RED on today's message: the object face raises the grammar issue through the very property schema the form face shares, with no remedy. The blanket-rule predicate is LIT on the two spellings the first round shipped, so its "states no blanket rule" assertions cannot be vacuous. - **Ruling 乙 item 1, on real spec enums.** Each case has a firing and a dark control. Every enum is read off the served JSON Schema (`z.toJSONSchema(getMetadataTypeSchema(type))`, input side), so "unspellable", "spellable" and "subset" are measured, not assumed. - `object.managedBy` (members that cannot be spelled): with inline `options` it is REFUSED. Every unspellable member is refused with the remedy, and no spellable one is. The same row without `options`, meanings in `helpText`, is GREEN. - `object.sharingModel` with a labelled full list (the objectstack-ai#19331 shape): GREEN, labels kept. The same list with one member re-spelled with a hyphen is REFUSED at that member. - `field.deleteBehavior` master_detail subset (`cascade`, `restrict`, no `set_null`): GREEN, not widened. The lit precondition shows `set_null` is a served member. The same subset with one member capitalised is REFUSED at that member. - **The verdict did not move.** The same values are refused, `new_tab` is still accepted, and a spellable inline option still builds. - **The remedy is scoped.** An unknown key on the option, and an unrelated refusal on the same form, are both answered without it. - **The describe states ruling 乙's rule** in the served JSON Schema (`z.toJSONSchema(FormFieldSchema)`). It permits an inline list (human labels, a deliberate subset). It names the derive path, scoped to members that cannot be spelled. It no longer states the blanket rule. It keeps the per-option `default` sentence. **Old-wording pins, reversed rather than deleted.** A `git grep` for the old describe, the old remedy and the old ruling's 「never hand-listed」 found one assertion pinning the old wording: the describe test's `toContain('spec enum')`. It became the assertions above: the permission and the scoped derive path are present, and the blanket rule is absent. The file header's restatement of the old rule is rewritten to ruling 乙. The other 「never hand-listed」 hits in the repository (nine test and source comments) describe unrelated derived vocabularies. `../objectui` has no hit for either old sentence. **Ablation, round 2** (one-shot, at `74ea5dbba3`, through `scripts/ablation-replace.mjs` under the verify lock, one leg at a time, with the old wording put back). The test imports `./view.zod` as source, so no `dist` is in the path. | leg | mutation | anchor | blob | result | |:--|:--|:--|:--|:--| | 1 | remedy constant back to *When this row edits a spec enum, omit `options`* | x1 → x0 | `d6471f538d06` → `c91e601551c2` | `Tests 6 failed \| 26 passed (32)`: the four scoped-remedy cases, the nested row, the `managedBy` FIRING case | | 2 | describe back to *a row whose key is a spec enum omits `options`* | x1 → x0 | `d6471f538d06` → `1e03c6756624` | `Tests 3 failed \| 29 passed (32)`: the permission, scoped-derive and no-blanket-rule describe cases | Both legs went red in the expected direction. Both restored: blob after restore `d6471f538d06` == HEAD, and `git diff HEAD` was empty. The first round's ablation, at `2aa26de218`, removed the remedy altogether (`throw parsed.error;`) and gave `Tests 9 failed | 7 passed (16)`, which showed the remedy itself is load-bearing. Suite runs, all at `76a053e9d0` (the PR head): | run | result | |:--|:--| | `@objectstack/spec` `vitest run --project local` | `Test Files 532 passed (532)` · `Tests 15688 passed \| 2 todo (15690)` | | `@objectstack/spec` `vitest run --project repo` | `Test Files 35 passed (35)` · `Tests 602 passed (602)` | | `@objectstack/spec` `typecheck` (tsc + scripts + test layer) | exit 0. The test file is in `tsconfig.test.json`'s program, and `view.zod.ts` in `tsconfig.json`'s (`--listFilesOnly`: 1 hit each) | | `@objectstack/spec` `check:generated` | `All 15 generated artifacts are up to date`, against a `dist` built at this head | | `@objectstack/spec` `check:docs` | `225 generated files in sync with packages/spec` | | eslint, narrowed to the two changed `.ts` files | `--no-inline-config --format json`: 2 files, 0 errors, 0 warnings. Both are in eslint's population (`--print-config` resolves a config for each). The config is not type-aware (no `parserOptions.project` / `projectService`), so this diff cannot move a verdict on an untouched file. The changeset and `view.mdx` resolve no eslint config | **The regenerated page against `main`.** Against the merged `main` tip `c8399867b8`, `view.mdx` differs in exactly the two `options` rows. The six PRs that last moved that page on `main` are `95fb417ec8`, `48c91e9e46`, `9dcdb775a0`, `2b52a5b013`, `b01bdbc4d9` and `1ff3a8f210`. Every line they added that is still on `main`, 51 in all, was grepped quoted-exact (`git grep -F -c`). Each has the same count on `c8399867b8` as on this branch, with 0 mismatches. In round 3 neither side moved the page: `git diff --quiet` exits 0 for `view.mdx` from `c8399867b8` to `origin/main` `e8f163fc3a`, and from `74ea5dbba3` to `76a053e9d0`. **Gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `76a053e9d0` derived 107 commands. The count matches round 2's 107 at `74ea5dbba3`, and the list is identical to the one derived on a probe tree at `origin/main` `e8f163fc3a` with this PR's four files. The `--ran` reconciliation reports `107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED`. All 107 exited 0 on the first run. Their prerequisites were built before it: a spec build, then a turbo build of every package except docs (`73 successful, 73 total`). In round 2, seven of them first exited 3 and went green once those prerequisites were built: - `check:doc-formula-expressions`, `check:doc-security-posture`, `check:docs-transcript-drift`: after the `@objectstack/lint...` closure. - `check:lean-entry-closure`: after the `@objectstack/objectql...` closure. - `check:skill-examples`, `check:dual-build-cjs-loads`, `check:type-check-debt`: after a turbo build of every package except docs (73 tasks). **The `Test Core (1/6)` walker race.** On the first round's head, `Test Core (1/6)` was red on `scripts/check-error-status-conformance.mjs`'s `walk()`: an ENOENT from a transient `tsup.config.bundled_*.mjs` (the open finding objectstack-ai#19667; objectstack-ai#19916 is closed). This base merge re-measured it. On `74ea5dbba3` every check run completed `success`, including `Test Core (1/6)` and all seven required contexts. That script is not edited here. ## Changeset: `patch` Runtime text in a released package changes. The `defineForm` refusal ships in `@objectstack/spec`'s `dist`, and the describe is served in the JSON Schema. That is a released-package change, so there is a changeset. Round 3 changes no word of it: the thrown class is `ZodRealError` again, so its sentence "`defineForm` still throws a `ZodError` at module load with the same issues and codes" is literally true. It is `Clause-②: no`: every value accepted or refused before is accepted or refused now, and nothing an author can write is added or removed. So it takes the checklist's `patch`, not `minor`. ## Sibling PRs - objectstack-ai#19861's region (`checkViewFilterRuleValueShape` / `ViewFilterRuleSchema`) has landed on `main` and came in with the base merge. It merged without a conflict, and this PR does not touch it. - objectstack-ai#19809 is the one open PR that also edits `view.zod.ts` and `view.mdx`. That was re-derived from the file lists of all 34 open PRs on 2026-09-24. Its regions (`PaginationConfigSchema`, the per-kind Gallery / Timeline / Kanban / AddRecord configs, `rowLimitKey`, the `CalendarConfig` type exports) do not overlap the `FormFieldBaseSchema.options` row or `defineForm`. Its `view.mdx` hunks do not touch the two `options` rows. If the two collide, the page is regenerated, never hand-merged. ## Acceptance notes - **Census: 27 inline `options` rows in 9 of the 17 `packages/spec/src/**/*.form.ts` modules** (`git grep` at `74ea5dbba3`: object 12, field 3, hook 3, action 3, page 2, and agent, skill, permission and email_template 1 each). The first round's census grouped them as 11 of 17 metadata forms. This round did not re-derive that grouping. Each row's key was resolved in the served JSON Schema at `dabf8d795e`. - All 27 keys are spec enums. None lists a non-member. **None contains an unspellable member.** So under ruling 乙 every row is the permitted shape, and item 2 keeps all of them. None is converted. - Lit control: the same instrument, run on the three option-less reference rows, reports the unspellable members it should: `object.managedBy` (4: `system-data`, `engine-owned`, `append-only`, `better-auth`), `action.execution` (`perRecord`) and `action.openIn` (`new-tab`). - **24 rows list every member with human labels**: object `fields.valueDomain`, `fields.deleteBehavior` (lookup row), `fields.returnType`, `fields.summaryOperations.function`, `ownership`, `sharingModel`, `editMode`, `lifecycle.class`, `lifecycle.storage.strategy`, `lifecycle.storage.unit`; field `returnType`, `summaryOperations.function`; hook `body.language`, `onError`, `runAs`; action `mode`, `body.language`, `operation`; page `type`, `interfaceConfig.recordAction`; agent `surface`; skill `surface`; permission `managedBy`; email_template `category`. - **3 rows are deliberate subsets**: object `fields.type` omits `secret` and `user`, and the two master_detail `deleteBehavior` rows (object `fields.deleteBehavior`, field `deleteBehavior`) omit `set_null`. - The objectstack-ai#19331 comment in `object.form.ts` ("Each enum gets an explicit `options` list because the bare member reads as a word…") and the served describe now agree. The first round's contradiction between them is what objectstack-ai#19907 decided. - **Boundary:** a schema-bound form view authored outside `defineForm` (a stack's `view` metadata with `data: { provider: 'schema' }`, parsed at compose or publish) still gets the bare grammar message. The ruling names the module-load refusal. The object-field option face is unchanged by design. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- 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>
Documentation Restructure - Fix Build Error
✅ Fixed
Type '...' is not assignable to type 'MDXComponents'as anytype assertion to components objectPrevious Changes
Removed numbered prefixes from all documentation folders (00-, 01-, 02-, 03-, 04-)
00-introduction→introduction01-core-concepts→core-concepts02-protocols→protocols(and subdirectories)03-development→development04-transport→transportUpdated navigation configuration in meta.json files to control sorting order instead of relying on folder name prefixes
Updated all internal links in .mdx files to reference new folder structure
Fixed MDX syntax errors where
<numberwas being interpreted as HTML tagsBuild Status
✅ Build passes - TypeScript compilation successful
✅ 404 static pages generated including all new documentation structure
✅ Clean URLs -
/docs/introduction/overview,/docs/protocols/objectql/schemaFolder Structure
Addresses CI build failure reported by @hotlong.
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.