Repository navigation
feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) - #22094
Conversation
…— the structured ADR-0025 block is the only form WIP: schema narrowing with the list-form prescription, the ADR-0087 D2 conversion manifest-permissions-string-list-removed, its D3 semantic entry and step-18 rationale fragment, the liveness re-verdict, and the two drop reports reworded. Generated artifacts and tests follow. Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…ge the fixtures; regenerate references, authorable surface and liveness counts Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…st.permissions list arm Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…carries the retirement (check:future-spec-major) Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 147 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7b66a97d8b523a1d302b1d98be2467e2cad5c161 && git checkout 7b66a97d8b523a1d302b1d98be2467e2cad5c161
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 033e5c536db11e278182f321f0ae99cf10f4f630 1c3772561365e214d521f67cd2faa25caa897507 && git checkout -B drift-repro 033e5c536db11e278182f321f0ae99cf10f4f630 && git merge --no-ff 1c3772561365e214d521f67cd2faa25caa897507
node scripts/docs-audit/affected-docs.mjs --json 033e5c536db11e278182f321f0ae99cf10f4f630
|
Contract reviewServed-tier: Read at 2026-10-07T15:22Z by an isolated contract-review subagent, adopted by the dispatching seat. Inputs: card #13458 (body and all eleven comments; triage re-verification Check-runs as read. 18 completed: Auto Label, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · source gates, filter — all ① Derived judgmentsAccept set — one narrowing and nothing else moved: right. Carriers of the new refusal sentence — true on each: right. Load paths that parse Reader census: right. The claim's bound — no consumer migration owed, stop if one exists at the head — holds. The dev's H1 is confirmed from The two reworded drop reports: right. The D2 conversion The step-18 chain: right. D3 Public surface: right. Both export names stay; Liveness: right. Residual prose. A sweep of the head tree for legacy-flat-list prose on GitHub content. No model identifier in the PR title or body, the four commit messages (the AGENTS model-free trailer pair), the changeset or the diff. ② Semver levelClause-②: Changeset — right for the If this PR lands before PR #22084: every sentence holds as written. The grade is right; the prescription "removed in @objectstack/spec 17" and If this PR lands after PR #22084 ( ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
✅ ACCEPT: PR #22094 at
|
…tire-manifest-permissions-legacy-arm
…safe master-reference shapes at error on a controlled_by_parent object (objectstack-ai#22109) Fixes objectstack-ai#9139 Clause-②: no ## What this does `relationship/master-detail-required` (R2 in `packages/lint/src/data-model-rules.ts`) now has two tiers under one rule id, as the maintainer ruling of 2026-08-16 (Direction 1, scheduled for the v18 boundary) orders. - **On an object with `sharingModel: 'controlled_by_parent'`**, every `master_detail` field is refused at `error` in each of the three unsafe shapes: `required` absent (or `false`); `required: true` + `readonly: true`; `required: true` + `system: true`. One finding per field, located at the first defect (`.required`, `.readonly` or `.system`); the `fix` names every edit the field needs. - **On every other object** the verdict is unchanged: a `warning` for a `master_detail` without `required: true`, with the same message and fix, and silence on the two flagged shapes. Before this change the predicate was `required !== true` at `warning` on every object. Two of the three shapes drew no finding at any severity, as the census note on objectstack-ai#9139 measured. A one-line severity flip would have closed one shape and left two open, so the predicate covers all three. Each shape is pinned on its own. Runtime is untouched, as the card's scope requires: `resolveCbpRelation`'s fallbacks and the security gate stay as they are. ## The mechanism hypotheses, measured | # | hypothesis | reading | |---|---|---| | H1 | R2 at `data-model-rules.ts:742`, `warning`, `required !== true`, every object | Holds at `b04a5295f`. Before-probe: CBP and non-CBP alike gave `warning` for absent / `false`, and nothing for `+readonly` / `+system`. | | H2 | the `:817` pin filters to `SECURITY_CBP_NO_RELATION`, so promoting R2 leaves it green untouched | **Holds.** The pin asserts only that the CBP no-relation rule (a mirror of `resolveCbpRelation`) is silent. That stays true, because the runtime still resolves step 2. The pin is **reversed, not removed**: see the pin section below. | | H3 | R2 bites `os lint` and the eval rubric only | **Holds.** `lintDataModel` is called from `lintConfig` (`os lint`, exit 1 on any error) and from `scoreMetadata` (the generation rubric: `valid` requires zero errors). It is not an `AUTHORING_RULES` entry: `authoring-rule-wiring.test.ts` pins `lintDataModel` in `DIRECT_CALL_RATCHET` as lint-only. So `os build`, `os validate` and the metadata save door do not run it. `check:i18n-coverage` spawns `os lint` but tolerates a non-zero exit. Registration was **not** widened; see the open question in the report. | | H4 | objectstack-ai#9138 (builder force) landed | **Holds.** `forceCbpMasterDetailRequired` (`object.zod.ts`) forces an omitted `required` to `true` under CBP and refuses an explicit `false`. It skips the array field form and never inspects `readonly` / `system`. So through `ObjectSchema.create` only shapes 2 and 3 reach the lint, and both pass the builder untouched. Shape 1 reaches the lint from anything not built through `create`: a plain object literal, the array form, or raw parse of stored metadata. Pinned with real `ObjectSchema.create` objects. | | H5 | 0 in-tree corpora turn red | **Holds.** Census below. | | H6 | take the next free order in the step-18 chain | **Falsified as stated.** There is no order to take: entries are one file each under `entries/semantic/`, and `gen:migration-registry` sorts the registry by id. The new entry is `cbp-master-detail-required-lint-error`. Whichever of this PR, objectstack-ai#22094 and objectstack-ai#21974 lands later merges `main` and re-runs the generator. Per the entries README's measured table, a driver-less merge conflicts only when two new ids are adjacent in sort order. Whether that holds against those two PRs' ids is NOT MEASURED. | ## The step-2 pin: reversed, not removed `validate-security-posture.test.ts` "stays silent on step 2: ANY master_detail (not marked required)" pinned the step-2 shape as supported. The ruling retires that reading as a deliberate contract narrowing. Under H2 the pin's assertion is about the runtime mirror, and that half stays true, so the pin now asserts **both halves on one stack**: 1. the CBP no-relation rule stays silent: `resolveCbpRelation` still resolves a non-required `master_detail`, and metadata at rest keeps loading; 2. the same declaration draws `relationship/master-detail-required` at `error` from `lintDataModel`. The pin's title and comment state the narrowing. The pin is kept rather than deleted, so a later change that re-tolerates the shape at authoring time, or drops the runtime tolerance, turns it red. ## Census (H5), at `b04a5295f` plus this change Every `*.object.ts` outside tests and fixtures (111 files, 0 import failures), plus the CLI's golden eval corpus and the multi-package example's two sub-stacks. Each corpus was linted with its own controls appended to its own array: one positive control per unsafe shape and a clean negative control. | corpus | objects | `controlled_by_parent` | R2 error | R2 warning | controls | |---|---:|---:|---:|---:|---| | examples/app-crm | 6 | 1 | 0 | 0 | 3 reached, neg clean | | examples/app-showcase | 22 | 2 | 0 | 0 | 3 reached, neg clean | | examples/app-showcase (external sub-stack) | 2 | 0 | 0 | 0 | 3 reached, neg clean | | examples/app-todo | 1 | 0 | 0 | 0 | 3 reached, neg clean | | examples/app-multi-package (core, orders) | 2 | 0 | 0 | 0 | 3 reached, neg clean | | packages/cli DEFAULT_METADATA_EVAL_CORPUS | 11 | 4 | 0 | 0 | 3 reached, neg clean | | packages/platform-objects | 46 | 0 | 0 | 0 | 3 reached, neg clean | | packages/plugins, services, metadata-core, qa, create-objectstack | 39 | 0 | 0 | 0 | 3 reached, neg clean | | **total** | **129** | **7** | **0** | **0** | | No stored in-tree metadata turns red. ## v18 upgrade-checklist line (for the v18 release notes) > On every object with `sharingModel: 'controlled_by_parent'`, give each `master_detail` reference `required: true` and remove any `readonly: true` or `system: true` from it. `os lint` now refuses the missing-`required`, `required` + `readonly` and `required` + `system` shapes there at `error` (`relationship/master-detail-required`). An object authored through `ObjectSchema.create` already gets `required: true` when the key is omitted, so the edit there is dropping the flag. The changeset's Remedy section carries this line verbatim. The step-18 semantic entry `cbp-master-detail-required-lint-error` carries the same prescription for `os migrate meta`. This PR does not touch `content/docs/releases/**`. ## Release grading `.changeset/pre.json` is absent on `origin/main`: read at `b04a5295f` (2026-10-07T15:37Z) and again at `bafb58bb0` before this push. So the changeset grades `@objectstack/lint` and `@objectstack/spec` at `minor`, with the BREAKING banner, `Clause-②: no (narrowing)` in its body, and the ADR-0087 disposition `registered cbp-master-detail-required-lint-error`. ## Files - `packages/lint/src/data-model-rules.ts`: R2's CBP tier (`cbpMasterReferenceFinding`), placed above the first exported rule. `authoring-rule-wiring.test.ts` reads a rule body as the source text up to the next `export`, so an error-emitting helper placed below `lintLegacyOrganizationComposites` was read as that advisory rule emitting `error`. That was measured red once, then fixed by moving the helper. - `packages/lint/src/data-model-rules.master-detail-required.test.ts` (new): 19 cases. Each unsafe shape at `error` with its path; two clean controls; one finding for several defects; every `master_detail` field; the array field form; a lookup ignored; non-CBP unchanged across 4 sharing models plus the two flagged shapes; and real `ObjectSchema.create` objects. - `packages/lint/src/validate-security-posture.test.ts`: the step-2 pin, reversed as described above. - `packages/spec/src/migrations/entries/semantic/18.cbp-master-detail-required-lint-error.ts` and the regenerated `registry.ts`. The entry carries no tracker id and no call spellings. `spec-changes.json` and the upgrade guide do not project step 18 yet, and `check:generated` reports all 15 artifacts current. - `packages/cli/test/score.test.ts`: fixture triage. The "warning" fixture of "suggestions cost less than warnings cost less than errors" was a CBP object, so it now measured the error weight. Probe: `counts.errors` 1, `valid: false`, with R2 at `error`. Its assertions still passed, but for a different reason. The fixture moves to `sharingModel: 'private'` and two assertions pin it to the warning tier. - `content/docs/protocol/kernel/error-handling.mdx`: this change made one sentence false ("does not report the `readonly`, `system` … shapes at all"). It now names `os lint`'s error tier and its reach. - `.changeset/9139-cbp-master-detail-required-error.md`. ## Verification (final head `bee9c1e25`) All readings below were taken at `bee9c1e25`, after the last commit. Each exit code was captured before any pipe. - **Derived gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` gave 113 commands. All 113 ran at `bee9c1e25`: **112 exit 0, 1 exit 3**. The exit 3 is `node scripts/check-plugin-teardown-shape.mjs --self-test` refusing its own prerequisite, because its positive control is pinned to commit `621a4876` outside this shallow clone. That is NOT MEASURED, not a finding; CI runs it on a full checkout. Reconciled with `--ran`: "113 derived famil(ies) accounted for — 112 run, 1 NOT-MEASURED, 0 UNRUN". Gate lines quoted from the run: - `check-adr-0087-registration`: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing] registered cbp-master-detail-required-lint-error (new here: …)" - `check:migration-registry`: "registry.ts is current (382 semantic, 247 retired-key, 218 retired-def)" - `check-issue-citations`: "every citation this change adds resolves" - `check:nul-bytes`: "OK … no raw ASCII control bytes" - `check:doc-authoring`: "17816 customer-facing string(s) … clean" - **`@objectstack/lint`:** `pnpm --filter @objectstack/lint test` passed 122 files / 5665 tests. `pnpm --filter @objectstack/lint typecheck` was green; "check:test-typecheck: OK". - **`@objectstack/spec`:** `vitest run src/migrations scripts/build-migration-registry-entry.test.ts scripts/step18-rationale-merge.test.ts` passed 5 files / 191 tests. `pnpm --filter @objectstack/spec check:generated` was "All 15 generated artifacts are up to date". That run was on top of `3f6d9ca97` (the registry regeneration); `packages/spec` has not changed since, and its per-artifact gates are in the 113 above. - **`@objectstack/cli` (consumer that reads R2 findings):** `vitest run --project unit` passed 259 files / 3795 tests. `pnpm --filter @objectstack/cli typecheck` was green. The `integration` tier is declared to CI; `migrate-meta-engine-guidance.test.ts`, which holds every semantic entry's printed prose, lives there. - **ESLint, a proven narrowing.** `eslint --no-inline-config --format json` over the 6 touched `.ts` files counted 6 files, 0 errors and 0 warnings. `--print-config` resolves all 6 inside the config. The `.md` / `.mdx` files answer "File ignored because no matching configuration was supplied". The config enables no type-aware linting (`parserOptions` is `{ ecmaVersion, sourceType }`, with no `project`), so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is CI's run. **Reverse verification.** With `data-model-rules.ts` reverted to `b04a5295f` and the tests run from the committed state, **10 failed / 142 passed**. The failures: all 4 CBP shape cases, several-defects, every-field, array form, the 2 `ObjectSchema.create` refusals, and the reversed step-2 pin. The controls and every non-CBP case stayed green, as expected. The file was restored with `git checkout HEAD -- …`, and the restore was proven by blob hash equal to HEAD's and an empty `git diff HEAD`. ## Acceptance notes (not changed here) - `skills/objectstack-data/references/lint-rules.md:13` lists this rule's severity as `warning`. That is now true only off `controlled_by_parent`. `skills/**` is a Tier H governed surface outside this claim's file surface, so it is left for the seat (see the report's open question). - `packages/plugins/plugin-security/src/security-plugin.ts`, the paragraph above the freeze note: "Direction 1 … has NOT landed … nothing warns on the way past". It says itself that it "goes stale when objectstack-ai#9139 lands". The dispatch fences the runtime package, so it is left for the seat. - The `object.zod.ts` docblock of `forceCbpMasterDetailRequired`, and the sibling entry `cbp-master-detail-required-forced`, say the lint "stays `warning` until v18". That is still accurate as a schedule, so neither was edited. - The census's earlier global red (`showcase_field_zoo.f_master_detail`) was already fixed on `main` (`required: true`). The global warning count is now 0. --- _Generated by [Claude Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ubflow node config, with its location (objectstack-ai#22129) Part of objectstack-ai#21982 Clause-②: no (narrowing) **Draft, patch round 1 done** (claim revision `6050287134`). This PR lands the `script` / `subflow` half of the card. The card stays open for the remainder named below. ## What changes The build doors now refuse a key that a `script` or `subflow` node's executor contract does not declare. They refuse it at its location, in the existing family of flow slot refusal codes. - **The doors:** `FlowSchema.parse` / `defineFlow()`, `defineStack`, `objectstack validate`, `objectstack compile`, an artifact's parse, the metadata save door's `flow` type schema, and `registerFlow`, which parses `FlowSchema` first. - **The code:** the existing `node-config-refused-by-contract`, `params: { nodeType, key }`. There is one refusal per undeclared key, anchored at the key (`nodes.N.config.bogusKey`), and its message is the contract's own sentence. - **The edit:** `packages/spec/src/automation/flow-node-config-refusals.ts`, the builtin branch of `flowNodeConfigRefusals`. It judges key membership where `builtinKeysJudged(nodeType)` holds. That is a builtin contract whose type is in the spec's own schemaless class, `SCHEMALESS_NODE_CONFIG_SCHEMAS`. - **Why the narrow lift:** a schemaless descriptor publishes no `configSchema`, so `registerFlow`'s undeclared-key walk skips it. Its executor still parses the strict contract, so it refuses the node at every run. - **Unchanged:** `getBuiltinNodeConfigContracts()` keeps its 13 entries, and no new code joins `FLOW_SLOT_REFUSAL_CODES`. - **Premise corrected:** `builtinValueJudged`'s docblock said "registration refuses an undeclared key against the descriptor". That was false for exactly these two types. The docblock now says which door owns which key. - **Comments made true:** the `FlowSchema` header in `flow.zod.ts` and the `node-config-refused-by-contract` docblock in `flow-node-expression-paths.ts` now name each arm that judges key membership: approval whole (objectstack-ai#21850), builtin values (objectstack-ai#21898), and `script` / `subflow` keys. - No `service-automation` source line moves, and there is no second key check anywhere. ## Built-ins: which get the key refusal, and why (measured at `15ec50e528`) | type | in `getBuiltinNodeConfigContracts` | descriptor `configSchema` | contract strict | executor parses it | key refusal here | |:--|:--|:--|:--|:--|:--| | `script` | yes | none | `strictObject` | yes (`screen-nodes.ts` `parseNodeConfig`) | **yes** | | `subflow` | yes | none | `strictObject` | yes (`subflow-node.ts` `parseNodeConfig`) | **yes** | | `decision` | no | none | `strictObject` | no: its executor reads `conditions[]` raw | no. An undeclared key fails no run. `decisionShapeRefusals` judges the `conditions` shape, not keys. | | `wait`, `connector_action` | no | none | their contracts are the FlowNode sibling blocks `waitEventConfig` / `connectorConfig`, `strictObject` inside `FlowNodeSchema` | they read the sibling block, not `config` | no. `FlowSchema` already refuses an unknown key in those blocks. | | `get_record`, `create_record`, `update_record`, `delete_record`, `notify`, `http`, `screen`, `map`, `loop`, `parallel`, `try_catch` | yes | yes | `strictObject` (all 11) | yes | no: the remainder, below | | `assignment` | no | yes (keyValue map) | no: open top-level variable names | no single contract | no | ## The remainder: the card stays open for it Triage's direction step 2 covers every builtin whose executor contract is strict. All 13 are. This PR takes the two schemaless ones, by the seat's ruling `6050287134`. - **Remainder 1, the 11 descriptor-`configSchema` builtins at the build doors:** `get_record`, `create_record`, `update_record`, `delete_record`, `notify`, `http`, `screen`, `map`, `loop`, `parallel` and `try_catch`. - They have the same gap at the build doors. Measured at this branch's round-0 head `f281d801f` on `examples/app-showcase`, flow `showcase_task_completed`, node `notify`, with `config.bogusKey: 1`: - `objectstack validate` exits 0; - `objectstack compile` exits 0, and the artifact carries `"bogusKey":1`; - `registerFlow` refuses the same node: "Flow 'p' rejected: 1 undeclared config key(s). … unknown config key `bogusKey` at config.bogusKey". - So boot drops the flow with a warning, and the build never says so. - **Remainder 2, an open design choice, not decided here:** once the spec arm covers those 11 types, who judges a builtin's undeclared key at `registerFlow`? The spec arm pre-empts registration's descriptor walk, and with it that walk's pinned prescriptions (`service-automation` `config-unknown-keys.test.ts`). ## Census first (triage step 1): no writer found | corpus | read at | `script` nodes | `subflow` nodes | with a key outside the contract | |:--|:--|:--|:--|:--| | this repo: `examples/**`, `packages/platform-objects/**`, `packages/apps/**`, `packages/create-objectstack/**` (templates), `skills/**`, `content/docs/**` | `15ec50e528` | 6 | 2 | 0 | | hotcrm, whole tree | `c9678036d9` | 0 | 5 | 0 | | objectui flow designer `FLOW_NODE_CONFIG` | pin `a58626c88d` (same file at objectui `main` `9990f9e122`) | form writes `function`, `inputs`, `outputVariable` | form writes `flowName`, `input`, `outputVariable` | 0 (its `timeoutMs` field writes the node; the five retired `script` keys sit behind a `showWhen` no field satisfies) | | objectui designer seeds `defaultNodeExtras` | `a58626c88d` | empty `config` | empty `config` | 0 | | objectui console preview samples | `a58626c88d` | 4 | 0 | 0 | - **Method:** a TypeScript-AST scan for object literals carrying `id` and `type: 'script'` / `'subflow'`, reading the keys of their `config`. Code fences in `.md` / `.mdx` and `.json` files were parsed too. - **Control:** over this whole repo, tests included, the same scan finds 103 nodes and flags 17. All 17 are fixtures: - 9 in the D2 conversion fixtures (`conversions/registry.ts`); - 5 in `lint` tests; - 3 test-double keys in `service-automation` `engine.test.ts`, repaired below. ## Doors, measured - **`objectstack validate` / `compile`.** Built CLI at round-0 head `f281d801f`, `examples/app-showcase` node `summarize` (`script`), one edit: `function: 'summarizeCompletedTask' , bogusKey: 1,`. - Control: validate exit 0, compile exit 0, and the artifact has no `bogusKey`. - With `bogusKey`: validate **exit 1**, compile **exit 2**, and no artifact is written. Both print the refusal at path `nodes, 1, config, bogusKey`. - The mutation was made with `scripts/ablation-replace.mjs`: anchor 1 → 0, then restored to the HEAD blob, `git diff HEAD` empty. - **`registerFlow`** (real builtin executors): - `script` / `subflow` with `bogusKey` are refused at `nodes.1.config.bogusKey`; - both controls register; - a `script` `functionName` alias registers: it is converted before the parse; - `http` `bogusKey` is still refused by the descriptor walk. - **Pinned in `flow-builtin-node-config-keys.test.ts`** (19 tests): - the refusal at `FlowSchema` (also inside a region body), `defineStack` (`STACK_SCHEMA_INVALID` 422 at `flows.1.nodes.1.config.bogusKey`), `ObjectStackDefinitionSchema`, the save door's `flow` type schema and an artifact parse; - the controls: no extra key; a descriptor type's key still left to registration (`http`, `create_record`, `screen`); `decision`; a retired `script` key keeps its tombstone path. - **Reverse verification** at round 0, `builtinKeysJudged` mutated to `return false`: 12 of 19 went red and the 7 controls held. It was restored to the HEAD blob. ## Cross-lane fixtures repaired (claim revision `6050287134`) - **`service-automation`, test only:** - The doubles in `engine.test.ts` ("should execute unconditional branches in parallel", "should fail when parameter type is wrong") and in `input-schema-retry-parity.test.ts` now register under the type `probe_step`, executor and nodes alike, never the builtin `script`. - The `function: 'noop'` filler went with them. - `inputSchema` reads top-level config keys, which a real `script` executor refuses. - **`lint`:** `validateStackExpressions` keeps the pre-conversion tolerance it declares. - The filter in `validate-expressions.ts` also hands the judge's undeclared-key refusal for a `script` node's `functionName` alias to the callable check, which already reads that alias. - A new pin holds that every other undeclared `script` key is still refused there (`bogusKey`, on a canonical and on an alias source). - The changeset gains `'@objectstack/lint': patch`. **Red → green.** Round 0 at `f281d801f` had 4 red in `service-automation` and 2 red in `lint`. All six now pass at `ef0dfb44d`: - `engine.test.ts` › "should execute unconditional branches in parallel" ✓ - `engine.test.ts` › "should fail when parameter type is wrong" ✓ - `input-schema-retry-parity.test.ts` › "never executes a node whose config mis-types its declared inputSchema — on ANY attempt" ✓ - `input-schema-retry-parity.test.ts` › "still retries a VALID flow normally …" ✓ - `validate-expressions.test.ts` › "accepts a script node that names a callable via the functionName alias" ✓ - `validate-expressions.test.ts` › "a `script` with no `function` is ONE finding, the callable check's …" ✓ ## ADR-0087 - **D3 entry:** `18.flow-script-subflow-config-undeclared-keys-refused.ts`. - **Rationale fragment:** step 18 `order: 87`, re-read on `origin/main` `8fc50b764` (the merged base): its highest order is 86, and open PRs objectstack-ai#22103 and objectstack-ai#22094 hold 86 and 85 at their heads. - **Registry:** `registry.ts` was regenerated by `gen:migration-registry`. - **Changeset:** `@objectstack/spec` `minor`, BREAKING, with the `registered` marker, plus `@objectstack/lint` `patch`. `.changeset/pre.json` is absent on `origin/main`. ## Merge - `origin/main` `8fc50b764` was merged by `scripts/pm/os-regen-merge.sh` as merge commit `521e16f1f`, with parents `f281d801f` and `8fc50b764`. There were no conflicts. - Step 2 took `main`'s side of the generated artifacts that `main` moved, and there was nothing more to commit. - After a spec build on the merged tree, `check:generated` reported all 15 artifacts up to date. The delta against `main` was exactly this PR's 5 round-0 files. - `gen:schema` was not run. - Round 1's edits are commit `ef0dfb44d` on top. ## Verification at `ef0dfb44d` - **Spec:** - `check:generated`: all 15 artifacts up to date. - The pin files `flow-builtin-node-config-keys.test.ts` and `flow-builtin-node-config-values.test.ts`: 63/63. - Round 0's whole spec suite at `f281d801f`: 624 files, 18628 tests passed. - **lint:** the whole suite, 123 files, 5689/5689. - **service-automation:** the whole suite, 175 files, 2120/2120. The first attempt collided with a concurrent gate run that left `@objectstack/spec/automation` unresolvable for 17 files; it was re-run alone. - **Typecheck:** `@objectstack/lint` exit 0 and `@objectstack/service-automation` exit 0, both including `check:test-typecheck`, over a closure rebuilt with declarations. - **eslint, narrowed** (`--no-inline-config --format json`) over the diff's 10 `.ts` files: 10 files, 0 errors, 0 warnings. The population is read from the json count. `parserOptions.project` and `projectService` are null for each file, so there is no type-aware linting and no untouched file's verdict can move. - **Gates:** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 92 commands from the merged head. All ran, with exit codes captured before any pipe. The `--ran` reconciliation reads 91 run, 1 NOT-MEASURED, 0 UNRUN (`check:dts-closure` recorded at its re-run). - 91 exit 0. - `check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET (packages outside this worktree's build closure have no `dist`). It is read from CI, as are round 0's `cli` published-subpath pins. - `check:dts-closure` first exited 1, naming exactly the 19 closure packages built with `OS_SKIP_DTS=1` for the test runs. Re-run after the closure was rebuilt with declarations, it exits 0: 169/169 declaration files across 71 built packages. --- _Generated by [Claude Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… verdict (objectstack-ai#22032 pass 3) (objectstack-ai#22151) Part of objectstack-ai#22032 Clause-②: no (narrowing) This is pass 3 of objectstack-ai#22032: a field option's `visibleWhen`. Pass 4 (the object's own action predicates) stays fenced, and the card stays open for it. ## What changes **The object save door gives the build's verdict on a field option's `visibleWhen`.** `formulas.mdx` says "the same `validateExpression` validator backs `os build` and metadata registration". After pass 2 the object door ran the whole field walk except the per-option loop, which kept its own guard. So an object whose option carried `visibleWhen: 'amount > 1'` (a bare field reference) still saved with a 200, while `os build` refused it at error. The server's option check cannot evaluate such a predicate and fails open (logged, allowed through), so the gate it declares is never enforced (read from `evaluateOptionVisibility` in `packages/objectql/src/validation/rule-validator.ts`). - **The lift is the guard, nothing else (H1 held).** On `origin/main` `8fc50b7647` the loop read `for (const [oi, opt] of (objectWrite ? [] : recordsOf(f.options)).entries())` (`validate-expressions.ts:2072`), under a `[objectstack-ai#22032] FENCED on an object write (pass 3 …)` comment. It now reads `recordsOf(f.options)`. On an object write the loop runs at the build's own position in the field walk, so the door gets both of the option's checks: - `check(optionWhere, opt.visibleWhen, objectName, 'record')`; - `refuseFieldTraversal(optionWhere, 'option visibleWhen', …)`, the refusal of a read through a reference field on `record` or `previous`. - **`current_user` keeps the build's two verdicts (H2).** An option's evaluator binds the acting user (ADR-0068 D1), so the build accepts `current_user` on an option and refuses it on the field-rule slots one level up. The door now gives both verdicts as the build does. The showcase's role gate, `'org_admin' in current_user.positions`, still saves on an option, and the same text on the field's own `visibleWhen` is refused at both doors. Both are pinned. - **No registry change (H3 re-verified).** The `validateStackExpressions` entry declares `runtimeTypes: ['flow', 'action', 'hook', 'object']` (`authoring-rules.ts`), and `runtimeAuthoringRulesFor('object')` (`runtime-gate.ts`) dispatches it. `runtime-gate.ts` is untouched. - **Docblocks made true.** `StackExpressionOptions.runtimeWriteType` now names four admitted passes and one fenced pass. `AuthoringRuleContext.runtimeWriteType` in `authoring-rules.ts`, the one line that reaches a built `.d.ts`, names the per-option pass. The function-head comment and the field walk's two comments move with it. In `authoring-rules.ts` the registry entry gains a `[objectstack-ai#22032, pass 3]` measurement comment, as passes 1 and 2 added theirs. Comments in four sibling test files are corrected so that none of them still says option `visibleWhen` is fenced. - **The door's verdict is the build's finding (H4).** The door's 422 issue and `runAuthoringRules('build', …)` give the same rule (`expression-invalid`), location (`object 'fx_option' · field 'province' option 'zj' visibleWhen`), path, message and hint. The pins compare these key by key. - **No code change in `packages/metadata-protocol`.** Only its test file gains the door-level pins. ## Pins - **Lint door:** `packages/lint/src/runtime-gate.object-option-visibility-writes.test.ts` (new, 14 tests). - LIT: one refused body per finding the pass gives. These are a bare `amount > 1`, an unregistered `sqrt(record.amount) > 1`, an unknown field `record.amont > 1`, a syntax error `record.country ==`, and the traversal refusal through `record.account` and through `previous.account`. Each is located at the option and asserted on its named subject. - CONTROL (the still-accepted cases): the `record.country` cascade, the showcase's `current_user.positions` role gate, a grant check plus role gate (`current_user.can(…) && …`), and a reference compared as a value (`record.account != null`). Each is clean at the door and at the build. - CONTRAST: `current_user` on the option and on the field's own `visibleWhen` in one body. The build and the door both give exactly one finding, at the field slot. - PARITY: for each refused body, the door's findings equal the build's. - The differential: a stored sibling's broken options are not this write's to answer for. - **The fence (enumeration pin)** in `packages/lint/src/runtime-gate.object-formula-writes.test.ts`. The fenced site is now pass 4's alone (an action `visible`). The option `visibleWhen` site moves to the lifted sites, beside the validation rule and the `requiredWhen`. The build flags all four sites. The object door flags the three lifted sites in the build's order, and `runStackExpressionPasses` on an object write returns exactly the build's findings for the admitted passes. - **Protocol door:** a new pass-3 block in `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, through the real `saveMetaItem`, `publishMetaItem` and `publishPackageDrafts`. - (a) A bare reference, an unregistered function and a read through a reference field are each refused on an active save. The answer is a 422 `INVALID_METADATA` carrying the build's located finding, and nothing lands. - (a) The card-shaped body is refused on a draft's promotion and on a package draft publish (`outcome: 'refused'`, `failed` naming the object with `INVALID_METADATA`, the row left a draft). The draft saves themselves still succeed. - (b) The showcase's cascade and its `current_user` role gate still save, and the row lands active. The role gate rides every refused body too, which each yield exactly one finding. - (d) For each refused body, the door and `os build` give the same finding on rule, where, path, message and hint. ## Reverse verification (one-off, from committed HEAD `23ce6c494c`) - **What was mutated.** `scripts/ablation-replace.mjs` (wrap mode) put the guard back on the option loop. The new text was `const ablationFence22032p3 = objectWrite;` followed by `for (const [oi, opt] of (ablationFence22032p3 ? [] : recordsOf(f.options)).entries()) {`. The anchor was hit once, 1 to 0, and the blob went `879fcb828037` to `73bd026d1554`. The outer script carried `trap restore EXIT INT TERM` on the absolute path. - **Rebuild and dist proof.** `@objectstack/lint` was rebuilt, and `ablation-dist-preflight` found the marker in 4 built files. - **Lint suites (source): 9 failed and 14 passed, as predicted.** - Red: the 6 LIT tests, PARITY, and the two fence tests that assert the lifted sites. - Green: the 4 CONTROL tests, CONTRAST, the differential, the registry test, the build-flags-each-site test and the six formula-door tests. - **Protocol pass-3 block (dist-mediated): 6 failed and 1 passed, as predicted.** Red: the three (a) saves, (a) on promotion, (a) on package publish, and (d). Green: (b). - **Restore.** The tool restored the file: blob `879fcb828037` equals HEAD, and `git diff HEAD` is empty. The whole tree had 0 changed paths. Lint was rebuilt, and `--absent` found the marker gone from all 14 built files. Both suites went green again: lint 23 of 23, and the protocol file 99 of 99. ## Measurements - **Corpus first: the stop condition was not met (H5).** Every object this tree ships was judged before the door changed. That is every `*.object.ts` under `packages/**` and `examples/**` (111 files) plus the two `app-multi-package` sub-stacks: 118 objects in 18 groups. Each was judged at the raw shape and at the `ObjectSchema.parse` shape (0 parse failures), with its own group as context. - 5 option predicates on 2 fields of 1 object, all on `showcase_cascade`: `province`'s four `record.country` cascades (`zj`, `gd`, `ca`, `tx`) and `tier`'s `restricted` role gate (`'org_admin' in current_user.positions`). - At base `8fc50b7647`: 0 build errors and 0 build warnings for the option pass, through `validateStackExpressions` and through `runAuthoringRules('build')`, at both shapes. There were 0 door expression findings over all 118 objects. - Non-vacuity: the 5 sites are judged. Mutating one cascade to a bare `country` and the role gate to `sqrt(record.amount) > 1`, in a copy, gave 2 build errors at those two options. - At head `23ce6c494c`, and again at the merged heads `06d3cad963` and `3c1d3262ee`: 0 door errors and 0 door advisories over every object, through `runRuntimeAuthoringRules` with type `object`, at both shapes. The harness passes each object's own group as context, so at `3c1d3262ee` every one of those saves is an update and also runs the stored-universe pass that objectstack-ai#22118 added. - Positive control in the same harness (an option `visibleWhen: 'amount > 1'`): 1 build error at every head. The door gave 0 at base and 1 at head. - **Which doors newly answer 422.** The active publish save, a draft's promotion, and a package draft publish. Each was measured through the real methods above. A draft save stays ungated, measured by the same pins. ## Clause-② (measured) - **Accept set: narrowing.** An object write in publish mode answered 200 for an option `visibleWhen` the validator refuses. It now answers 422 on the three doors above. - **Built entry declarations.** In `@objectstack/lint` one doc comment moves (`AuthoringRuleContext.runtimeWriteType`). `StackExpressionOptions` and `runStackExpressionPasses` are not in the built declarations. No exported signature moves. - **Changeset.** `.changeset/22032-object-save-door-option-visible-when.md` covers `@objectstack/lint` and `@objectstack/metadata-protocol` as `minor`. It carries `fix(lint)!`, the `Clause-②: no (narrowing)` line, a BREAKING section with the remedy, and ADR-0087 `not-required (no-migration-prescription)`. `@objectstack/metadata-protocol` is listed as passes 1 and 2 listed it, although no code moves there: its save, promotion and package-publish doors are where the BREAKING behaviour can be seen. `.changeset/pre.json` is absent on `origin/main` (read at `8fc50b7647`, 2026-10-08T01:54Z, at `ef1fcb26a2`, 2026-10-08T02:40Z, and at `7ef50a4fbb`, 2026-10-08T03:39Z), so the bump is `minor` with the BREAKING banner, as in passes 1 and 2. The changeset says it supersedes the earlier objectstack-ai#22032 entries' line that option `visibleWhen` is not judged at this door. ## Merges - **`06d3cad963`** merges `origin/main` `ef1fcb26a2` (PR objectstack-ai#22103), through `scripts/pm/os-regen-merge.sh`. It was clean. - **`3c1d3262ee`** merges `origin/main` `7ef50a4fbb` (parents `06d3cad963` and `7ef50a4fbb`), through `scripts/pm/os-regen-merge.sh`. That brought PR objectstack-ai#22129 (objectstack-ai#21982), PR objectstack-ai#22094, PR objectstack-ai#22134, PR objectstack-ai#22133 (objectstack-ai#22118, the gate's object-write baseline keeps the written item's stored self), PR objectstack-ai#22126 and PR objectstack-ai#22140. - `validate-expressions.ts` auto-merged. PR objectstack-ai#22129's edit is in the flow `script` / `subflow` region; the option loop's lift is unchanged. - One conflict: `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, one hunk. Both sides had added a new top-level `describe` block at the same place, after the pass-2 block. It was resolved by stacking: the pass-3 block first, then objectstack-ai#22118's stored-self block, and every line of both was kept. Against `7ef50a4fbb` the file shows only this branch's lines; against `06d3cad963` it shows only objectstack-ai#22118's 114 lines. - No generated path was touched by this branch. The rerun of the script took `origin/main`'s side of every generated path main moved, and that left nothing to commit. - This branch's own changes are the same before and after the merge: for each of the 9 files, the added and removed lines against the merge base are identical. The delta against `7ef50a4fbb` is 9 files, +467 / −45. - **The interaction with objectstack-ai#22118, measured.** objectstack-ai#22118 adds a third, stored-universe pass on an update into a context collection. A finding whose path names no entry is judged as one on the written item. The expression rule's paths are `where` strings, so an option finding is always judged that way. - A probe ran the real gate (`runRuntimeAuthoringRules`, type `object`) over 20 cases, against this branch's lint source before the merge (`06d3cad963`) and after it (`3c1d3262ee`). The cases are a create, an update over a stored self that is broken and over one that is clean, and a stored sibling that is broken, each for three refused bodies, plus two still-accepted bodies. - The verdicts are identical, case for case. Re-saving a broken option is still refused, including over a broken stored self, because re-saving a row is writing it. A clean save over a broken stored self passes. A broken sibling is never charged. - The pass-3 pins (differential, PARITY, LIT, CONTROL, CONTRAST) and objectstack-ai#22118's pins all pass at `3c1d3262ee`: lint 48 of 48 across the three files, and the protocol file 103 of 103. - This matches the code. The option pass reads only the written object's own field index, and binds `record` and `previous`, never `parent`, so the stored universe has nothing extra to offer it. ## Tests and gates (all at `3c1d3262ee`, the merge of `origin/main` `7ef50a4fbb`) - **`main` moved during the run (H6).** PR objectstack-ai#22103 landed as `ef1fcb26a2` and touched two files this pass edits, `authoring-rules.ts` and `runtime-gate.object-writes.test.ts`, in other hunks. It was merged with `scripts/pm/os-regen-merge.sh` as a merge commit. The merge was clean, and no generated path was taken from either side. After the merge: `pnpm install --frozen-lockfile`, a full turbo build (72 tasks), and `@objectstack/spec check:generated` ("All 15 generated artifacts are up to date"). objectstack-ai#22118 and objectstack-ai#21982 had not landed at that read (2026-10-08T02:40Z). Both have since landed, and they are merged at `3c1d3262ee` (see Merges). After that merge the same sequence ran: a full turbo build (72 tasks) and `check:generated` ("All 15 generated artifacts are up to date"). - **`@objectstack/lint`:** 125 files and 5729 tests passed. `typecheck` exit 0, with its test-typecheck included (`--listFiles`: the five touched or new lint test files are in the `tsconfig.test.json` program). - **`@objectstack/metadata-protocol`:** 221 files passed and 3 skipped; 28260 tests passed and 19 skipped. `typecheck` exit 0 (`--listFiles`: the door test file is in the program). - **Consumer readings.** Every package was built at the head named. - `@objectstack/objectql`, 8 files and 282 tests passed: `publish-package-drafts-response-conformance`, `save-meta-response-conformance`, `publish-meta-response-conformance`, `plugin.integration`, `engine-field-predicate-fault`, `engine-option-permission-predicate`, `validation/rule-validator.option-visibility` and `engine`. - `@objectstack/rest`, 11 files and 197 tests passed: every `meta-object-*` file and `meta-publish-package-scope`. - `@objectstack/cli`, 3 files and 16 tests passed, run as `--project integration` because the tier predicate puts them there: `validate-field-predicate-traversal` (which asserts an option `visibleWhen` `expression-invalid` finding), `authoring-rule-command-parity` and `verify-author-time-stage`. The three nightly-tier `*.e2e` files that assert `expression-invalid` are left to CI. - The search for other consumers covered every test file in the repository carrying `visibleWhen`, matched against the save-door entry points. Only the two packages above save an option `visibleWhen` through a door. - **Gates.** `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 63 commands, the same set at `23ce6c494c`, `06d3cad963` and `3c1d3262ee`. At `3c1d3262ee` all 63 exit 0, each exit code captured before any pipe. `--ran` reconciles 63 derived, 63 run, 0 NOT-MEASURED and 0 UNRUN, with an exit code recorded for each. The printed artifact-roster block names 51 families, and all 51 ran at `3c1d3262ee`, each with exit 0. That is 47 in the same battery, one (`check:engine-double-contract`) already among the 63, and the three PR-scoped ones (`check-partof-closing-keyword`, `check-closing-target-claim`, `check-single-claim-paths`) run with this PR's number and this body. The same 63 also ran at `06d3cad963`, all exit 0. At `23ce6c494c`, before the merge, the same 63 ran: 61 exited 0 on the first run. `check:dual-build-cjs-loads` and `check:lean-entry-closure` first exited 3 (PREREQUISITE NOT MET: no full build) and exited 0 after the full build. - **ESLint, narrowed to the 8 touched TypeScript files** (`--no-inline-config --format json`): 8 files, 0 errors and 0 warnings. Each file is matched by `eslint.config.mjs` (`--print-config`), and none was ignored. The config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move the verdict on any untouched file. ## Acceptance notes - **Out of this pass, reported for the seat: an option `visibleWhen` reading `parent` gets no verdict at the build, so none at the door either.** - Measured at `23ce6c494c` with scratch tests that were deleted afterwards. Through the real `saveMetaItem`, an object whose option carries `visibleWhen: "parent.status == 'closed'"` saved with success, and the row landed `active`. `runAuthoringRules('build')` gave 0 findings. - The server's option check (`evaluateValidationRules`, authenticated caller, the option picked) then logged `failed to evaluate (authenticated caller) — allowed through`, with `Unknown variable: parent`, and admitted the value. The option evaluator binds `record`, `previous`, the user and permissions only. - This is a gap in the build, which the door now mirrors. This card's contract is parity with the build, so it is not changed here. - **A code comment names an old spelling.** The comment above the option loop calls the showcase's legal usage `'admin' in current_user.positions`. The showcase now writes `'org_admin' in current_user.positions`, and the pins use that spelling. The comment is not changed here: per the contract review, it rides pass 4. - **The pending objectstack-ai#22032 changesets.** Pass 1's and pass 2's changesets each list option `visibleWhen` under "Unchanged", which was true at their heads. This pass's changeset says it supersedes that line rather than editing them, the same way pass 2 left pass 1's changeset alone. Per the contract review, reconciling those lines rides pass 4. - `formulas.mdx` could name the object save door. That would be a docs addition, not a correction of a false line. - **Contract review.** Triage's grade asks for one per pass. A PASS is on record for head `06d3cad963` (`6051573476`). The head has since moved to `3c1d3262ee` by the merge above, so the review for this head is the seat's. --- _Generated by [Claude Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…its it can prove, you apply the rest (objectstack-ai#22142) Fixes objectstack-ai#9591 Clause-②: no (prescription text only; no input's accept or reject result changes) This is the spec-lane half of the card: the shared retirement sentence names `--write`, and the class-wide pin moves in the same PR. The codemod itself landed in PR objectstack-ai#22108 (`a959493cdf`). ## The sentence Before (the objectstack-ai#9529 wording): ```text Run `os migrate meta --from N` to list the mechanical edits for existing sources; apply them by hand. ``` After: ```text Run `os migrate meta --from N` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. ``` The one allowed two-clause variant (a conversion that covers only part of a value) carries the same clause: `… to list the mechanical edits for the X case; --write applies the ones it can prove, and WHAT-HAPPENS-TO-THE-REST.` Its two members are dashboard `compareTo.offset` and the script node's `config.actionType`. **Checked against the tool on `main` (`51290bca`).** `packages/cli/src/commands/migrate/meta.ts` declares `write: Flags.boolean({ … default: false, exclusive: ['stored'] })`. Its help text says it rewrites the authored sources in place "for each mechanical change traced to one literal in one project file; every other change is listed with the reason it was not written". Without the flag the run writes only the `--out` snapshot. The wording satisfies every ruling that binds it: - **Triage `6045697201`.** The sentence never says "rewrite existing sources automatically" unqualified ("the ones it can prove"), and it names `--write` because the default run still only lists. - **"It must be TRUE of the tool."** Every clause is a property of the command, read from `meta.ts`. - **"One antecedent."** "existing sources" still names one thing. "The ones" can only be edits, because an edit is what gets applied. The key's fate stays in the body prose. - **Vocabulary.** The wording matches PR objectstack-ai#22122's skill text ("lists the mechanical edits"; `--write` "rewrites in place each edit it can trace to one literal in one project file, lists every other with the reason it was not written"). ## Where it moved (counted at `017761f0`; the merge of `main` added no site) - **161 sentences the pin judges.** `packages/spec/src`: 157 (155 house form, 2 two-clause). `packages/lint/src`: 1. `packages/drivers/driver-turso/src`: 3. Every one passes the new anchors; `apply them by hand` survives only in the pin's own RED fixtures. - **Not judged by the pin, moved anyway:** - `migrations/registry.ts`: 3 sentences, regenerated from the moved `entries/semantic/18.*.ts` by `gen:migration-registry`. - The lint `chartConfig.xAxis.field` hint in `validate-widget-bindings.ts`: a template literal with interpolation after the sentence, so the pin cannot see it (Acceptance notes). - The `retiredKey()` docblock example. - **Mechanical replacement.** A script replaced the tail `apply them by hand` in 63 files (188 occurrences; old tail left: 0) and printed per-file before/after counts. Two seams split mid-phrase (`translation.zod.ts`) and the two two-clause sites were rewritten by anchored edits that had to hit exactly once. - **Pins in other test files.** 23 test files asserted the old sentence verbatim, as string or regex. Each now asserts the new sentence verbatim, so a revert reds them. The ones that assert only the unchanged prefix (`form-layout-inline-grid-retired.test.ts`, the turso / driver-memory `toContain('os migrate meta --from 17')`) are untouched, because they stay true. - **Changeset.** `.changeset/9591-retirement-sentence-write.md`: `patch` for `@objectstack/spec`, `@objectstack/lint` and `@objectstack/driver-turso`, the three packages whose shipped text moves. - **Generated.** `content/docs/references/**`: 32 files (+268/−268) from `pnpm --filter @objectstack/spec check:generated --fix`; `check:docs` was the only stale artifact. `check:generated` then exited 0. ## The class pin (`retired-key-migrate-sentence.test.ts`) - **Anchors.** `HOUSE_AT_MARKER` and `MIXED_AT_MARKER`, and their markdown twins, require the new clause. The objectstack-ai#9529 sentence, which does not name `--write`, is now RED. Two further spellings are RED: one that names `--write` without the qualification, and one that qualifies it but leaves the rest unowned. - **Withdrawn claim.** `WITHDRAWN_CLAIM` is unchanged: the unqualified automatic-rewrite claim stays a hard RED everywhere. A new non-vacuity case proves neither legal shape trips it. - **Truth anchor (new).** The pin reads `os migrate meta`'s own flag table. A `write` boolean flag must exist and must have `default: false`, the two facts the sentence rests on. The read is covered by `@objectstack/spec`'s existing `packages/**/*.ts` cross-package declaration. - **Corpus widened by one root.** `packages/drivers/driver-turso/src` joins, on objectstack-ai#7030's terms. Its three `turso` config tombstones carry the house sentence, and their docblock defers to `retired-key.ts`, but the pin never walked them. Without this, a rewording leaves them behind with every assertion green. The lint-only anti-vacuity case now covers each widened corpus. - **Header and docblock.** The pin header and the `retired-key.ts` module docblock record the new sentence and why. The "the claim may be restored" note is gone, replaced by what was restored and how far. **Reverse verification**, run from committed HEAD `017761f0` through `scripts/ablation-replace.mjs` (each anchor hit as declared and was restored to a blob equal to HEAD with `git diff HEAD` empty). Expected direction: red. | Mutation | Result | |:--|:--| | A. the three `turso.zod.ts` sentences back to the objectstack-ai#9529 wording (anchor ×3→0, blob `e25cca4d5508`→`a05fe3f09733`) | 3 failed / 12 passed, naming `driver-turso:spec/turso.zod.ts:63`, `:75`, `:82` | | B. `meta.ts` `write` flag `default: false` → `true` (blob `c036012c63c9`→`71acff21ef8c`) | 1 failed / 14 passed: "the sentence is TRUE of the command it names" | | C. `meta.ts` flag renamed `write` → `inPlace` (blob `c036012c63c9`→`29a8a9402284`) | 1 failed / 14 passed: "os migrate meta declares no `write` boolean flag" | Under the old corpora, mutation A would have stayed green, because driver-turso was in no corpus. ## One bounded fix on the same sentences: `CHATTER_POSITION_RETIRED` The three `record:chatter` / `record:discussion` `position` value prescriptions (`'sidebar'`, `'inline'`, `'drawer'`, in `ui/component.zod.ts`) told the author to run a bare `os migrate meta`. The command refuses that with `Missing required flag --from` (`meta.ts` `run()`, the `flags.from === undefined` branch). The conversion is `record-chatter-position-vocabulary`, `toMajor: 18`, so they now name `--from 17`. They therefore join the pin's judged set in house form, and the "(registered under protocol major 18)" aside goes. The fix qualifies as bounded: the same sentence class, a mechanical change to an already-pinned form, a file inside this claim's surface, and the same gate family. No test pinned the old text. ## Governed surface: `.claude/skills/spec-property-retirement/SKILL.md` (Tier S) The pin requires the retirement playbook to teach both shapes (`SKILL_HOUSE_TEMPLATE` and `SKILL_MIXED_TEMPLATE` must match its convention 5). So changing the sentence forces the playbook edit, and this PR lands as Tier S. Convention 5 now carries the two new templates. Its note that the command "never writes a source file" was made false by PR objectstack-ai#22108, so it is deleted. Line count 337 → 337 (ceiling 337), with every line within the 120-byte budget: `node scripts/pm/check-skill-line-ratchet.mjs` exits 0. ⛔ No published `skills/**` file changes: PR objectstack-ai#22122 owns `skills/objectstack-upgrade/SKILL.md`, and no published skill carries the sentence (`git grep` count 0). ## 维护者速读(草稿) **改了什么**:所有退役键报错末尾那句统一提示,从「运行 `os migrate meta --from N` 列出机械修改,然后手工改」改为「……列出机械修改;`--write` 会写入它能证明的那些,其余你手工改」。共 161 处被 pin 判定的报错文案(含 2 处两从句变体),外加生成的 registry 3 处、lint 模板字符串 1 处;其中 3 处原先写成不带 `--from` 的命令(该命令会直接拒绝),一并改正。守这句话的 pin 同步更新,并新增一条断言:CLI 必须真有 `--write` 且默认不写。 **为什么改**:`--write` 已随 PR objectstack-ai#22108 落地,旧句只说「手工改」,低估了工具;但 `--write` 只写能证明的站点,所以不能说「自动重写源文件」。新句两头都如实。 **风险与代价(含回滚)**:纯文案,不改任何 schema、键、类型、导出或错误码;解析结果不变。依赖旧整句原文匹配的调用方会失配(仓内 23 个测试已同步);前缀「…for existing sources;」不变。回滚即 revert 本 PR。在途的兄弟 PR 若新增处方仍用旧句,会被 pin 打红,后落地者改用新句。 **席位意见**: **你要做的**:无需操作;本 PR 触 `.claude/**`(Tier S),由席位按合同审查记录落地。 ## Verification (branch base `51290bca`; final head `f9ca14d548`) - `pnpm --filter @objectstack/spec build`: VERDICT command-exit 0. `check:generated --fix` regenerated the one stale artifact; `check:generated` then exited 0 (15 of 15 current), and again in the gate run at `f9ca14d548`. - spec `vitest run --project local`: Test Files 623 passed (623), Tests 18613 passed, 1 todo, at `017761f0`. - spec `vitest run --project repo` (53 files incl. the class pin): 53 passed (53), Tests 903 passed (903), at `017761f0`. - `@objectstack/driver-turso` `vitest run`: Test Files 88 passed (88), Tests 2373 passed, 33 skipped, at `017761f0` (after `pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' --filter '@objectstack/driver-turso...' build`; the first run, before that build, could not resolve unbuilt dependencies and is NOT MEASURED, not red). - `typecheck` for spec (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`), lint and driver-turso: exit 0 at `017761f0`. - **After merging `main`** (`033e5c536d`: objectstack-ai#22122, objectstack-ai#22127, objectstack-ai#22106) as `f9ca14d548`, with no conflict (objectstack-ai#22127 also edits `validate-expressions.ts`): the class pin 15 passed (15); `@objectstack/lint` `vitest run`: Test Files 123 passed (123), Tests 5688 passed (5688); lint `typecheck` exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `f9ca14d548` derives 124 commands (the claim-time 79 plus 45). All 124 exit 0 at `f9ca14d548`. `--ran` reconciliation: 124 derived, 124 run, 0 NOT-MEASURED, a zero derived from the recorded exit codes. (At `017761f0`, five gates first answered exit 3, PREREQUISITE NOT MET: one shallow-clone fixture and four that need unbuilt dists. The clone was deepened as the gate asked, and all five are green in the `f9ca14d548` run.) - `node scripts/pm/check-skill-line-ratchet.mjs`: exit 0; the playbook is 337 lines (ceiling 337), and no line is over 120 bytes. - eslint, narrowed: `pnpm exec eslint --no-inline-config --format json` over the 66 changed `.ts` files reports 66 files, 0 errors and 0 warnings. The population is `eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` glob, which excludes the changed `.md` / `.mdx` files. The config never enables type-aware linting (its own comment at `:326`–`:328`), so this diff cannot move any untouched file's verdict. The repo-wide `pnpm lint` is CI's. ## Siblings in flight objectstack-ai#21982, PR objectstack-ai#22094 (objectstack-ai#13458) and PR objectstack-ai#22103 (objectstack-ai#5082) each add prescriptions with today's sentence. Whichever lands after this one carries the new sentence; the class pin reds it at that merge otherwise. Whichever of those lands first, this branch merges `main` before landing. ## Acceptance notes - **Hand-written docs still use the old sentence** (`content/docs/automation/flows.mdx` ×2, `protocol/objectql/query-syntax.mdx`, `data-modeling/queries.mdx`, `protocol/objectui/actions.mdx`, `ui/apps.mdx` ×2). Each is the page's own advice, not a quoted error, and still true of the default run; each undersells `--write`. They are `domain:devx` pages outside this claim, so they are not touched here. - **QA checklist item `cli.migrate-meta-codemod`** (`docs/qa/platform-checklist/areas/cli.json`). Its RESTART CHECK fired when PR objectstack-ai#22108 added `--write`, and the item still asserts a print-only command. Its step 1 greps for the objectstack-ai#9529 sentence verbatim and now finds none. Re-authoring the item belongs to the checklist author, not this PR. - **Comments that say the default run "lists the mechanical edits"** stay as they are, because they are still true: the `migrations/registry.ts` migration notes (outside the pin's scope by design) and the `conversions/registry.ts` comments. - **The lint `chartConfig.xAxis.field` hint** (`validate-widget-bindings.ts`) moved, but it remains invisible to the class pin: a template literal, with `suggestName(…)` and the suppress hint interpolated after the sentence. - **A published skill still claims an automatic strip.** `skills/objectstack-data/rules/indexing.md:31` says "run `os migrate meta --from 16` to strip them automatically", and `skills/objectstack-data/SKILL.md:377` says the command "strips them". The default run strips nothing from sources, and `--write` strips only what it can prove. `WITHDRAWN_CLAIM` has no strip spelling, so the pin cannot see this. Widening it here would red `main` on a Tier H file this PR may not touch, so it is reported to the PM for the skills lane. - **The `config.actionType` two-clause tail** ("the stub and marker values are removed") is unchanged in substance; only the `--write` clause was inserted before it. ## Patch round 1 (written by the PM seat from the dev's report `6052088494`) - **Merge:** `origin/main` `ef1fcb26a2` (PR objectstack-ai#22103) was merged through `os-regen-merge.sh` as `feca6b5ace`. The three reference pages both sides had changed (`api/metadata`, `data/object`, `system/migration`) were regenerated from the merged tree as `98e2f6e373`. That brings back objectstack-ai#22103's `unique?: false | 'global' | 'organization'` rows, which the driver had dropped. - **objectstack-ai#22103's sites:** the merge brought two non-test sites with the old tail and one test that asserts it verbatim. All three carry the new sentence at `b9d6e82619`: - `packages/spec/src/data/object.zod.ts` (`DECLARED_INDEX_BARE_TRUE_RETIRED`); - `packages/lint/src/data-model-rules.ts` (the `unique-unscoped-declared-index` fix text); - `unique-scope-message.test.ts`. The pin now judges 163 sentences: `spec` 158 (156 house + 2 two-clause), `lint` 2, `driver-turso` 3. - **The pin's blind spot:** the `data-model-rules.ts` sentence sat in a template literal, which the judge cannot read (escaped backticks), so it was never judged. It is now plain-quoted, as in `validate-expressions.ts`, and the pin's Mechanism paragraph records that template literals are invisible to the scan. Ablation D (that sentence back to the old tail) gives 3 failed / 12 passed, naming `lint:data-model-rules.ts:463`. `validate-widget-bindings.ts` stays the one template-literal site the pin cannot judge (an Acceptance note). - **Verification at `b9d6e82619`:** - spec `--project local`: 623 files / 18,619 tests; - spec `--project repo`: 53 / 903; - lint: 123 / 5,689; - driver-turso: 88 / 2,373; - typecheck: exit 0 for all three packages; - `dispatch-gates --ran`: 124 derived / 124 run / 0 NOT-MEASURED; - CI: 33 success, 2 expected skips. ## Patch round 2 (written by the PM seat from the dev's report `6053397952`; claim revised `6052335087`) - **Why:** PR objectstack-ai#22094 (objectstack-ai#13458, `fec87e7e07`) landed first with a two-clause prescription lacking the `--write` clause, which this PR's class pin refuses. The sibling rule here ("whichever lands later carries the new sentence") puts the edit in this PR. - **Merge:** `origin/main` `959c209d56` was merged through `os-regen-merge.sh` as `3b6335b9be`, with no hand-written conflict. `content/docs/references/api/protocol.mdx` was regenerated as `c40b3babd7`. - **The edit** (`fe3af5642c`, 4 files beyond the merge): - `packages/spec/src/kernel/manifest.zod.ts` `PLUGIN_PERMISSIONS_LIST_FORM` now closes with "Run `os migrate meta --from 17` to list the mechanical edits for the package manifest case; `--write` applies the ones it can prove, and a granted-permission record is not a source it reads." It keeps objectstack-ai#13458's own second clause and adds the house `--write` clause, the seat's wording. - Its verbatim pin `manifest-permissions-string-list.test.ts` moved with it. - The changeset's two-clause bullet now names three members and says "before their second clause". - **The two-clause variant now has three members:** dashboard `compareTo.offset`, the script node's `config.actionType`, and the package manifest `permissions` case. The pin judges 164 sentences (spec 159 = 156 house + 3 two-clause; lint 2; driver-turso 3) with 0 bad sites. - **Verification at `fe3af5642c`:** - spec `--project local`: 625 files / 18,661 tests; - spec `--project repo`: 53 / 903; - class pin: 15 / 15, and the manifest pin: 17 / 17; - `dispatch-gates --ran`: 124 / 124 / 0 NOT-MEASURED; - CI: 33 success, 2 expected skips. --- _Generated by [Claude Code](https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #13458
Clause-②: no (narrowing)
Phase 2 of the plugin-permissions ruling (option A, maintainer 「同意」, 2026-08-30, as recorded in #13458's body): the legacy flat
string[]arm ofManifestPermissionsSchemais retired by the standard retirement route (ADR-0049 enforce-or-remove, an ADR-0087 conversion, the spec-property-retirement playbook). The structured ADR-0025 §3.2 block{ services, hooks, network, fs }is now the only form a package manifest'spermissionstakes. Triage's corrections (6018679144) were applied as binding: there was no legacy reader to migrate. The two drop reports are reworded, and the liveness row is re-verdicted.What changes
packages/spec/src/kernel/manifest.zod.ts).ManifestPermissionsSchemais nowPluginPermissionsSchema, by identity. Both export names stay, and the api-surface is unchanged. The block answers a list on its own error map:Expected the plugin permission block { services?, hooks?, network?, fs? }, received a flat list., followed by the prescription. This is the bare-array patternListViewExportOptionsSchemaalready uses, built from the same exportedstrictObjectError+closedObject, with theprimehandle forwarded. The block also carriesEnvironmentArtifactSchema.grantedPermissionsvalues, so the answer is worded true on both carriers. Itsos migrate metasentence uses the house two-clause form, which names the case the conversion covers. The manifest's YAML example and the key's TSDoc and.describe()lose the list.manifest-permissions-string-list-removed(conversions/registry.ts,MAJOR_18_CONVERSIONS, order 61, inserted where its identifier sorts and defined above the entry that follows it). It is retired from the authoring load path, withretiredAfter: '17.7.0'. It deletes an all-string list from the stack'smanifestand everypackages[].manifest, and the notice carries the dropped strings. It never touches the structured block, an array of objects, the top-level ADR-0090 collection, ordevPlugins[], whose entries may be live plugin objects.manifest-permissions-string-list-retired(migrations/entries/semantic/, generated into the registry) and itsSTEP18_RATIONALEfragment (order 85).packages/spec/liveness/manifest.json).permissionsstayslive, on corrected evidence, and its four keys are drilled. See the census below. TheREADME.mdmanifest row gains one sentence, andstate-counts/manifest.mdwas regenerated (live 23 → 26).packages/runtime/src/app-plugin.tsandpackages/plugins/plugin-security/src/suggested-audience-bindings.tsnow name the flat list as the RETIRED legacy form, refused at parse. Classification, dedupe, log level and thearmlabels in the meta are unchanged, so their existing tests pass untouched. No other file in those two packages moved.content/docs/protocol/kernel/plugin-spec.mdxsaid the key "accepts either the legacy flatstring[]or" the block. That sentence was made false here and is corrected. The twostack.zod.tsdocblocks that described the list are corrected too.content/docs/references/**was regenerated.authorable-surface/kernel.jsongainskernel/ManifestPermissions:{fs,hooks,network,services}, because the identity alias now emits the block under its own def key.authorable-surface.base.jsonwas not touched.Measurements behind the premise (
origin/maine67ba80049)H1, reader census: no in-repo consumer reads the legacy arm. CONFIRMED. The census covered every spelling of a
manifest.permissions/manifest?.permissions/manifest[...]read,manifestSourcereads, and everyManifestPermissions*/PluginPermissions*importer. In-repo it finds:collectDeclaredSuggestions→classifyManifestPermissions;AppPlugin'sSECURITY_FIELDSloop). Both read the ASSEMBLEDPermissionSet[]reading and only REPORT an authoring-stage value as skipped (permissions一个键承载两个互不兼容的语义,且共用 registry 同一存储槽 —— ADR-0025 授权 vs ADR-0090 权限集集合 #18031);permission-set-name-collisionstest, an assembled-stage read;artifact-granted-permissions.ts,plugin-permission-enforcer.ts), which imports thePluginPermissionstype forgrantedPermissionsand never reads the manifest.Zero consumers act on the list. Control: the same instrument finds the two drop reports and the assembled-stage read, so the zero is about the key's readers and not about the pattern.
os plugin buildonly parses the manifest.os plugin publishuploads it whole asplugin_manifest.objectui pin
a58626c88dc8: 0 reads ofmanifest.permissions, against 91manifest.(id|name|version)reads as the control. Its only reader of the structured block is the marketplace install disclosure (PluginDisclosure), which readsservices/hooks/network/fsand shows "Requests no special permissions." when none is present. So the retired list was disclosed to an installer as requesting nothing.H2, conversion vs disposition: chose a STRIP conversion, registered. A capability string such as
system.user.readnames no service, hook, host or path, and no table maps one onto the four lists. So no conversion can carry the author's intent, and inventing grants in a consent request would be worse than dropping them. The list had zero readers, so the delete changes no load, grant or refusal: lossless for every runtime outcome. The conversion still has to exist, because a built artifact that carries a list would otherwise be refused at the artifact door. The judgement the delete cannot make is the D3 entry's.Authored census, by heuristic text scan with a lit control (it finds the five fixtures below): no manifest in
examples/,apps/,packages/,skills/orcontent/docs/writes a list. The exceptions are five@objectstack/specmanifest.test.tsfixtures, re-triaged here: one re-spelled to the block, three translated, and one deleted because an app ships no code that needs a grant. The other exception is the drop-report tests in runtime and plugin-security, which hand a list to an unparsed bundle on purpose and still pass.docs/design/marketplace-publishing.mdshows a list at the top level ofdefineStack, which is already the permission-set collection. It is noted below and was not edited.H3, the structured arm's accept set did not move. CONFIRMED. The error map only rewords the type error a non-object already raised. Pinned in
manifest-permissions-string-list.test.ts: every declared key alone, the full block and{}parse, and an unknown key and a non-string member are still refused. Themanifest-unknown-keys.test.tsdoor tests keep their exact rename and alias content. Only the envelope moved: the issue is now the block's ownunrecognized_keysat['permissions'], with noinvalid_unionaround it. Load-time enforcement of the block (it still refuses nothing) is not this card's and is unchanged.Liveness re-verdict. The row's old evidence (
collectDeclaredSuggestionsreading the legacy arm) was the misattribution #18031 corrected. The new evidence is the objectui install disclosure, with produceros plugin publish. The cloud hop, publish to the version row the disclosure reads, is NOT MEASURED:add_repoon cloud was denied from this seat. The note asks a cloud-capable seat to re-read and pin it. The re-verdict islive, notdead, because authoring the block changes what an installer is shown. It is not enforced at load, and the note says so.Changeset grade
mainhas no.changeset/pre.json: absent ate67ba80049, and re-read absent atorigin/main3d9188502ejust before opening. Changesets pre mode is not in, so per triage's release-state note (6038073833) this shipsminorwith its BREAKING banner and the ADR-0087 dispositionregistered manifest-permissions-string-list-removed, manifest-permissions-string-list-retired.@objectstack/runtimeand@objectstack/plugin-securitycarrypatchfor the reworded log text (same fixed group). The prescription names the bare published major, "removed in @objectstack/spec 17". That is whatcheck:future-spec-majorrequires of an unshipped pre-GA retirement, which shipsminorin the current major. A first draft said 18.0.0, and that gate refused it.Verification
All commands run at head
55481fe9ae, unless a line says otherwise. Exit codes were captured before any pipe.pnpm --filter @objectstack/spec build(JS + DTS) → 0. The dependency closures of runtime and plugin-security (--filter 'PKG^...', 31 packages) → 0.--filter '@objectstack/client-react...'→ 0 (prerequisite forcheck:skill-examples).pnpm --filter @objectstack/spec check:generated→ 0. All 15 generated artifacts are up to date against a dist built from this head (declaration stamp match). That coverscheck:liveness,check:api-surface(exports unchanged),check:authorable-surfaceandcheck:docs.@objectstack/spectests: the full--project localrun → 622 files / 18536 passed (at71735b5bc0; the later commit changes only the prescription's version word and its pin). Targeted at the final head:src/kernel/ src/conversions/ src/migrations/plus the migrate-sentence pin → 69 files / 1828 passed.--project reposcripts/step18-rationale-merge.test.ts scripts/conversions-major18-merge.test.ts→ 2 files / 21 passed. The newmanifest-permissions-string-list.test.ts→ 17/17.pnpm --filter @objectstack/spec typecheck(tsc + scripts + test layer) → 0.@objectstack/plugin-securityvitest → 169 files / 3640 passed.@objectstack/runtimevitest--project local→ 332 files / 4693 passed.typecheckof both (tsc +check:test-typecheck) → 0, at the final head.packages/runtime/srcassigned a string list toManifestPermissionsand toObjectStackManifest['permissions'], plus a structured block as a control.tsc -p packages/runtimeagainst the rebuilt spec.d.ts→ exit 2, exactly two TS2559 errors on the two list lines, the block line clean. The probe was removed andgit statusread clean.node scripts/pm/dispatch-gates.mjs --commandsderived 121 families, re-derived identically after the last commit. 117 → 0.check:future-spec-major→ 1 at first (the 18.0.0 wording above), fixed, re-run → 0.check:skill-examples→ 3 (prerequisite) at first, re-run after building client-react → 0, with 262 prose examples type-checked. The changeset gates (check-adr-0087-registration,check-changeset-no-major,check-empty-changeset, each with--self-test) → 0.--ranreconciliation: 119 of 121 run.check:i18n, reason: its prerequisite is the CLI plus the build closure of every package whose extract config it runs (exit 3), and this diff touches no metadata form or translated label.check:dual-build-cjs-loads, reason: it needs every package'sdist/(37+ missing, exit 3). Both are left to CI.eslint --no-inline-config --format jsonon the 10 changed.tsfiles → 10 files linted, 0 errors, 0 warnings. That is the whole changed-TS population, every one inside eslint's own file globs, with none ignored.eslint.config.mjsnever enables type-aware linting (noparserOptions.project), so this diff cannot move the verdict of an untouched file. The repo-widepnpm lintis CI's.main: the branch is at basee67ba80049.maingained3d9188502eandd4680d2820, which touch no file in this diff (their generated shards are other categories). The PR's merge ref and the queue validate the joint state.Acceptance notes
ManifestPermissionsSchemasurvives as an identity alias ofPluginPermissionsSchema. Deleting the export would be a second breaking removal. It would also regenerateskills/objectstack-platform/references/_index.md, which is Tier H and outside this card. The cost is fourkernel/ManifestPermissions:*authorable-surface rows that duplicatekernel/PluginPermissions:*. Retiring the name is a separate decision.permissionsonto the marketplace version row. Re-reading it needs a cloud-capable seat, and the row's note says so.packages/runtime/src/app-plugin.manifest-security-collision.test.ts. Its header table still describesmanifest.permissionsas the legacy flatstring[]or the structured block, and a comment saysdefineStackrefuses an object array withinvalid_union; it is nowinvalid_typewith the list answer. The tests still pass, and the dispatch fenced every other file in those packages. Carrier: none.docs/design/marketplace-publishing.md(a design doc, not published) showspermissions: ['system.object.create', …]at the top level ofdefineStack, which was already the permission-set collection before this change, and a §7.4 reviewer snippet in the same flat form. It was not made false here and was not edited. Carrier: none.@objectstack/spec, a legacy-list plugin manifest is refused at its publish gate. A stored version row that holds a list is cloud's own data and was not measured from here.Generated by Claude Code