Skip to content

docs(skills): name the complete public-form opt-in the anonymous form endpoints read - #21650

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-21567-api-skill-public-form-optin
Oct 4, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-21567-api-skill-public-form-optin

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21567
Clause-②: no

What

The published objectstack-api skill's public-form section named two of the three sharing keys the anonymous form endpoints read, and the objectstack-ui skill's UI-assembly row repeated the omission. An AI following either authors a form that both GET /api/v1/forms/:slug and POST /api/v1/forms/:slug/submit answer 404 FORM_NOT_FOUND. Both now state the complete opt-in as the merged rule reads it, plus the walled-posture condition and its remedy.

Skill text only (skills/**, Tier H). Nothing under packages/** is touched; the shipped migration prose in packages/spec/src/migrations/** stays as written (triage ruling 5967394091, "Not here").

The rule the text now describes (read on origin/main 15fe567)

  • packages/spec/src/ui/sharing.zod.ts:95 — enabled: z.boolean().default(false).describe('Enable public sharing'); :102 — allowAnonymous: z.boolean().optional().default(false).
  • packages/metadata-core/src/anonymous-form-intake.ts:66-68 — the three checks (s.enabled !== true, s.allowAnonymous !== true, publicLink a non-empty string); :57-59 — publicFormSlug folds /forms/x, forms/x and x into one slug.
  • packages/rest/src/rest-server.ts:10807 and :10977 — both doors answer 404 FORM_NOT_FOUND whenever resolveFormBySlug returns null, which it also does for a form the posture withholds (:10781-10789).
  • packages/metadata-core/src/anonymous-form-intake.ts:212 — a wall is in force only on a posture postureEnforcesWall admits (group / isolated); :223-225 — the remedy text: declare tenancy: { enabled: false } on an object whose rows belong to no organization.
  • packages/metadata-protocol/src/runtime-authoring-gate.ts:404 — PUBLIC_FORM_INTAKE_UNAVAILABLE = 'public-form-intake-unavailable', a warning the authoring gate raises for a view write (save and publish).

Every skills/** site that describes the opt-in

Sweep at 15fe567: git grep -n -i over skills/ for allowAnonymous, publicLink, anonymous form, public form, forms/:slug, Web-to-Lead, web-to-case, guest_portal, walled.

Site (line numbers at 15fe567) Verdict
skills/objectstack-api/SKILL.md:77-78 — "Any FormView declared with sharing.allowAnonymous: true and a publicLink slug is auto-mounted at:" changed — names all three keys, the enabled default, and the 404 FORM_NOT_FOUND answer when one is missing
skills/objectstack-api/SKILL.md:94 — "with allowAnonymous / publicLink" changed — now enabled / allowAnonymous / publicLink
skills/objectstack-api/SKILL.md:85-94 — the paragraph after the route block changed — one sentence names the walled-posture condition, the public-form-intake-unavailable warning on save and publish, and the tenancy: { enabled: false } remedy
skills/objectstack-ui/SKILL.md:208 — the UI-assembly table's public-form row changed — the row now shows sharing: { enabled: true, allowAnonymous: true, publicLink: 'slug' }
skills/objectstack-data/SKILL.md:188 — "public forms" in the list of surfaces a field's conditional rule applies to already correct — names the surface, not the opt-in
skills/objectstack-data/references/_index.md:57, skills/objectstack-ui/references/_index.md:48 already correct — file pointers to sharing.zod.ts, no opt-in prose

No other hit; guest_portal and Web-to-Lead occur only inside the edited section.

Readings

File Lines before → after Tokens before → after (ceiling)
skills/objectstack-api/SKILL.md 428 → 434 (+6, the +6 net-line budget) 4634 → 4758 (6319; headroom 1561)
skills/objectstack-ui/SKILL.md 310 → 310 3854 → 3854 (3856; headroom 2) — bytes 15415 → 15415; the edited row is 179 bytes before and after
Bundle: sum of every skills/*/SKILL.md 4397 → 4403 —

Tokens are the ratchet's own convention, ceil(bytes / 4); the gate's lines at 4adec02: "skills/objectstack-api/SKILL.md is 4758 tokens (ceiling 6319; headroom 1561)" and "skills/objectstack-ui/SKILL.md is 3854 tokens (ceiling 3856; headroom 2)". The objectstack-ui row paid for its three keys inside the same line: the label "Public / anonymous form" is now "Public form" (the cell still carries allowAnonymous), and "/ Web-to-Case" and "Auto-exposed" are dropped (the API skill's section keeps "Web-to-Lead / Web-to-Case"). No other line of that file moved; the sibling card #21537 holds the subforms example at :72-85. origin/main was still 15fe567 when this PR was opened, so no merge was owed.

Tests

All 24 gate families that node scripts/pm/dispatch-gates.mjs --commands derives for this diff were run locally at 4adec025, each exit captured before any pipe. --ran reconciliation: "24 derived famil(ies) accounted for — 24 run, 0 NOT-MEASURED (a DERIVED zero — all 24 recorded an exit code and none of them is 3)". Verdict lines:

  • node scripts/check-skills-token-ratchet.mjs exit 0 — "✓ check-skills-token-ratchet: 54 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted."; --self-test exit 0 — "65 cases pass".
  • node scripts/check-doc-route-spelling.mjs --advisory exit 0 — "✓ route-spelling guard (advisory): population clean — every shape-matched literal spells its ledger row."; --self-test exit 0.
  • pnpm check:skill-identifier-liveness exit 0 — "Leg 1: 457 citation(s) over 53 published file(s) checked against 118365 implementation word tokens (0 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s)."
  • pnpm --filter @objectstack/spec run check:skill-docs exit 0 — "✅ Skill docs in sync" (after pnpm --filter @objectstack/spec build under the verify lock, VERDICT command-exit 0).
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — first run exit 3, PREREQUISITE NOT MET (@objectstack/formula and @objectstack/lint not built; "Nothing was measured"); after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the lock, exit 0.
  • pnpm check:corpus-claim-drift, check:doc-authoring, check:role-word, check:skill-compatibility, check:skill-frame-sync, check:nul-bytes, check:agent-test-spelling, check:cross-package-test-inputs, check:driver-memory-census, check:gitlink-declared, check:pm-governed-merges, check:refd-timer-probe, check:watch-hint-literal, node scripts/check-ci-filter-parity.mjs, node scripts/check-closing-keyword-parity.mjs (and --self-test), node scripts/check-comment-mask-corpus.mjs — all exit 0.
  • Control-character self-scan over both files (the grep -naP spelling from the agent rules): clean.

Not run locally, CI owns them: the repo-wide pnpm lint, the type-check lanes and the package test suites — no package source changed.

维护者速读(草稿)

改了什么: 两份对外发布的技能页(objectstack-api、objectstack-ui)里描述「匿名公开表单」开关的句子,原来只写了 allowAnonymous: true 和 publicLink 两个键。现在补齐第三个键 enabled: true(schema 默认是 false),写明三者缺一则两个匿名端点都答 404 FORM_NOT_FOUND,并加一句:在 group / isolated 这类带租户墙的部署姿态下,目标对象若按组织列隔离,表单同样不对外提供,保存与发布时会收到 public-form-intake-unavailable 警告,补救是对不归属任何组织的对象声明 tenancy: { enabled: false }。

为什么改: 运行时规则已在 PR #21566 落地到 main:三个键齐全才服务。技能文本是 AI 写元数据的直接依据,按旧文本写出来的表单会被两个端点同时拒绝,而作者不知道为什么。本 PR 让文本与已合并的规则、与 SharingConfigSchema 的默认值完全一致。

风险与代价(含回滚): 纯文本改动,不碰 packages/**,无 changeset(skip-changeset)。objectstack-ui/SKILL.md 的 token 棘轮只剩 2 的余量,所以那一行用同一行内的删字付账(行标签「Public / anonymous form」改为「Public form」,去掉「/ Web-to-Case」与「Auto-exposed」);objectstack-api/SKILL.md 净增 6 行,在 PM 给的 +6 预算内。回滚:revert 本 PR 即可,没有派生产物。

席位意见:

你要做的: 这是 Tier H 受管面(skills/**),需要你的 APPROVED review;批准后由席位落地。请顺带看一眼 objectstack-ui 那一行的措辞取舍是否接受(标签缩短、去掉 Web-to-Case)。

Acceptance notes

  • Noted, not filed (code comments, not authoring guidance; carrier: none): examples/app-showcase/src/ui/views/inquiry.view.ts:10 and examples/app-showcase/src/data/objects/inquiry.object.ts:10, and the header comments of packages/qa/dogfood/test/showcase-public-form.dogfood.test.ts:5 and showcase-public-form-withdrawal.dogfood.test.ts:7, narrate the opt-in with the two-key wording; the example view itself declares all three keys (inquiry.view.ts:70-73), as does examples/app-crm/src/views/lead.view.ts:124-126.
  • CHANGELOG.md:607 carries the pre-rule wording — release-owned history, never edited in a code PR.
  • content/docs/ui/forms.mdx:23 and :234 already name sharing.enabled: true + sharing.allowAnonymous: true + publicLink; nothing owed there.
  • The slug-normalisation fact (/forms/x, forms/x and x are one slug) is not in the skill text — it did not fit the +6 line budget; both example apps spell publicLink: '/forms/contact-us', which the UI row's publicLink: 'slug' placeholder admits.
  • The commit's author/committer name is objectstack-fleet[bot] (set with -c user.name on this one commit; the container default is Claude); the trailer pair is the model-free form the pre-push hook accepted.

Generated by Claude Code

… endpoints read

The objectstack-api public-form section and the objectstack-ui assembly row named
two of the three `sharing` keys the anonymous form endpoints require. Both now
name `enabled: true` (schema default `false`), `allowAnonymous: true` and a
`publicLink` slug, the `404 FORM_NOT_FOUND` answer when any one is missing, and
the walled-posture condition with its `tenancy: { enabled: false }` remedy.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Oct 4, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 4, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4adec0257ce8917a63f03615b73a46be59329676
Local-runs: none

Read-only shape: the diff against the merge base 15fe567, card #21567 with every comment, and this head's check-runs; nothing built or run locally. Reviewed by the dispatch seat in seat (served tier equals the constant's value, read from get_session). Review face: skills/**, governed rule text (Tier H). Readings taken at 2026-10-04T00:14Z.

① Derived judgments

  • Accept set: unchanged. The diff is prose in two published skill files (skills/objectstack-api/SKILL.md +10/−4, skills/objectstack-ui/SKILL.md +1/−1); no schema, export, error code, route or runtime behaviour moves. Clause-②: no holds, and the direction of the text change is a narrowing of what an author is told works (two keys → three keys plus a posture condition), never a widening.
  • Statements the diff adds, each checked against origin/main 15fe567: sharing.enabled defaults to false (packages/spec/src/ui/sharing.zod.ts:95); the doors serve a form only when enabled, allowAnonymous and a non-empty publicLink all hold (packages/metadata-core/src/anonymous-form-intake.ts:66-68); both doors answer 404 FORM_NOT_FOUND (packages/rest/src/rest-server.ts:10807, :10977), and a form withheld on a walled posture is resolved to null before them (:10778-10789); the walled condition and its remedy sentence (anonymous-form-intake.ts:212, :223-225); the warning name raised for view writes (packages/metadata-protocol/src/runtime-authoring-gate.ts:404, :449, :1168). Correct, every one.
  • Scope: the claim's two files only; nothing under packages/**; the shipped migration prose stays as triage ruled.

② Semver level

  • No released package publishes from this diff (skills/** ships by npx skills add, outside every package files[]). No changeset owed; skip-changeset is the correct declaration. No ADR-0087 disposition applies.

③ Boundary flags

Implemented-by: claude/issue-21567-api-skill-public-form-optin
Reviewed-by: session_01CB6W87z22K2yjUCDyVrJRk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— PR #21650(#21567)· skills seat 1 · 2026-10-04T00:27Z

改了什么: 两份对外发布的技能页里描述「匿名公开表单」开关的句子。objectstack-api 的公开表单一节现在写全三个键:enabled: true(schema 默认 false)、allowAnonymous: true、publicLink slug,缺一则两个匿名端点都答 404 FORM_NOT_FOUND;另加一句:带租户墙的部署姿态(group / isolated)下,按组织列隔离的对象上的表单同样不对外提供,保存与发布会收到 public-form-intake-unavailable 警告,补救是对不归属任何组织的对象声明 tenancy: { enabled: false }。objectstack-ui 的 UI 装配表那一行同步写全三个键,并在同一行内删字付账(该文件 token 棘轮余量只有 2)。

为什么改: 运行时规则已在 PR #21566 落到 main(三个键齐全才服务);技能是 AI 写元数据的直接依据,按旧文本写出的表单会被两个端点同时拒绝而作者不知道为什么。本 PR 让文本与已合并的规则、与 SharingConfigSchema 的默认值一致,并同步 #21476 落地的 walled 条件。

风险与代价(含回滚): 纯技能文本,不碰 packages/**,无 changeset(skip-changeset);objectstack-api/SKILL.md +6 行(派发预算内),objectstack-ui/SKILL.md 行数与 token 均不变。席内契约复核 PASS(5974907048),必查门禁 Lint & Repo Gates 与 TypeScript Type Check 已绿。回滚 = revert 本 PR 的单个 commit。

席位意见: 建议批准。每一句都对照 sharing.zod.ts:95、anonymous-form-intake.ts:66-68 / :212 / :223-225、rest-server.ts:10778-10789 / :10807 / :10977、runtime-authoring-gate.ts:404 核过。UI 行把标签「Public / anonymous form」缩为「Public form」、去掉「/ Web-to-Case」与「Auto-exposed」,是为了在 2 个 token 的余量里付账;语义不丢(单元格仍写 allowAnonymous,API 页保留 Web-to-Lead / Web-to-Case)。与 PR #21651(#21537)共用 skills/objectstack-ui/SKILL.md 但区域不相交,先批哪个都可以,席位串行落地。

你要做的(一个动作): 在 PR #21650 上给一次 APPROVED review;批准后由席位清标、ready、挂 auto-merge 入队。

@os-zhuang
os-zhuang marked this pull request as ready for review October 4, 2026 01:08
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 1a23054 Oct 4, 2026
44 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-21567-api-skill-public-form-optin branch October 4, 2026 01:32
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…nd an unknown --object is refused (objectstack-ai#21662)

Fixes objectstack-ai#21644

Clause-②: no

A deployment-level flag is now written only by a full-scope run. `os
migrate value-shapes` and `os migrate files-to-references` narrowed by
`--object` apply their fixes, record no deployment flag, and say so. A
full-scope `--apply` records the flag exactly as before. Across the
family (`value-shapes`, `files-to-references`, `summary-nulls`,
`duplicates`), an `--object` name the deployment does not declare is
refused with `OBJECT_NOT_FOUND` before anything is read or written. The
refusal names the unknown name and the declared objects. This follows
triage ruling `5974774596`. `--apply --object` is not refused.

## Measured first (base `759dbe9ed3`)

### A1. The reach, at the public door (hypothesis confirmed)

A throwaway SQLite project held three objects, one of them
`os21644_site` with a `location` field. A served-shape boot seeded one
clean row per object. Then one off-shape value was written past the
write path: the site's `geo` stored as `{latitude, longitude}`. The
fresh-datastore attestation had recorded both ADR-0104 flags as verified
at birth, so the flag table was emptied first.

- `os migrate value-shapes --json` exited 1, with `gatePassed: false`
and `blocking: 1`.
- `os migrate value-shapes --object os21644_sitee --apply --yes --json`
(misspelled) exited **0**. It answered `gatePassed: true` with
`scannedObjects: []`, and the `adr-0104-value-shapes` row read
**verified** (`verified_at` set, `blocking: 0`).

### A2. The census, one row per command

| command | `--object` | `--apply` records a deployment flag | where it
is written (base) | unknown `--object` on base (measured) |
| --- | --- | --- | --- | --- |
| `value-shapes` | repeatable | `adr-0104-value-shapes` | the CLI:
`recordDataMigrationRun` at `value-shapes.ts:221` | exit 0,
`scannedObjects: []`; with `--apply`, the flag is recorded **verified**
|
| `files-to-references` | repeatable | `adr-0104-file-references`, then
the column step's `columns_moved_at` | the producer:
`runFilesToReferencesMigration` at
`files-to-references-migration.ts:120`; the column stamp is
`recordFileColumnMove` in the CLI (`files-to-references.ts:472`) | exit
0, both scans' `scannedObjects: []`; with `--apply`, the flag is
recorded **verified**, **and the column step moved
`os21644_product.image` and stamped `columns_moved_at`** |
| `summary-nulls` | repeatable | none (its header: "No deployment flag,
deliberately") | none | exit 0, `fields: []`, on a dry run and on
`--apply` |
| `duplicates` | single | none: no `--apply`, and it writes nothing |
none | exit 0, `scanned: []`, `filter: { object: 'os21644_sitee' }` |

A correctly spelled narrowed `files-to-references --apply` on base also
recorded the flag verified and moved the column. Every scan draws its
default candidates from the same registry: `options.objects ??
Object.keys(engine.getConfigs())` in `scanValueShapes`,
`backfillFileReferences`, `verifyFileReferences` and
`backfillSummaryNulls`, and `stack.allObjects()` for
`collectScanTargets`. Each keeps only the candidates it covers, which is
where an undeclared name was dropped.

### A3. The narrowed run

- **CLI-recorded flag (`value-shapes`).** The flag write is skipped on a
narrowed run, whether the run passes or fails. `--json` carries `flag:
null` and `filter: { objects }` (`null` on a full-scope run, the shape
`duplicates` already keeps). Both faces print one sentence: the run was
narrowed, no deployment flag was recorded, and the command that records
one is the same command without `--object`.
- **Producer-recorded flag (`files-to-references`).**
`runFilesToReferencesMigration` skips the write when it is given
`objects`. This is the declared `service-storage` path only. Its `flag`
result is `null` on a narrowed run. The CLI prints the same sentence and
carries `filter`.
- **The column step (`files-to-references`) does not run on a narrowed
run.** The census row above is why. The step retypes every single-value
media column in the database on the authority of the gate, and a
narrowed gate vouches only for the named objects. Its stamp also
requires a verified flag, which a narrowed run no longer records. Left
running, a narrowed `--apply` would move columns and then fail to record
the move. It now returns a stated skip, `narrowed_run`, and the human
face says why.
- **What "narrowed" means.** Any `--object` narrows, even a list that
names every declared object. The flag is earned by the one spelling that
means "every object", which is a run without `--object`. Treating a full
list as full scope would need a second definition of "the whole
deployment", checked against the registry of the moment, and that
registry changes with the composition between two runs. The operator
also gets one unambiguous prescription.
- **Deviation from the dispatch wording ("skips it when `objects` is
non-empty").** The producer treats **any** `objects` as narrowed, `[]`
included. A scan handed `[]` walks nothing (`[] ?? …` is `[]`). A
non-empty test would therefore record a verified flag over an empty
scan, the card's own defect at the producer's API. A unit pin holds
this.
- The prompts and closing lines that promised a flag on a narrowed run
now say it records none. ⛔ `--apply --object` is not refused, and the
full-scope write is unchanged.

### A4. Unknown `--object`

- **Checked against the registry the command's own boot resolved, before
the scan.** For `value-shapes`, `files-to-references` and
`summary-nulls` that registry is `Object.keys(engine.getConfigs())`. For
`duplicates` it is the names of `stack.allObjects()`. These are the same
sets the scans draw from, so the refusal and the scan judge one
population. There is no `packages/objectql` edit and no scanner edit.
- **The refusal is objectstack-ai#21643's.** It is `objectNotFoundError` from
`@objectstack/core`: `code: 'OBJECT_NOT_FOUND'`, `status: 404`, and
`object` naming the first unknown name. Its message names every unknown
name and the declared objects, sorted. There is no new error code.
`value-shapes`, `files-to-references` and `summary-nulls` answer `{
error, code }`, as `unmapped-columns` does. `duplicates` keeps its own
error shape, `{ error: 'report_failed', detail, code }`: its catch now
passes `errorCodeFields` through.
- **The list is the declared set, not the covered subset.** Computing
the covered subset for `value-shapes` needs
`isScannableValueShapeField`, which `@objectstack/objectql` does not
export, and that package is fenced. The declared set is also exactly the
accept set. A declared object the command has nothing to check on is
accepted, because an empty answer about a real object is true. On the
fixture boot the list is 12 names, platform objects included.
- **Clause-②: no stands as the claim declared it.** A misspelled name
moves from exit 0 to exit 1, which is the ruled correction of a wrong
answer. Every declared name and `--apply --object` are still accepted.

## Changes

- `packages/cli/src/utils/migrate-object-scope.ts` (new):
`refuseUndeclaredObjects`, `isNarrowedRun` and `narrowedFlagNote`,
shared by the four commands.
- `packages/cli/src/commands/migrate/value-shapes.ts`: refuses an
unknown name, skips the flag on a narrowed run, adds `filter`, and
adjusts the narrowed prompt and closing lines.
- `packages/cli/src/commands/migrate/files-to-references.ts`: refuses an
unknown name, adds the `narrowed_run` column-step skip, adds `filter`,
and adjusts the narrowed prompt and closing lines.
- `packages/cli/src/commands/migrate/summary-nulls.ts` and
`duplicates.ts`: refuse an unknown name. `duplicates`' error document
carries the error's `code`.
-
`packages/services/service-storage/src/files-to-references-migration.ts`:
skips the flag write when given `objects`.
- `content/docs/deployment/cli.mdx`: one paragraph under "Data
migrations" (`--object` narrows, an unknown name is refused, only a
full-scope run records a flag), and the two `--object` example comments.
- `.changeset/21644-narrowed-apply-flag.md`: `@objectstack/cli` patch
and `@objectstack/service-storage` patch, `Clause-②: no`.

`packages/objectql`, `packages/platform-objects`, `packages/spec`, every
other `service-storage` path, and `content/docs/releases/` are
untouched.

## Pins

- **`object-scope.integration.test.ts`** spawns the CLI against SQLite,
one database copy per run, and is one enumeration over the census
(`FAMILY`).
- A narrowed `--apply` (`value-shapes`, `files-to-references`): exit 0,
`flag: null`, `filter: { objects }`, no flag row, and the note on stderr
naming the full-scope command.
- A full-scope `--apply`: the flag recorded verified, in the document
and in the row.
- `summary-nulls` and `duplicates`: no flag row, narrowed or not, as
before.
- A narrowed `--apply` after an earned flag leaves that row byte-equal.
- `files-to-references` narrowed: `columnMove: null` and
`columnsMovedAt: null`. Its full-scope control moves
`os21644_product.image` and stamps it.
- Unknown `--object`, on all four: exit 1 and `OBJECT_NOT_FOUND`, naming
the name and the declared objects. The one document is the refusal and
no report, no flag row is written, and the app rows are unchanged. The
human face exits 1 and names it.
- The measured repro. Control: the full-scope scan sees `blocking: 1`
and exits 1. The misspelled `--object --apply` exits 1 with
`OBJECT_NOT_FOUND`, and the flag stays unrecorded. Spelled right, the
narrowed run finds the value, exits 1, and still records no flag.
- **`migrate-object-scope.test.ts`** (unit): the envelope (`code`,
`status`, `object`), every unknown name named once, the declared list
sorted, the empty-registry message, the accepted cases, and what
`isNarrowedRun` treats as narrowed (an empty list and a full list both
narrow).
- **`files-to-references-migration.test.ts`** (`service-storage`, beside
the producer):
  - a narrowed apply converts and records no flag;
  - a narrowed failing apply records nothing;
  - a narrowed apply leaves an earned flag row equal;
  - `objects: []` records nothing.

## Reverse verification (implementation committed first; all three legs
re-run at the final head `fe988c20f0`)

Each leg ran through `node scripts/ablation-replace.mjs` in wrap mode,
under a script trap that restores from `HEAD`. The spawned CLI loads its
commands from `src/` through `bin/run-dev.js`. In the first round
`packages/cli/dist` did not exist. In the final round it held a build of
the unmutated source, and legs 1a and 2 still went red, which shows the
spawned CLI read the mutated `src/`. The `service-storage` unit pin
imports the producer from `src/`. Neither needed a rebuild.

- **Leg 1a, the narrowed-run skip in the CLI** (`value-shapes.ts`):
- The anchor `if (apply && !narrowed) {` became `if (apply) {`: anchor 1
to 0, replacement 0 to 1, blob `9f241dc2` to `d3a5c236`.
- **3 red, 17 green.** Red: the `value-shapes` narrowed pin, the
earned-flag-unchanged pin, and the spelled-right repro. Green: both
full-scope controls (the column-step control among them), the
`files-to-references` narrowed pin (its skip is the producer's), and
every unknown-name pin.
- Restored: blob `9f241dc2` equals `HEAD`, and `git diff HEAD` is empty.
- **Leg 1b, the narrowed-run skip in the producer**
(`files-to-references-migration.ts`):
- The same anchor and replacement: anchor 1 to 0, blob `1aa9fea2` to
`d4bb5002`.
- **4 red, 6 green.** Red: all four narrowed pins (passing, failing,
earned-flag-unchanged, and `objects: []`). Green: the six original pins,
the full-scope apply among them.
  - Restored: blob `1aa9fea2` equals `HEAD`.
- **A first round is recorded here because one of its readings was
vacuous.** At `80e5eda6ea` this leg read 3 red and 7 green: the
earned-flag-unchanged pin stayed green under the mutation, because the
fake engine's rewrite landed in the same millisecond as the earned row.
The pin now dates the earned row in the past (`fe988c20f0`), and the
re-run is the reading above.
- **Leg 2, the unknown-name refusal** (`migrate-object-scope.ts`):
- The anchor `if (unknown.length === 0) return;` became `if
(unknown.length >= 0) return;`: anchor 1 to 0, replacement 0 to 1, blob
`b78a88b4` to `9286a990`.
- **Unit: 3 red, 3 green.** Red: the three refusal cases. Green: the
accepted cases and the two `isNarrowedRun` cases.
- **Integration: 10 red, 10 green.** Red: all eight unknown-name pins
(two per command, on all four), the human face, and the misspelled
repro. Green: every narrowed and full-scope pin, and the repro's
control.
  - Restored: blob `b78a88b4` equals `HEAD`.
- In the first round, the `duplicates` "refused before anything was
read" pin stayed green under this mutation: it asserted only on a key
that report never carries. It now asserts that the one document is the
refusal, which reds on all four commands.

After all legs, `git diff HEAD` was empty and `git status --porcelain`
was clean.

## Local verification (final head `fe988c20f0`, on base `759dbe9ed3`)

`origin/main` was `759dbe9ed3` for the whole verification. Just before
this PR opened, it gained four commits, `f40bb3217f` to `1a230548cf`
(objectstack-ai#21649, objectstack-ai#21632, objectstack-ai#21648, objectstack-ai#21650). None of them touches this diff's paths
(`packages/spec`, `metadata-protocol`, `service-automation`, `lint`,
skills and docs references), so they were not merged in. CI runs on the
merge ref.

- **Builds.** The CLI's dependency closure (`turbo run build
--filter=@objectstack/cli^...`) gave VERDICT 0.
`@objectstack/service-storage` was rebuilt after the producer change
(exit 0), and `@objectstack/cli` was built (exit 0). A repo build for
the gate prerequisites gave VERDICT 0 (turbo: 72 tasks, 71 cached).
- **`@objectstack/cli` typecheck** (`tsc --noEmit` plus
`check:test-typecheck`): exit 0 at `80e5eda6ea`. No CLI file changed
after that commit.
- **`@objectstack/cli` unit project in full** at `80e5eda6ea`:
- 255 of 257 files passed, with 3742 tests passed and 29 skipped (the
two files below).
- The other two files,
`test/published-subpath-{console,hook-body}.pin.test.ts`, refused before
testing because `packages/cli` was not built (their own prerequisite
message). After the CLI build, both passed: 2 files, 29 tests.
- **`@objectstack/service-storage`**: typecheck exit 0, and the full
suite at `fe988c20f0` passed 41 files and 633 tests.
- **`os migrate` integration pins on built packages**, at `80e5eda6ea`:
  - this PR's pin plus the absent-database roster: 2 files, 59 passed;
- the one-shot family plus `duplicates.integration`: 2 files, 79 passed.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) derived 97 commands at
`fe988c20f0`.
  - All 97 ran there, and each exited 0.
- `--ran` with exit codes: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN.
- An earlier round at `80e5eda6ea` had three gates answer `PREREQUISITE
NOT MET` (exit 3): `check:skill-examples`, `check:dual-build-cjs-loads`
and `check:i18n-coverage`. They read packages outside the CLI closure.
The repo build cleared them.
- **Full `pnpm lint`** (`eslint . --no-inline-config` over the whole
repo): exit 0 at `fe988c20f0`, with nothing printed.
- **No exported symbol was renamed or moved,** so the liveness-ledger
anchor check had nothing to read.

## Acceptance notes

- **A narrowed run's counterexample is not recorded.** The ruling says a
narrowed `--apply` records no flag, so it records none even when it
finds a violation. Such a counterexample is deployment-level evidence,
since one off-shape value disproves "every value is on shape". The
operator still gets exit 1 and the findings, and the next full-scope run
closes the gate. This is an observation, not a filing. Carrier: none.
- **The declared list includes platform objects.** It is 12 names on the
fixture's lean boot, and a deployment that composes more plugins prints
more. A long list in an error message is the price of naming the exact
accept set. Carrier: none.
- **A narrowed `value-shapes --apply` still takes the plain
(DDL-performing) boot** even though it now writes nothing. That is
unchanged, and the boot paragraph in the docs still describes it
truthfully. Carrier: none.

---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants