Skip to content

fix(spec,service-automation): the wait executor reads its declared contract only (#4045) - #4161

Merged
os-zhuang merged 3 commits into
mainfrom
claude/console-screen-flow-submit-jfpjy4
Jul 30, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/console-screen-flow-submit-jfpjy4

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

#4045 的 wait 一半。另一半(超时契约压根没实现)单开为 #4158,刻意没有塞进本 PR。

先纠正我自己在 #4045 上说错的判断

我一度把 wait 报成「没有 configSchema,所以设计器配不了它」。那是错的,已在 issue 上更正。wait 的契约不在 config 里,而在 FlowNodeSchema.waitEventConfig(flow.zod.ts:199)—— 每个属性都有 .describe()、在可授权字段清单里、进了生成参考文档、showcase 也确实这么写。描述符不带 configSchema 是设计如此。

站得住的那一半

// wait-node.ts(改动前)
// …fall back to a loose `config` for hand-authored flows that put the same keys under config.
const eventType = String(wec.eventType ?? loose.eventType ?? 'timer');

执行器另外读了 6 个 loose config 键,其中 duration 和 signal 是 spec 从未声明过的拼法。这就是 #4050 退役掉的 notify.source 形状:一份只由代码注释宣告的第二契约,写了它的作者会得到一个永远能跑、也永远不会被引导到声明拼法的流程(PD #12)。

而且它不是假想的 —— 见下方第二处更正。

做法

新增 ADR-0087 D2 转换 flow-node-wait-event-config-lift,把 6 个 loose 键提升到声明块,优先级严格镜像被删掉的 ?? 链 —— 已声明的值胜出,其 loose 对应物留在原地(和 renameConfigKey 对被遮蔽别名的处理一致)。

一个会把存量流程写坏的陷阱

engine.ts:1465 是 applyConversionsToFlow(...) 紧接 FlowSchema.parse(converted) —— 加载器解析的是转换后的流程。而 waitEventConfig.eventType 没有 .optional():

// flow.zod.ts:200
eventType: z.enum(['timer', 'signal', 'webhook', 'manual', 'condition'])

所以一个只写了 config: { duration: 'PT1M' } 的存量流程,若提升后不带 eventType,会从能跑变成加载失败。转换因此补上 'timer' —— 执行器对这个形状原本的默认值。这不是收尾修饰,是这个改动能不能安全上线的关键。

?? 'timer' 在执行器里保留了,并在注释里说明它不是同类兜底:waitEventConfig 本身可选,没有它的 wait 节点是合法的定时等待。

第二处更正:4450fba

第一版 commit 里我写了 "Nothing in-repo authors it"。错的。 showcase 自己的 wait_revision 节点写的正是这个形状:

config: { eventType: 'signal', signalName: 'budget_revision' }

所以这道后门不是假想的,而且演示 wait 的那个示例本身就在用被退役的拼法。它已改为 waitEventConfig。

转换本来就正确处理了这个形状(执行器行为两边一致),新增的测试钉住了 showcase 撞上的那个精确组合:声明的键名出现在未声明的位置 —— 这正是候选顺序必须处理对的地方(signalName 先于 signal),并断言转换后的流程仍能 parse。

这个错误是在排查一个卡住的 objectstack verify CI 步骤时发现的 —— 排查结论是那不是本改动引起的,但排查过程本身翻出了这句假陈述。

两份清单,第二份我第一版漏了

这个层维护两份注册:CONVERSIONS_BY_MAJOR[17] 和 step17.conversionIds(链式回放走后者)。我第一版只加了前者,被仓库自己的棘轮抓住 —— migrations.test.ts 把每个转换的 fixture 都过一遍链,未接线的转换会在那里失败,而不是静默地失效上线。

顺带一提:这本身又是一个「两份手写清单」实例(#3786 那个模式),但这一个有闸门,所以它的表现正如设计。

验证

项 结果
spec 全量 272 文件 / 7097 测试通过
转换 + 迁移 95 通过(新增 6)
wait-node.test.ts 13 通过(新增 3)
spec 12 道闸门 全 PASS(含重生成后的 check:spec-changes / check:upgrade-guide)
根级 8 道闸门 全 PASS
pnpm --filter @objectstack/spec exec tsc --noEmit(CI 的 typecheck 门禁) clean

两处负控都做实了:

  1. 摘掉转换注册后重新跑 —— 新测试里 2 个失败。第三个即使没有转换也通过(执行器本来只读声明值),所以我把它的测试名改成只声称它实际证明的事:它钉的是执行器那一侧的优先级。
  2. 从转换输出里删掉 eventType —— FlowSchema.parse 抛错,证明那个默认值是承重的。

新的执行器测试走 registerFlow(也就是真正施加转换的那个 seam),所以它们证明的是存量源端到端可用,不只是「执行器不再去看 config 了」。

生成物

spec-changes.json 和 docs/protocol-upgrade-guide.md 已重生成 —— 迁移步骤的 rationale 是生成物输入(该字段被声明为「the one place prose is load-bearing」,所以我也扩写了它)。两道闸门都是先报 FAIL 再修好的,不是猜的。

一条存量情况,非本 PR 造成

packages/services/service-automation 里 src/engine.test.ts 有 2 个类型错误(缺 resumeAuthority,#3951 加该字段时带 .default 留下的)。我的 diff 里 resumeAuthority 出现 0 次,也不含 engine.test.ts;而且 CI 的 typecheck 只覆盖 @objectstack/spec,所以这两个错误既不是我引入的、也不在门禁内。记录在此,未在本 PR 处理。

🤖 Generated with Claude Code

https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq

…ntract only (#4045)

`wait` keeps its contract in `waitEventConfig` — a declared, `.describe()`-annotated
block on `FlowNodeSchema` that sits in the authorable-field list, reaches the
generated reference, and is what the showcase actually authors. Its descriptor
publishes no `configSchema`, which is by design rather than the gap it looks like.

The executor nevertheless also read six loose `config` keys behind `wec.X ?? loose.X`,
two of them (`duration`, `signal`) spellings the spec never declared anywhere. That is
the `notify.source` shape #4050 retired: a second de-facto contract announced only by a
code comment, so an author who wrote it got a flow that worked forever and was never
steered to the declared spelling (PD #12). Nothing in-repo authors it.

- New ADR-0087 D2 conversion `flow-node-wait-event-config-lift` lifts
  config.{eventType,timerDuration,duration,timeoutMs,signalName,signal} onto the
  declared block, in the executor's own `??` precedence — a declared value wins and
  its loose counterpart is left shadowed, as `renameConfigKey` treats a shadowed alias.
- `eventType` is stamped `'timer'` when the lift would otherwise leave the block
  without one. Load-bearing, not tidiness: the loader parses the CONVERTED flow
  (`applyConversionsToFlow` → `FlowSchema.parse`) and `waitEventConfig.eventType` is
  required once the block exists, so a stored flow carrying only
  `config: { duration: 'PT1M' }` would have gone from working to failing to load.
  `'timer'` is the exact default the executor applied to that shape.
- The six `?? loose.*` fallbacks are deleted. The surviving `?? 'timer'` is not one:
  `waitEventConfig` is itself optional, and a node without it is a valid timer wait.

Registered in both lists the layer keeps: `CONVERSIONS_BY_MAJOR[17]` and
`step17.conversionIds`. The first draft missed the second and the repo's own ratchet
caught it — `migrations.test.ts` replays every conversion fixture through the chain,
so an unwired conversion fails there rather than shipping inert.

Verified at the real seam: the new executor tests author the legacy shape and go
through `registerFlow`, which is what applies the conversion. Negative control run by
unregistering the conversion — two of the three fail without it; the third passes
either way because it pins the executor's half of the precedence, and its name says so.
A second negative control pins the `eventType` default: deleting it from the converted
output makes `FlowSchema.parse` throw.

Regenerated `spec-changes.json` and `docs/protocol-upgrade-guide.md` (the step
rationale is a generated-artifact input). All 12 spec gates and 8 root gates pass;
spec suite 7097 tests green.

Deliberately unchanged and filed as #4158: `waitEventConfig.timeoutMs` is declared a
timeout guard but read as a timer duration, and `onTimeout` has zero readers — `wait`
has no timeout implementation at all, while the showcase authors `onTimeout: 'continue'`.
Implementing or retracting that is a behaviour change, not a contract cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
@vercel

vercel Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 1:55pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): packages/services, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits July 30, 2026 13:41
…#4045)

Corrects a factual claim in the previous commit. It said "Nothing in-repo authors
it" of the loose `config` back door — that is wrong. The showcase's own
`wait_revision` node authored exactly that shape:

    config: { eventType: 'signal', signalName: 'budget_revision' }

so the back door was not hypothetical, and the example that demonstrates `wait`
was itself on the spelling this PR retires. It moves to `waitEventConfig`.

The conversion already handled this shape correctly — the executor's behaviour is
identical either way, and a new test pins the exact combination the showcase hit:
the DECLARED key names sitting in the UNDECLARED location, which is what the
candidate ordering has to get right (`signalName` before `signal`). It also asserts
the converted flow still parses, since the lift creates the required block.

Found while checking whether a stuck `objectstack verify` CI step could be caused
by this change rather than by the runner — it could not, but the audit that ruled
it out is what surfaced the false claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 14:09
@os-zhuang
os-zhuang merged commit 9b702dc into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/console-screen-flow-submit-jfpjy4 branch July 30, 2026 14:09
os-zhuang added a commit that referenced this pull request Jul 30, 2026
#4183)

`packages/spec` has eight checked-in generated artifacts, each with its own gate,
split across two CI jobs that run their gates SEQUENTIALLY. The first stale
artifact therefore masks every one behind it: you fix it, push, and learn about
the next on the following run. Two pushes on #4040 (`check:docs`, then
`check:api-surface`), two more on #4161 (`check:spec-changes`, then
`check:upgrade-guide`) — four round trips spent discovering something one local
run could have said at once.

Every gate runs; a failure does not stop the rest. The summary lists all stale
artifacts with the exact `gen:` command for each.

`--fix` regenerates ONLY what this run proved stale. Deliberately not a
regenerate-everything button: blanket regeneration rewrites artifacts whose
staleness you never saw, which is how a real semantic change lands silently
inside a mechanical diff.

The gate -> generator ledger reconciles against `package.json` on EVERY run, in
both directions, rather than behind a `--self-test` flag. An unclassified
`check:`/`gen:` script fails the run instead of quietly dropping out of coverage
— otherwise the summary would still say "all artifacts up to date" while checking
fewer, the exact class of lie this exists to remove. It proved itself by
rejecting its own `package.json` entry on the first run.

Two things it refuses to be silent about: the `check:api-surface` stale-`dist`
trap is printed inline when that gate is the one failing (it reads the built
`.d.ts`, so an unbuilt tree reports newly-added exports as breaking removals),
and the four source audits it does NOT run are named, so "all up to date" never
reads as "everything passed". It also reports that `gen:openapi` and `gen:sbom`
have no gate at all.

Verified both directions: clean built tree -> all 8 pass; a change injected into
a migration step's `rationale` -> `check:upgrade-guide` fails and `--fix`
regenerates that artifact and no other. That run also corrected an earlier
reading of mine: the two ADR-0087 gates have different inputs — the conversion
registry drives `spec-changes.json`, the rationale prose drives
`protocol-upgrade-guide.md` — they had only appeared to fail together because
#4161 changed both.

AGENTS.md's hand-rolled loop over eight hardcoded gate names is replaced by the
command; that list could not survive a ninth artifact, and the ledger can.


Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq

Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 31, 2026
…never got (#4219)

The drift comment on #4161 was computed by the mapper bug #4206 fixed: the
service-automation change collapsed to `packages/services`, so these four
docs were attributed to the wrong package — and a service-automation-only
diff would have reported none of them. Re-derived with the fixed mapper and
audited against the implementation.

flows.mdx carries the #4161 axis itself: a `runAs:'user'` run that resolved
no trigger user has its data operations refused. Also documents `node.type`
as an open string checked against the live registry (ADR-0018), the script
executor as naming a callable with logger-backed `email`/`slack` markers,
the `.strict()` shells, and six previously undocumented node keys.

implementation-status.mdx gains the real `/api/v1` route prefixes, the
memory driver's actual `InMemoryDriver.supports` matrix, and the ADR-0090 D3
`Role` -> `Position` rename — which ratchets the role-word baseline down by
one, so that gate-mandated update ships here too.

Gates: docs build, check:doc-authoring, check:role-word, check:nul-bytes,
check:release-notes, check:skill-examples (198 prose examples).


Claude-Session: https://claude.ai/code/session_01J2x8Tie9WT1VgWFifqMKrR

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ptions state each decision in words instead of a tracker number (stage 4) (objectstack-ai#21555)

Part of objectstack-ai#20749
Clause-②: no

Stage 4 of the `domain:spec` lane's share of the runtime-string
burn-down (ruling `5902360492`, form D): class (b) of the stage-3
census, the text `packages/spec/src` shows authors and administrators.
Every rewritten string now states in words what the cited decision was,
or drops a citation its sentence already explained. Text only.

## What changed

- **56 ADR-0087 conversion summaries** in
`packages/spec/src/conversions/registry.ts` (68 tracker ids): every
protocol 16 → 17 summary that carried an id. A summary is the "Change"
column of `docs/protocol-upgrade-guide.md`, the `to` text of
`spec-changes.json`'s `converted[]` records and what `os migrate meta
--json` reports under `specChanges`, so it is read by an author
upgrading metadata.
- **Three descriptions** (3 ids): `FieldSchema.autonumberFormat`'s
`.meta()` description (`data/field.zod.ts`), and the route descriptions
of `GET /:type/:name/layers` and `POST /:type/:name/publish`
(`api/plugin-rest-api.zod.ts`).
- **Generated, by `check:generated --fix`** (exactly the three artifacts
it proved stale): `docs/protocol-upgrade-guide.md` (56 rows),
`packages/spec/spec-changes.json` (56 summaries, twice each: the
per-major record and the aggregate), and the `autonumberFormat` rows of
`content/docs/references/data/field.mdx`, `data/object.mdx` and
`system/migration.mdx`.
- One `@objectstack/spec` **patch** changeset, `Clause-②: no`.

## Size: the split (A2)

At this base the class (b) population is unchanged from the stage-3
census: 91 conversion summaries with 108 ids (81 distinct cards) plus 3
descriptions with 3 ids. That is over the ~60-card bar, so this PR
delivers up to a protocol-step boundary, lowest step first:

| protocol step | summaries with ids | ids | distinct cards |
|---|--:|--:|--:|
| toMajor 11 / 13 / 14 / 15 | 0 | 0 | 0 |
| **toMajor 17 (delivered)** | **56** | **68** | **48** |
| toMajor 18 (next stage) | 35 | 40 | 34 (33 not cited in 17) |

The three descriptions ride this part (3 more cards, no overlap): 59
messages, 71 ids, 51 distinct cards delivered. The next stage's exact
list is the 35 toMajor-18 summaries at the end of this body.

## Delivered: each site, the decision read, the new words

Every cited card was read through REST with all comments; the record
column names the comment (or commit) the decision was read from. Where a
summary already said why, the citation is dropped and the sentence kept.
Placeholders `OBJECT_NAME` / `RULE_NAME` below stand for the
angle-bracket spelling in the source.

| conversion (head line) | cited | decision as read (record) | summary
now reads |
|---|---|---|---|
| `action-execute-to-target` (:727) | objectstack-ai#3713 | `execute` is the
deprecated alias of `target`; spec and objectui resolved the pair in
opposite directions; align on the spec rule and drop the alias so the
divergence is unrepresentable (body (closed completed, no comments)) |
action key 'execute' → 'target' (the deprecated handler alias; the spec
and the renderer had resolved the pair in opposite directions, so one
key now names the handler) |
| `field-conditionalRequired-to-requiredWhen` (:767) | objectstack-ai#3754 | same
fold-and-drop as objectstack-ai#3713: `requiredWhen` canonical, alias folded and
dropped from parsed output (body (closed completed, no comments)) |
field key 'conditionalRequired' → 'requiredWhen' (the deprecated
predicate alias, folded into the canonical key so no reader picks its
own precedence) |
| `agent-tools-to-skills` (:822) | objectstack-ai#3894 | ADR-0109 accepted;
`agent.tools[]` removed because it resolved names against the full
registry with no surface check, breaking ADR-0064 (tool set = union of
skills' tools) (PR body (merged)) | agent key 'tools' removed — declare
capability in a skill (ADR-0064: an agent's tools are exactly its
skills' tools, and this inline slot resolved names against the whole
registry with no surface check) |
| `sharing-rule-access-level-full-to-edit` (:881) | objectstack-ai#3865 | route B is
the end state: sharing grants read/edit only; delete, transfer and
re-share come from object permissions, ownership and admin scope; `full`
→ `edit` is lossless (5105498900) | sharing-rule accessLevel 'full' →
'edit' (`full` never granted more than `edit`; a sharing rule grants
read or edit, while delete and transfer come from object permissions and
ownership) |
| `flow-node-crud-object-alias` (:954) | objectstack-ai#3796 | the seven aliases (six
open-coded `??` + the shim's last `object`) graduate straight into the
D2 layer; the `readAliasedConfig` shim is deleted (5125152179) | CRUD
flow-node config key 'object' → 'objectName' (the last alias in the
executors' `readAliasedConfig` shim graduates into this layer, and the
shim is deleted) |
| `flow-node-notify-config-aliases` (:1077) | objectstack-ai#3796, objectstack-ai#4045 | objectstack-ai#3796: `??`
fallbacks graduate, `actionUrl` canonical (downstream chain uses it).
objectstack-ai#4045: `notify.source` was a read-but-undeclared shape → conversion
layer, not configSchema (5125152179; 5127636357, 5138487536) | notify
flow-node config keys 'to' → 'recipients', 'subject' → 'title', 'body' →
'message', 'url' → 'actionUrl' (executor `??` fallbacks graduated into
this layer; `actionUrl` is canonical because the notification chain
downstream already uses it), and nested 'source: {object, id}' →
'sourceObject' / 'sourceId' (a shape the executor read that no config
schema declared) |
| `flow-node-wait-event-config-lift` (:1288) | objectstack-ai#4045 |
`node.config.eventType` etc. were an undeclared second contract beside
the declared `waitEventConfig`; graduate them (objectstack-ai#4161) (5130277099,
5138487536) | wait flow-node loose config keys → the declared
`waitEventConfig` block: 'eventType', 'timerDuration'/'duration' →
'timerDuration', 'signalName'/'signal' → 'signalName', 'timeoutMs' (the
executor also read these keys from the loose config, a second contract
beside the declared block) |
| `flow-node-map-flow-alias` (:1364) | objectstack-ai#4045 | reconciliation found the
`map.flow` alias (undeclared executor fallback); graduated (objectstack-ai#4228)
(5138487536) | map flow-node config key 'flow' → 'flowName' (an
undeclared spelling the executor accepted through a bare fallback; it
graduates into this layer) |
| `flow-node-subflow-flow-alias` (:1421) | objectstack-ai#4278 | reconcile the
schemaless nodes' forms with their executors and check `subflow`; its
bare `flowName ?? flow` fallback graduates (body (closed completed, no
comments) + conversion docblock) | subflow flow-node config key 'flow' →
'flowName' (an undeclared spelling the executor accepted through a bare
fallback, found when the schemaless nodes were reconciled with their
executors; it graduates into this layer) |
| `flow-node-connector-config-lift` (:1529) | objectstack-ai#4045 | executor reads
only `connectorConfig`; the descriptor schema rooted the triple at
`config`, so the Studio form wrote unread keys; descriptor stops
publishing, stored loose keys lift (5132926517, 5138487536) |
connector_action flow-node loose config keys 'connectorId' / 'actionId'
/ 'input' → the declared `connectorConfig` block (the executor reads
only that block; the published designer form had been writing these keys
where nothing read them) |
| `flow-node-script-config-aliases` (:1641) | objectstack-ai#3796 | as above:
open-coded `??` fallbacks graduate into the D2 layer (5125152179) |
script flow-node config keys 'functionName' → 'function', 'input' →
'inputs' (executor `??` fallbacks, graduated into this layer) |
| `app-dead-authoring-keys-removed` (:1742) | objectstack-ai#4001, objectstack-ai#4509, objectstack-ai#4667, objectstack-ai#4709
| liveness audits: keys unread or wrongly encoded are removed; objectstack-ai#4709:
the "no shell read homePageId" premise was false, ruling B keeps the
retirement (an ID cross-reference that dangles is the wrong encoding)
(5158421901 (objectstack-ai#4509), 5159774792 (objectstack-ai#4667), 5164227920 (objectstack-ai#4709 ruling B)) |
app keys
'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId'
plus contextSelectors 'includeAll'/'placement' and areas 'order' removed
(liveness audits found each one unread or wrongly encoded; sharing/embed
declared a public surface no route enforced, mobileNavigation was fully
unimplemented, includeAll was deliberately disobeyed because an 'All'
row would clear a mandatory scope, homePageId WAS read by objectui's
console before v17 but encoded the landing page as an ID cross-reference
that silently fell back when it dangled — the landing page is the first
nav item (the first retirement record said nothing read it, a premise
since corrected; the retirement stands), and no renderer ever sorted
areas) |
| `app-area-fail-open-gates-removed` (:1853) | objectstack-ai#4651 | ruling B: remove
both keys; removing a fail-open gate is strictly safer than keeping it;
prescription names the two enforced layers (5160072589) |
navigation-area keys 'visible'/'requiredPermissions' removed (ADR-0049 —
FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or
permission-gated area was served and rendered to every user, while the
identically named keys on a navigation ITEM and on the APP are enforced;
gate the items inside the area, or gate the app) |
| `permission-rls-priority-removed` (:1955) | objectstack-ai#3896 | the objectstack-ai#3896
security-audit line: rls.priority promised conflict resolution that
cannot exist (policies OR-combine) and had no reader; removed (card body
+ commits d6bfb3d (objectstack-ai#3990), eb95d97 (objectstack-ai#3998)) | RLS-policy key
'priority' removed (a security audit found no reader: policies
OR-combine, so the promised conflict-resolution semantics cannot exist;
dropping it changes no outcome) |
| `tool-inert-authoring-keys-removed` (:2024) | objectstack-ai#3896 | the audit
close-out removes the four inert tool keys (permissions gated nothing,
active:false withdrew nothing) (commit eb95d97 (objectstack-ai#3998)) | tool keys
'category'/'permissions'/'active'/'builtIn' removed (authorable and
inert, so removed under ADR-0049 enforce-or-remove; permissions gated
nothing, active:false withdrew nothing) |
| `action-inert-keys-removed` (:2137) | objectstack-ai#3896 | close-out sweep:
enforce-or-remove worklist, fourteen inert authoring keys leave the
surface (commit 12a19a8 (objectstack-ai#4054)) | action keys
'shortcut'/'bulkEnabled' removed (inert, removed under ADR-0049
enforce-or-remove: no keydown path dispatches shortcuts; the
multi-select toolbar reads the view's bulkActions) |
| `flow-inert-keys-removed` (:2162) | objectstack-ai#3896 | as above (commit
12a19a8 (objectstack-ai#4054)) | flow keys 'active'/'template', node 'outputSchema'
and errorHandling 'fallbackNodeId' removed (inert, removed under
ADR-0049 enforce-or-remove: active:false never stopped a flow; status is
the enforced lifecycle) |
| `view-inert-keys-removed` (:2221) | objectstack-ai#3896 | as above (commit
12a19a8 (objectstack-ai#4054)) | view keys removed as inert (ADR-0049
enforce-or-remove): list 'responsive'/'performance', form
'defaultSort'/'aria' — no renderer read them (list aria/data and form
data stay live) |
| `view-list-passthrough-keys-removed` (:2267) | objectstack-ai#7176 | maintainer
ruling: retire under ADR-0049; pass-through-only reads are dead in
effect (5236139723) | view list keys removed:
'striped'/'bordered'/'virtualScroll' — every measured reader copied the
key forward and none applied it (a key that is only passed through is
dead in effect; ADR-0049 enforce-or-remove) |
| `view-export-options-pdf-removed` (:2335) | objectstack-ai#8010, objectstack-ai#1301 | option A;
`pdf` leaves the enum (honest narrowing, not a runtime console.warn);
PDF export declined (5270998514; objectstack-ai#1301 is not planned) | list-view
export format 'pdf' removed (PDF export was declined as not planned, and
ObjectGrid dropped the declared format from the menu with only a runtime
console.warn; an honest enum replaces that warning) |
| `dashboard-inert-keys-removed` (:2404) | objectstack-ai#3896 | close-out sweep (as
above) (commit 12a19a8 (objectstack-ai#4054)) | dashboard keys 'aria'/'performance'
and widget 'performance' removed (inert, removed under ADR-0049
enforce-or-remove: no renderer applied any of them) |
| `dashboard-widget-responsive-removed` (:2474) | objectstack-ai#4876, objectstack-ai#11027 | objectstack-ai#4876
ruling A: retire widget `responsive` (no reader); objectstack-ai#11027 ruling B:
retire `page.components[].responsive`, equally unread (5169512655;
5380752244) | dashboard widget key 'responsive' removed (no renderer
ever applied per-widget breakpoint overrides; the
page.components[].responsive key this entry once deferred to was
measured equally unread and retired at protocol 18) |
| `dashboard-widget-action-aria-removed` (:2555) | objectstack-ai#5010 | retire the
four dead widget keys; colorVariant kept (body ruling + 5179556002) |
dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria'
removed (no renderer ever drew a per-widget action button, and widget
ARIA attributes never reached the DOM; use header.actions[] and the
widget title/description) |
| `dashboard-widget-compareto-converged` (:2652) | objectstack-ai#5011 | converge
`compareTo` on the implemented executor contract `{ kind, dimension? }`;
`1y` rewrites, other offsets delegated (5173559485) | dashboard widget
'compareTo' converged on the executor's { kind, dimension? } contract
(the shape the dataset executor implements; the bare strings and {
offset: '1y' } rewrite mechanically; other { offset } durations have no
faithful target and are reported, not guessed) |
| `agent-knowledge-removed` (:2726) | objectstack-ai#3896 | close-out sweep (as above)
(commit 12a19a8 (objectstack-ai#4054)) | agent key 'knowledge' removed (inert,
removed under ADR-0049 enforce-or-remove: declaring sources/indexes
never scoped retrieval; restrict at the knowledge-service level) |
| `skill-trigger-phrases-removed` (:2744) | objectstack-ai#3896 | close-out sweep (as
above) (commit 12a19a8 (objectstack-ai#4054)) | skill key 'triggerPhrases' removed
(inert, removed under ADR-0049 enforce-or-remove: activation is
triggerConditions + the agent's skills[] allowlist; phrases were a
dead-end projection) |
| `stack-api-require-auth-removed` (:2782) | objectstack-ai#3963 | delete the
`api.requireAuth` opt-out; anonymous always denied; public surfaces
derive authorization from a declaration (form, share link, book
audience) (body (decision recorded in body)) | stack key
'api.requireAuth' removed — anonymous access is always denied; publish
public surfaces by declaration (a public form, a share link or
`book.audience: 'public'`), which replaced the deployment-wide opt-out |
| `flow-node-wait-timeout-keys-removed` (:2864) | objectstack-ai#4158 | wait never had
a timeout: withdraw the contract (route B), `timeoutMs` moves to
`timerDuration` (body (closed completed) + conversion docblock) |
waitEventConfig keys 'timeoutMs' (→ 'timerDuration', stringified — its
only reader used it as the duration) and 'onTimeout' (removed — zero
readers, so no timeout ever fired): wait never had a timeout, so its
timeout contract is withdrawn rather than built |
| `datasource-inert-blocks-removed` (:2944) | objectstack-ai#4583 | all 20 dead
datasource keys removed; each job already has a different live mechanism
(5157934690) | datasource keys 'retryPolicy'/'healthCheck' and external
'label'/'requirePermission' removed (nothing retried, nothing probed on
a schedule, and the federation label/permission were read by nobody;
each of those jobs already has a live mechanism) |
| `mapping-inert-keys-removed` (:3032) | objectstack-ai#4509 | the three mapping keys
retire (schema defaults made authorWarn impossible; removal is the only
signal) (5158421901, 5158744185) | mapping keys
'extractQuery'/'errorPolicy'/'batchSize' removed (no exporter reads a
mapping, error handling belongs to the import request, and the write
path sizes its own batches) |
| `book-translations-removed` (:3087) | objectstack-ai#4667 | six dead authorWarn keys
retired (5159774792) | book keys 'translations' (book-level and
group-level) removed (no resolver read them; the tree endpoint and
portal render labels verbatim, so a localized book served its authoring
locale to everyone). Localize the docs instead: `doc.translations` is
live |
| `job-id-removed` (:3149) | objectstack-ai#4667 | as above (5159774792) | job key
'id' removed (nothing read it; `name` is the job's identity everywhere,
so two jobs differing only in `id` were the same job, and the key's own
description advertised an override that did not exist) |
| `translation-validation-messages-removed` (:3206) | objectstack-ai#4667, objectstack-ai#3778,
objectstack-ai#14381 | objectstack-ai#4667 retire; objectstack-ai#3778 legacy-key table had pointed `errors` at
it; objectstack-ai#14381 an object-scoped key ships with its reader (ADR-0049
enforced) (5159774792; objectstack-ai#3778 body; 5503980929) | translation key
'validationMessages' removed (no resolver read it, so a translated rule
message was stored and never shown; the legacy-key table of the
translation-bundle migration had been steering retired `errors:` authors
into it). Author the message on the rule itself
(`object.validations[].message`), and translate it under the
object-scoped group
`objects.OBJECT_NAME._validations.RULE_NAME.message`, which the write
path resolves (17.3.0, a translation key shipped together with its
reader) |
| `datasource-capabilities-removed` (:3264) | objectstack-ai#4583 | as above
(5157934690) | datasource key 'capabilities' removed (eleven flags no
code read; pushdown comes from the driver's own supports.*, and
`readOnly` never made anything read-only) |
| `datasource-read-replicas-removed` (:3319) | objectstack-ai#4468 | remove:
read-replica routing is an unbuilt feature (5150771330) | datasource key
'readReplicas' removed (no driver opened a replica connection and no
query path splits reads from writes; front replicas behind one endpoint
and point `config` at it) |
| `datasource-config-driver-key-aliases` (:3419) | objectstack-ai#4456 | the factory's
undeclared `??` fallbacks graduate to a D2 entry and are deleted from
the reader (5157922838) | datasource config keys → canonical per driver:
sqlite 'file'/'database' → 'filename', postgres/mysql 'connectionString'
→ 'url' and 'user' → 'username', mongo 'uri' → 'url' and 'user' →
'username' (undeclared driver-factory `??` fallbacks, graduated into
this layer and deleted from the reader) |
| `flow-node-script-branch-keys-removed` (:3665) | objectstack-ai#4343 | operator
ruling: `script` converges to a pure function-call node; the five branch
keys retire (5151704360) | script flow-node config keys 'actionType' (→
'function' when it was shorthand for one; otherwise removed —
'email'/'slack' were logger-backed stubs that delivered nothing), plus
'template' / 'recipients' / 'variables' (fed those stubs) and 'script'
(inline JS the runtime never executed); script is now a pure
function-call node, the only path that ran real logic |
| `object-managed-by-system-to-system-data` (:3762) | objectstack-ai#3355 | retire
`system`, new value `system-data` (not `platform-data`) (5157022965) |
object managedBy 'system' → 'system-data' (ADR-0103's residual bucket
named the engine-owned half v16 had already moved out to `engine-owned`;
the rename leaves the name describing what the bucket actually holds:
admin/user-writable platform data) |
| `object-enable-trash-mru-removed` (:3829) | objectstack-ai#3207, objectstack-ai#2377 | remove
`enable.trash` / `enable.mru`; soft delete stays parked; last slice of
the dead-property removals (5156966571, 5161298967; 5051634768) | object
capability flags 'enable.trash'/'enable.mru' removed (the last slice of
the dead author-facing property removals: no recycle bin and no MRU
tracking ever ran; both default-true flags gated nothing) |
| `object-index-type-partial-removed` (:3912) | objectstack-ai#5248, objectstack-ai#4943 | remove
both index keys; no DDL consumer; return enforce-first on real demand
(5199336983; 5194762679 (duplicate)) | object index keys
'indexes[].type'/'indexes[].partial' removed (no driver ever read
either: the index method is the dialect's choice and a partial index is
built by a database-layer migration, not declared) |
| `retry-policy-converged` (:4070) | objectstack-ai#4661, objectstack-ai#4964 | one RetryPolicy
declaration, `backoffMs`, merged default 0 / 1 with pre-17 job defaults
written out; flow.errorHandling joins, default 0 (silent retry can
double-write) (5158710540 (analysis); 5173148383) | retry policy unified
across job.retryPolicy, try_catch retry and flow.errorHandling: base
delay 'retryDelayMs' → 'backoffMs', and the pre-17 job defaults
(maxRetries 3, backoffMultiplier 2) written out explicitly now that the
merged default is 0 / 1: two declarations that differed only by accident
became one, and retry is opt-in because a retry replays whatever the
attempt already did |
| `hook-body-crypto-hash-removed` (:4249) | objectstack-ai#4391 | remove the
capability token and its build-time inference (5156969500) | script-body
capability token 'crypto.hash' removed (the sandbox never installed
ctx.crypto.hash, so the token granted a call that always threw; the CLI
inferred it too) |
| `dataset-measure-array-string-agg-removed` (:4419) | objectstack-ai#6188 | retire
`array_agg` / `string_agg`; keep and enforce `count_distinct`
(5219849918) | dataset measure aggregates 'array_agg' / 'string_agg'
removed (no SQL backend compiled them and the v1 dataset runtime refused
them by name, so a measure declaring one never produced a value; the
measure is dropped, and with it any derived measure left referencing it)
|
| `connector-rate-limit-config-removed` (:4544) | objectstack-ai#4911 | outbound
rate-limit vocabulary removed: no engine exists (implementation-first)
(body ruling + 5169405647) | connector key 'rateLimitConfig' removed (no
outbound rate-limiting engine exists; the runtime's only token bucket
limits INBOUND requests, so every knob here was inert while reading like
a configured cap. The whole ConnectorRateLimitConfig shape went with it)
|
| `field-mapping-transform-removed` (:4669) | objectstack-ai#5552, objectstack-ai#3278 |
enforce-or-remove: all five members dead, the union retires; `js`
dialect was retired as redundant with the L2 script body (5199338349;
objectstack-ai#3278 body) | field-mapping key 'transform' removed (the whole
five-member FieldMappingTransform union went with it: no runtime ever
executed constant/cast/lookup/javascript/map, and the javascript member
advertised dialect="js", a dialect already retired because JavaScript
belongs in a script body. The enforced transform pipeline is the import
mapping's string-enum `mapping.fieldMapping[].transform`, which is
unaffected) |
| `theme-inert-token-scales-removed` (:4786) | objectstack-ai#5021 | retire all nine
token groups; re-declare under `customVars` (5175091297) | theme keys
'typography.fontSize'/'fontWeight'/'lineHeight'/'letterSpacing',
'typography.fontFamily.heading'/'mono', 'animation' and 'zIndex' removed
(ADR-0049 — the engine emitted --font-size-*, --font-weight-*,
--line-height-*, --letter-spacing-*, --duration-*, --timing-*, --z-*,
--font-heading and --font-mono faithfully, and no first-party component
or stylesheet has ever read one. Re-declare any variable you actually
consume under customVars, which emits it verbatim) |
| `page-header-subtitle-alias` (:4940) | objectstack-ai#3226 | route B: a D2
conversion rewrites `description` → `subtitle`; the consumer's bare `??`
retires (5160118898, 5194297145) | page-header component prop
'description' → 'subtitle' (the off-spec spelling a renderer tolerated
through a bare `subtitle ?? description` fallback; `subtitle` is the
declared key, and the fallback retires) |
| `record-picker-display-field-to-label-field` (:5165) | objectstack-ai#5775 |
direction A: `labelField` (the delivered spelling) becomes canonical;
`displayField` retires via conversion (5202137085) | record-picker
component prop 'displayField' → 'labelField' (the required key no
renderer read; `labelField ?? 'name'` is what renders the row, so the
delivered spelling became the declared one) |
| `record-picker-inert-keys-removed` (:5287) | objectstack-ai#5775 | `searchFields` /
`multiple` retire (zero readers) (5202137085) | record-picker component
props 'searchFields'/'multiple' removed (the control is a plain
single-select with no search box; neither key had a reader) |
| `page-card-body-to-children` (:5414) | objectstack-ai#5775 | `children` is the one
composition key (5202137085) | page:card component prop 'body' →
'children' (one composition key across every container; the card
renderer already reads both) |
| `inline-action-api-params-to-body-extra` (:5582) | objectstack-ai#5777 | direction
A: the static payload gets its own key (`bodyExtra`); `params` keeps one
meaning (5202138112, 5228796853) | inline type:'api' action prop
'params' (object form) → 'bodyExtra' (a static payload and a parameter
definition are two things, so the payload gets its own key; `params`
stays the ActionParam[] definition array) |
| `page-tabs-type-to-tab-style` (:5847) | objectstack-ai#6776 | Route A: rename to the
spelling the renderer reads (5229120747, 5229693342) | page:tabs
component prop 'type' → 'tabStyle' (a props key named `type` collides
with the node's dispatch key and is unauthorable in flat/JSX carriers;
`tabStyle` is the spelling the renderer reads in all of them) |
| `page-structure-inert-keys-removed` (:6035) | objectstack-ai#6946 | retire three
zero-reader UI keys (body (maintainer ruling) + 5232767409) |
page:header prop 'icon' and page:card prop 'actions' removed (neither
has a renderer read point in objectui; the header resolves icons per
action and the card renders title/children/footer only) |
| `record-details-layout-removed` (:6201) | objectstack-ai#6946 | as above (as above)
| record:details component prop 'layout' removed (the declared
auto\|custom modes were never implemented; the renderer branches only on
inline\|compact, values the schema never permitted, so both legal values
selected nothing) |
| `app-hidden-to-unpublished` (:6343) | objectstack-ai#4829 | A1: a machine-managed
key carries the publish gate; `hidden` back to navigation presentation
only; ADR-0045 amended (5173161521) | stored app publish gate 'hidden' →
'_unpublished' (ADR-0045 amended — `hidden` carried BOTH the publish
gate and 'keep out of the App Switcher', so the built-in Account app was
withheld from every non-builder; the gate is now the machine-managed
`_unpublished`, and `hidden` is navigation presentation only, never an
access gate. Stored rows only — an authored `hidden: true` is left
untouched) |
| `action-global-nav-location-removed` (:6456) | objectstack-ai#6888 | direction 2:
retire `global_nav` (no demand; the designer previewed a surface the
product lacks) (5229990375) | action location 'global_nav' removed (no
running-app surface rendered it; the ⌘K palette reads no action
metadata, while the Studio designer previewed a command-palette frame
for it. The value is stripped and the key kept, so an action left with
no location becomes the documented headless shape `locations: []`) |

Descriptions:

| site | cited | decision as read (record) | change |
|---|---|---|---|
| `data/field.zod.ts` `autonumberFormat` | objectstack-ai#6555 | route 3: `{0000}` is
a declared contract default, and both hand-written fallbacks read it
(5225535766, family done 5240072006) | "⇒ the contract default `{0000}`,
which every driver and the engine fallback read, so one field numbers
alike on every backend." |
| `api/plugin-rest-api.zod.ts` `GET /:type/:name/layers` | objectstack-ai#5882 |
ruling B: its own `/layers` path and schema, one route one shape
(recorded 5216370790) | "…hence its own path and its own response
schema, since one route answers one shape." |
| `api/plugin-rest-api.zod.ts` `POST /:type/:name/publish` | objectstack-ai#7294 |
declare the served publish route's response, the save door's discipline
(5237410722) | "The route was served for a long time with no declaration
behind it — this entry is what makes its response contract nameable, the
same declared-equals-returned rule the save door follows." |

## Text-only proof (A4)

Stage 3's AST-skeleton + string-text tool (`skeleton.cjs`, one line
changed: the TypeScript 6.0.3 load path), BASE `1ac7308d7a` against this
branch: **3 of 3 SAME** on both legs, exit 0 each — `registry.ts` 62346
tokens, 4920 string groups, 56 changed; `field.zod.ts` 8682 / 581 / 1;
`plugin-rest-api.zod.ts` 4912 / 475 / 2 (one literal re-split into three
`+` pieces, which the skeleton reads as one string); parse diagnostics 0
/ 0. Every changed group carried an id before and carries none after;
every other string is byte-identical. Controls on scratch copies of the
head `registry.ts`, each mutation counted on disk first: an identifier
rename → DIFF exit 1; `!==` flipped to `===` → DIFF exit 1; one summary
re-split into two `+` operands → SAME exit 0; a `surface` string (never
carried an id) changed → text leg VIOLATION exit 1. No repo file was
mutated for the controls.

Census after the edit (stage 3's instrument, unchanged): non-test 219 →
160 messages, 429 → 358 ids; class (b) conversion summaries 91 / 108 →
35 / 40 (all toMajor 18); descriptions 3 / 3 → 0.

## Pins and quotes (A6)

- **Tests:** no test asserts a changed summary or description phrase.
The id-bearing fragments and the distinctive phrases of all 59 messages
were searched across every `*.test.*` / `*.spec.*`; the hits are other
files' own prose with their own citations (test titles and comments such
as `(objectstack-ai#3896 close-out)` in `view.test.ts`), not quotes of a summary.
- **`content/docs/**`:** the only quotes are the three generated
`references/**` pages, regenerated.
`content/docs/releases/v17/17-0.mdx:1052` repeats one action-key phrase
with its own `(objectstack-ai#3896 close-out)`; it is release-owned and untouched.
- **`skills/**`:** no quote of any changed text.

## Gates

`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` at `db81973cf6` (9 paths, 493 changed lines): 105 commands,
each run from the worktree with its exit code written before any pipe;
`--ran` → "✓ dispatch-gates --ran: 105 derived famil(ies) accounted for
— 105 run, 0 NOT-MEASURED". Three first exited 3 (prerequisite: unbuilt
lint / client packages) and were re-run green after the full package
build (71 tasks). Plus `@objectstack/spec` build, `check:generated`
("All 15 generated artifacts are up to date"), the package `test`
project (603 files, 17845 passed), 48 of the 51 `test:repo` files, and
`typecheck`. Details are in the report on the card.

## Acceptance notes

- **The `objectstack-ai#3896` citations.** Eight summaries cited `objectstack-ai#3896` as an "audit"
or a "close-out". The card's own body is the sharing-rule REST finding
that opened that security-audit line; the decisions the summaries cited
(remove `rls.priority`, the four inert tool keys, then the fourteen-key
enforce-or-remove sweep) are recorded in the landed commits
`d6bfb3d0ab`, `eb95d97c02` and `12a19a88a5`. Each summary already said
why its key went, so the new words name the rule (ADR-0049
enforce-or-remove) rather than the card.
- Comments in `conversions/registry.ts` still carry tracker ids; they
belong to objectstack-ai#20234's comment stages and are untouched. So is
`migrations/registry.ts`.
- `docs/protocol-upgrade-guide.md` is not a governed surface
(`check-governed-merges`' register).
- **Hot file:** open PR objectstack-ai#21547 (objectstack-ai#21459) also edits
`conversions/registry.ts`: it adds one toMajor-18 entry and its ordering
row, in a region this PR does not touch. A local `git merge-tree` of the
two heads is clean (the file is hand-written, so no merge driver is
involved). Neither PR's spec-changes / upgrade-guide output includes the
other's entries, so whichever lands second merges `main` and regenerates
them.

## Next stage: the 35 toMajor-18 summaries (file:line at this head · ids
· conversion)

- `registry.ts:6550` objectstack-ai#8321 `field-malformed-scale-precision-removed`
- `registry.ts:6658` objectstack-ai#8762 `record-chatter-position-vocabulary`
- `registry.ts:6777` objectstack-ai#9198 `element-input-target-variable-removed`
- `registry.ts:7024` objectstack-ai#9220 `element-filter-removed`
- `registry.ts:7177` objectstack-ai#9249 `element-form-removed`
- `registry.ts:7355` objectstack-ai#15178,objectstack-ai#19620
`translation-per-app-settings-removed`
- `registry.ts:7609` objectstack-ai#9249 `translation-component-submit-label-removed`
- `registry.ts:7782` objectstack-ai#3951,objectstack-ai#9227 `field-column-lists-canonicalized`
- `registry.ts:7909` objectstack-ai#10414 `metric-filters-removed`
- `registry.ts:8104` objectstack-ai#17296 `cube-sub-day-granularities-removed`
- `registry.ts:8237` objectstack-ai#18612 `cube-join-sql-and-relationship-removed`
- `registry.ts:8502` objectstack-ai#10054 `record-highlights-field-icon-removed`
- `registry.ts:8759` objectstack-ai#11027 `page-component-responsive-removed`
- `registry.ts:8860` objectstack-ai#11805 `object-grid-default-sort-removed`
- `registry.ts:9047` objectstack-ai#21445 `object-grid-resizable-columns-removed`
- `registry.ts:9250` objectstack-ai#17260 `object-kanban-quick-add-removed`
- `registry.ts:9563` objectstack-ai#12497,objectstack-ai#1883
`permission-allow-restore-purge-removed`
- `registry.ts:9881` objectstack-ai#6837 `field-reference-to-alias`
- `registry.ts:10301` objectstack-ai#14478 `hook-timeout-to-timeout-ms`
- `registry.ts:10342` objectstack-ai#14478 `job-timeout-to-timeout-ms`
- `registry.ts:10946` objectstack-ai#14478
`api-endpoint-cache-ttl-to-cache-ttl-seconds`
- `registry.ts:11015` objectstack-ai#14478
`dashboard-refresh-interval-to-refresh-interval-seconds`
- `registry.ts:11376` objectstack-ai#14478
`memory-persistence-auto-save-interval-to-ms`
- `registry.ts:11600` objectstack-ai#14478 `turso-config-timeout-to-timeout-ms`
- `registry.ts:11690` objectstack-ai#17063 `view-page-mount-removed`
- `registry.ts:11795` objectstack-ai#17053,objectstack-ai#8221
`list-view-sort-string-clause-to-array`
- `registry.ts:12295` objectstack-ai#19054 `object-tenancy-organization-field-removed`
- `registry.ts:12405` objectstack-ai#20085 `view-item-owner-hidden-removed`
- `registry.ts:12549` objectstack-ai#20230 `view-overlay-owner-hidden-removed`
- `registry.ts:13137` objectstack-ai#6206,objectstack-ai#17321
`page-component-filter-record-to-rule-array`
- `registry.ts:13392` objectstack-ai#20161 `report-joined-chart-removed`
- `registry.ts:13657` objectstack-ai#20221 `form-layout-inline-grid-to-vertical`
- `registry.ts:13813` objectstack-ai#19992 `currency-config-precision-removed`
- `registry.ts:13917` objectstack-ai#20321 `permission-rls-tags-removed`
- `registry.ts:14056` objectstack-ai#15429 `flow-decision-mode-inclusive-explicit`

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

---------

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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants