Skip to content

feat(spec)!: ObjectSchema 在 parse 路径上收紧,而不只是 create() —— #1535 的奠基例子一直是活的(#4001 批 3a) - #4522

Merged
os-zhuang merged 2 commits into
mainfrom
claude/strict-schema-authz-surface-s8vnok
Aug 1, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/strict-schema-authz-surface-s8vnok

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#4001 的批 3 前半(清单)。

#1535 的守卫没盖住真正要紧的那条路

ObjectSchema.parse ({ …, workflows: ['x'] })   →  静默丢弃
ObjectSchema.create({ …, workflows: ['x'] })   →  自 #1535 起正确报错

#1535 把未知键守卫做成了 create() 工厂里的手写检查,理由写在代码里:「作者写的 *.object.ts 模块都调 create()」。

这话没错,但它们不是唯一的生产者,也不是大多数实例走的路:defineStack({ objects })、/api/v1/meta/types/object、Studio 表单,三条全部经由 parse() / safeParse()。

于是对象级 workflows: [...]——作者以为接好了自动化、实际发布了一份死元数据,本 issue 正文举的那个例子——在 #1535 被认为已修复的整段时间里,一直在主路径上被静默吃掉。

现在 base shape 是 .strict(),带上 UNKNOWN_KEY_GUIDANCE 的墓碑,以及告警层早就知道的语义改名(capabilities / features → enable)。毕业不该让作者少拿一条处方——这两个编辑距离够不着,只能显式命名。

create() 不受影响:它自己的检查在 parse 之前跑,抛的是更详细的定位错误。

读取路径安全,理由和其他已关闭的注册类型一样:ADR-0010 信封已声明,且 stripReadDecorations 会在任何严格重解析前剥掉 _diagnostics / _draft(cloud#971)。实测而非推断——platform-objects 加三个示例应用里所有 ObjectSchema.create() 调用,顶层键全部已声明。

新墓碑:namespace

它此前没有墓碑,所以被静默剥离。{ namespace: 'sys', name: 'user' } 会以 user 发布——作者以为自己声明了命名空间,实际对象名不是他要的那个。拒绝信息现在直接给出 name: "sys_user"。

顺带:修掉一个本改动本会引入的覆盖回归

未知键告警层是按根 schema 的姿态决定整个集合走不走的。所以 object 顶层一收紧,整个 objects 集合被排除——它下面 71 个仍是 strip 的嵌套站点会在同一个 commit 里停止告警,而没有任何地方会说这件事。

姿态是每个节点的属性,走查现在按节点处理:strict 的根在自己那层沉默(解析已经在报,再警告就是重复),下探照常。object.fields.* 的嵌套告警不受这次毕业影响。

这是本战役第四次撞到同一个形状:一处改进悄悄拿掉另一处的覆盖。

三个「把静默剥离钉成预期」的测试改写了

namespace / 退役的 compactLayout 别名 / 移除的 detail 块。其中 compactLayout 那个把对作者不利的结局写得很直白:

"the retired key is STRIPPED, not aliased — an old-key author gets no highlightFields rather than silently working."

和 step 2 的 position.test.ts(断言一个不存在的层级)是同一类:测试把 strip 时代的假象固化成了正确行为。

验证

  • @objectstack/spec:283 文件 / 7159 用例通过,tsc --noEmit 干净
  • 8 个生成物闸门 up-to-date,15 个 check:* 全绿
  • 嵌套告警存活性直接实测:object 收紧后 objects.acct.fields.amt.lenght 仍然报告;根层不再重复报告

参考

🤖 Generated with Claude Code

https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY


Generated by Claude Code

…e() (#4001)

#1535's founding example was still reproducible on the path that matters.

  ObjectSchema.parse ({ …, workflows: ['x'] })   → silently discarded
  ObjectSchema.create({ …, workflows: ['x'] })   → rejected since #1535

#1535 built the guard as a hand-rolled check inside the `create()` factory,
reasoning that authored `*.object.ts` modules call `create()`. They do — but
they are not the only producer, and not the path most instances travel:
`defineStack({ objects })`, `/api/v1/meta/types/object` and the Studio form all
reach this schema through `parse()`/`safeParse()`. So object-level `workflows`
— an author believing they had wired up automation and shipping dead metadata,
the example this whole campaign was filed on — kept being eaten in silence for
the entire time #1535 was considered fixed.

The base shape is now `.strict()`, carrying the `UNKNOWN_KEY_GUIDANCE`
tombstones plus the semantic renames the WARNING layer already knew
(`capabilities`/`features` → `enable`). Graduating a surface from warn to reject
must not cost the author a prescription — edit distance cannot reach those two,
so they are named explicitly.

`create()` is unaffected: its own check runs before parsing and throws a richer
located Error.

Safe on the read path for the reason the other closed registered types are: the
ADR-0010 envelope is declared, and `stripReadDecorations` removes
`_diagnostics`/`_draft` before any strict re-parse (cloud#971). Verified rather
than assumed — every `ObjectSchema.create()` call across `platform-objects` and
the three example apps uses only declared top-level keys.

New tombstone: `namespace` (retired in ADR-0006 D4) had none, so it was stripped
in silence. `{ namespace: 'sys', name: 'user' }` shipped as plain `user`, under
a name its author never intended. The rejection now carries `name: "sys_user"`.

AND a coverage regression this change would otherwise have introduced.

The unknown-key warning layer gated each metadata collection on its ROOT
schema's posture, so closing `object` at the root would have switched off the
warnings for everything BENEATH it — its 71 nested strip-mode sites stopping in
the same commit, with nothing anywhere to say so. Posture is a per-node
property; the walk now treats it as one. A strict root stays silent at its own
level (the parse owns that failure) and the descent continues. Nested
`object.fields.*` warnings are unaffected by the graduation.

That is the fourth instance in this campaign of one shape: an improvement in one
place quietly removing coverage in another.

Three tests that asserted the strip as correct behaviour are now rejection
tests. The `compactLayout` one had pinned the author-hostile outcome in so many
words: "the retired key is STRIPPED, not aliased — an old-key author gets no
highlightFields rather than silently working."

Verified: spec 283 files / 7159 tests, `tsc --noEmit` clean, all 8 generated
artifacts current, all 15 `check:*` gates green.

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

vercel Bot commented Aug 1, 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 Aug 1, 2026 4:22pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling size/m labels Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

107 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 @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/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 @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/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • 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 @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 @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.

@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 16:23
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 7bba90b Aug 1, 2026
21 checks passed
@os-zhuang
os-zhuang deleted the claude/strict-schema-authz-surface-s8vnok branch August 1, 2026 16:43
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 2, 2026
…ctstack-ai#4001 批 5) (objectstack-ai#4529)

* feat(spec)!: translation closes at both doors, and objectstack-ai#3778's ten-key guard retires into the message (objectstack-ai#4001)

A translation that resolves to nothing is indistinguishable from a translation
nobody wrote — no wrong string appears, just the source language, forever. So
this type had the most literal version of the silent-strip failure in the spec.

objectstack-ai#3778 already knew that, and fixed it for ten keys: a `z.preprocess` scanning
for the retired object-first dialect. It had the shape every workaround for
`.strip` has — it caught only the mistakes someone had already thought of, and
it ran on the item door only, so the same ten keys in a file-authored bundle
were dropped in silence. Same asymmetry objectstack-ai#4522 found in objectstack-ai#1535's object guard.

The guard is now redundant and gone; its ten prescriptions ride the rejection
as `guidance`. What was worth keeping was never the detection — detection
generalizes for free once the default flips — it was the prose.

Closed across every authorable group (objects/fields/views/actions/sections,
apps, dashboards, pages, settings, metadata forms) and the i18n config, whose
four objectstack-ai#3494-removed knobs get tombstones.

Two gates were found doing half their job:

  - `translation` came off the ADR-0010 envelope debt list (down to four).
  - `metadata-create-seeds.test.ts` — the canonical "create shape ≠ spec"
    guard — asserts every seed parses. The `translation` seed ships
    `{ name, label, locale, objects }` and the type declared neither `name`
    nor `label`, so two thirds of the authoritative create shape was stripped
    while the gate reported green. A gate on a `.strip` schema catches a
    missing required key and never an extra undeclared one.

Registered types closed at the top level: 17 of 25.

Verified: 284 files / 7208 tests, tsc clean, 8 generated artifacts current,
15 check gates green, and the real bundles in examples/app-crm, app-todo and
platform-objects all parse at module load.

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

* docs(i18n): the retired-dialect rejection now covers both doors, not just the metadata one (objectstack-ai#4001)

Three passages described objectstack-ai#3778's guard accurately for what it was — ten keys,
rejected at the metadata door — and objectstack-ai#4001 changed both halves of that: any
undeclared key is rejected, in a runtime item and in a file-authored bundle.

- ui/translations.mdx: "only the groups on this page are accepted" was
  aspirational for bundles; it is now literally true. Says why this surface
  cares more than most — a translation that resolves to nothing looks exactly
  like one nobody has written yet.
- i18n-standard.mdx: same correction on the retired-dialect callout.
- i18n-standard.mdx: the `translationService` design-intent callout already
  warned the key is unrecognized; copying that snippet is now a build-time
  rejection rather than a silent drop, which is the part a reader acts on.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…e 关 (objectstack-ai#4721) (objectstack-ai#4922)

* feat(spec,metadata-protocol)!: reject a sort node spelling its direction `direction` (objectstack-ai#4721)

`SortNodeSchema` was a plain `z.object`, so zod's `.strip` default applied:

    SortNodeSchema.parse({ field: 'updated_at', direction: 'desc' })
      →  { field: 'updated_at', order: 'asc' }

The foreign key was discarded, `order` fell back to `asc`, and the sort ran the
OPPOSITE way under an ordinary success — with `limit`, a different set of rows,
with no signal anywhere in the response.

`direction` is not a typo: it is `IReportService.orderBy`'s live vocabulary,
which plugin-auth's objectql adapter already translates by hand. A translation
known to be necessary and enforced nowhere is the ADR-0049 shape.

Both doors onto that shape are closed here, in one change:

- `SortNodeSchema` (spec/src/data/query.zod.ts) → `strictObject` with
  `aliases: { direction: 'order' }`, so the rejection carries the translation.
  Edit distance can never bridge `direction` → `order`, so a bare
  "unrecognized key" would leave the caller where the silent strip did.
- `normalizeSortNodes` (metadata-protocol/src/protocol.ts) — the ingress every
  external `orderBy` funnels through — refuses `{ field, direction }` with
  400 INVALID_SORT naming `order` and quoting the corrected node.

Closing only the schema would repeat the objectstack-ai#1535/objectstack-ai#4522 door asymmetry:
`SortNodeSchema.parse` is reachable by three paths the REST normalizer never
sees, and the normalizer runs ahead of any QueryAST parse.

Deliberately NOT in scope: `QuerySchema`'s top level stays non-strict
(`QuerySchema.safeParse({object:'sales', nonsenseKey:1}).success === true`) —
tracked in the objectstack-ai#4001 campaign map for its own batch.

The `{field: direction}` map form is untouched: there `direction` is an ordinary
column name, and refusing it would be the mirror-image bug.

Strictness ledger: `query.zod.ts` keeps its `open` class for the four dialect
sites; `SortNodeSchema` is carved out as authorable (4 strip of 5), which also
resolves the recorded classification conflict — the FILE was the wrong unit.

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

* docs(spec): keep SortNodeSchema's page description short; rationale moves to line comments

`build-docs.ts` takes the FIRST `/** */` block in a `.zod.ts` file as the
reference page's description, and joins every line of it with a blank line. The
long objectstack-ai#4721 rationale therefore rendered as a 56-line wall at the top of
`content/docs/references/data/query.mdx`, where a customer reads what a sort
node IS — not why one schema in the file is strict.

The prose is unchanged, it is just `//` instead of `/** */` so the generator
cannot pick it up, with a note at the top saying why it must stay that way.

Regenerated: query.mdx (two lines), and the two skill reference indexes, which
grew transitive entries because query.zod.ts now imports shared/strict-object.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
 批 19/20) (objectstack-ai#5250)

* feat(spec)!: object 内层块拒绝未知键 —— 13 个站点收紧,IndexSchema 依据实测保留 (objectstack-ai#4001 批 19/20)

`data/object.zod.ts` 的顶层自 objectstack-ai#1535/objectstack-ai#4519/objectstack-ai#4522 起就会拒绝未知键,而它下面
的内层块一直是 zod 默认的 `.strip`。这个不对称正是本批的价值所在:一个已经
见过顶层拒绝拼写错误的作者,完全有理由把 `lifecycle: { maxAge: '30d' }` 的
干净解析读作"接受"。object 是本仓作者写入量最大的元数据类型。

批 20 —— 14 个站点关掉 13 个:
- `access`(ADR-0066 D2)、`lifecycle` 及其四个子块(retention/ttl/storage/
  archive)、`fieldGroups[]`(ADR-0085)、`external`(ADR-0015)、
  `userActions`、`systemFields`、`activityMilestones[]`、`publicSharing`、
  `objectExtensions[]`。
- 可达性是实测而非假定:从 24 个元数据类型根加 `ObjectStackSchema` 做 BFS
  (4810 节点 / 25 根),每一个都按**身份**判定 `direct`,没有一个依赖
  objectstack-ai#5056 发现的 `derived-clone` 桥;同一次运行里 `ObjectSchema`/`PageSchema`
  为正控制,批 14 的 `EmbedConfigSchema` 为负控制(unreachable,重叠 0.00)。
- parse 门单独探针、逐路径验证:今天顶层 REJECT、13 条嵌套路径全部静默
  STRIP,所以每一次收紧转化的都是实测到的静默丢弃,而不是猜测。

⚠️ `IndexSchema` 有意保留,这个"不动"才是本批的发现(objectstack-ai#5114 类,这次是在
发布**之前**抓到而不是之后)。console 为这个形状自带了一份手抄 JSON-Schema
(objectui `metadata-admin/EmbeddedItemEditor.tsx`)—— 因为 `index` 是框架
不发布 schema 的内嵌子类型 —— 而这份抄件已经漂移:它用 `where` 表示部分索引
谓词,spec 声明的是 `partial`。编辑器把表单结果拼回 `object.indexes[]` 后
PUT 整个对象,`saveMetaItem` 逐字保留 body 并做校验,所以单独关掉这个形状会
让 console 自己渲染的控件 422。该能力两个方向上其实都已经死了 ——
`driver-sql` 的 `syncDeclaredIndexes` 只读 `name`/`fields`/`unique` ——
这恰恰是必须先修生产者、并给 `type`/`partial` 一个 ADR-0049 结论的原因:
今天把作者指向 `partial`,是一条声称超出平台实际交付的 guidance(finding 18)。

批 19 —— `ui/app.zod.ts` 的 `BaseNavItemSchema`:`verify` 检查结论为否,
不改姿态。台账假定成员 `.extend()` 该基底(finding 16 的陷阱),实际它们
spread `...shape`;两者机制不同,并且双向实测过。九个分支各自已经 `.strict()`,
基底是模块私有且从不 parse,关掉它是保证的 no-op —— 而 objectstack-ai#4583 明确指出
no-op 收紧并非中性。`Class` 单元格未改动:它是机读的,而八个既有判定词
没有一个对"既不是门也不是死物"的形状是诚实的,加第九个属于改机读契约。

每条 alias/guidance 都是对 schema 的断言(finding 18),因此逐条锚定到具名
的兄弟契约并在测试中断言;`fieldGroups[]` 三个已弃用的 collapse 别名保持
**接受** —— 收紧一个形状不该把有文档的弃用变成拒绝。

已知并如实钉住的一处:`systemFields` 是 `false | {…}` 联合,拒绝信息是
`invalid_union` 的裸 "Invalid input"(objectstack-ai#5014 摊平);批 18 的
`discriminatedUnion` 解法在这里不可用(一个分支是字面量,没有判别键)。

三处记录齐备:schema JSDoc + `object-strictness-batch20.test.ts` /
`app-strictness-batch19.test.ts` + 台账散文行;计数由 os-regen 重新生成。

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

* build(spec): 合并 origin/main 后重算 strictness 计数(os-regen)

merge 带入 objectstack-ai#5221 的 chart 站点(482 → 483),计数文件按 merge=os-regen 的
约定从合并后的树重新生成,而不是任何一边的算术。authorable 仍为 16。

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

* docs(spec): 把批 19/20 的两个保留判定接到已归档的 issue 上(objectstack-ai#5247/objectstack-ai#5248/objectstack-ai#5249)

IndexSchema 的保留(生产者漂移 + ADR-0049)与 BaseNavItemSchema 的 Class
词汇缺口都已单独归档,台账行与 schema JSDoc 现在指向 issue,而不是只说
'交给维护者'——一条指不出去的判定,下一个人只能重新测一遍。

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

* build(spec): 合并 objectstack-ai#5237 后重算 strictness 计数(os-regen)—— authorable 16 + (-1) → 15

合并队列以 MERGE_CONFLICT 踢出后重新同步 origin/main(merge,非 rebase)。
objectstack-ai#5237(objectstack-ai#5073 allowAddTab)与本批从两个不同目录改这份台账,散文行不重叠,
git 干净合并;数字零手改,counts.md 按 merge=os-regen 从合并后的树整体重生成。

重算结果 15 = `ui/` 7 → 6(objectstack-ai#5237)+ `data/` 22 → 9(批 20),两边分支都没写过
这个数——正是 objectstack-ai#5107 拆分要达到的效果。这一条也补进 data/ 段落:它是第十二次
实例,也是第一次没让任何人付出代价。

objectstack-ai#5237 对 `ui/app.zod.ts` 的改动(`apis` 墓碑文案)与本批的 BaseNavItemSchema
判定互不相干,两侧都在;它对 scripts/strictness-ledger.test.ts 的 view 计数
5 也是它自己的领地。

门:spec 308 文件 / 7929 测试通过;typecheck 通过;check:generated 9/9;
check:strictness-ledger 通过(484 站点,authorable 剩 15)。

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…ion letter no version of that ADR declares (objectstack-ai#12917)

check-adr-anchors' letter validation (landed via objectstack-ai#12785) now reads ADR-0006's
declared letters, and the 2026-08-28 v4 addendum (objectstack-ai#12736, direct-merged) minted
D1-D3 for the API-surface boundary record — making the ancient D4 citation in
this tombstone (from objectstack-ai#4522) mechanically unresolvable. Every merge-queue build
since 07:49Z fails on it, so the whole queue is stalled.

The retirement is real and stays enforced; the citation was always a phantom
(no version of ADR-0006 ever declared a D4 or decided object naming). Per the
gate's own remedy order this is case (c): a real but unwritten decision needs
an ADR, not a citation — the false letter is dropped now, the message keeps
the fix (name: "sys_user") and the objectstack-ai#4001 history, and a follow-up card asks
for the decision to be written so the citation can return.

Verified: check-adr-anchors --self-test 106/106 EXIT=0 (was 1 failure on the
pristine tree, reproduced first), live run EXIT=0, vitest object.test.ts
185/185 (the tombstone pin asserts substrings this change keeps).


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

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

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

Stage 18 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the fourth name-ordered file group directly under
`packages/spec/src/data/`: the 17 test files that carry an id from
`filter.test.ts` to `object.test.ts`. They carried 103 messages and 114
tracker ids, citing 81 records. Every one of those ids now either states
what its record decided, in words (form D), or is dropped where the
title already says it. Text only: no assertion, identifier, test count
or code comment changes.

## Census at the base (`c9be1f179d`)

Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`),
`census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs`
(md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5
`dda605c54745b4a60cc14c9a686e4eff`). They are byte-identical to the
copies stages 10 to 17 used. A literal counts as a test title when its
folded message is argument 0 of a `describe` / `it` / `test` call,
`.each` / `.skip` / `.only` chains included. Everything else is an
"other" string.

The base is `c9be1f179d`, one commit past the claim's `969ffba25e`. That
commit (objectstack-ai#21857) touches only `plugin-security`, so the test census is
the same. Both instruments read **1045 messages / 1108 ids in 225
files**, the seat's reading and stage 17's head reading.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `ui/` | 84 | 396 / 419 | 378 / 401 | 18 / 18 |
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `data/` (this PR: the fourth group) | 35 | 185 / 200 | 184 / 199 | 1 /
1 |
| `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| `ai/` | 1 | 2 / 2 | 0 | 2 / 2 |
| `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 |
| **total** | **225** | **1045 / 1108** | **988 / 1050** | **57 / 58** |

The group reads **103 messages / 114 ids in 17 files**, the seat's
figures, file for file. Every one of them is a test title:

| file (under `data/`) | messages / ids | titles | other |
|:--|--:|--:|--:|
| `filter.test.ts` | 18 / 19 | 18 / 19 | 0 |
| `form-delete-behavior-options.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `form-return-type-options.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `hook-body.test.ts` | 4 / 4 | 4 / 4 | 0 |
| `hook.test.ts` | 10 / 10 | 10 / 10 | 0 |
| `import-coercion.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `import-mapping-target.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `injected-system-column-provenance.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `injected-system-columns.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `inline-grid-column-currency-scale-refused.test.ts` | 5 / 5 | 5 / 5 |
0 |
| `inline-related-columns.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `managed-api-affordance.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `masked-field-types.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `numeric-column-representation.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `object-image-field.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `object-strictness-batch20.test.ts` | 13 / 17 | 13 / 17 | 0 |
| `object.test.ts` | 36 / 42 | 36 / 42 | 0 |
| **17 files** | **103 / 114** | **103 / 114** | **0** |

Five more test files sit in the same name range and carry no id:
`hook-api`, `hook-body-stored-metadata-target`, `inline-grid-columns`,
`mapping-connector-source` and `mapping`. They are not touched.

- **Controls.** Lit, a title and an "other" string:
`data/query.test.ts`, outside the group, reads 11 / 11 at the head as at
the base, its "other" string at `:202` included. Dark:
`data/import-mapping-target.test.ts` reads 0 / 0 at the head while 2 of
its comment lines still carry a number. Planted in scratch copies of
head files: an id put into a `hook-body.test.ts` title reads 1 / 1, and
an id put into a `masked-field-types.test.ts` comment reads 0.
- **A wider pattern** (any `#` plus digits) reads the same totals as the
gate pattern in 16 of the 17 files at the base. `object.test.ts` reads
one more, at `:857`, which is the colour value `'#00FF00'` and not a
tracker id. At the head the wider pattern reads 0 in 16 files and that
same colour in `object.test.ts`.
- **At the head:** 942 messages / 994 ids in 208 files. The 17 files
read 0 / 0, and no other file moved.

## How the area was chosen

`data/` has no subdirectory to split by, so its stages take name-ordered
file groups near the ~100-id bound. Stage 17's re-cut named this group
at 114, with `object.test.ts` alone carrying 42, and this census reads
114, so the rule needed no re-cut.

**Named for the next stages** (re-cut from the head census, 942 / 994;
`data/` 82 / 86 left, in 18 files):
- `data/`, one more stage: `query-transport.test.ts` to
`validation.test.ts` (11 files, 34 / 34) with `data/driver/` (7 files,
48 / 52): 82 messages / 86 ids.
- `ui/` 419, about four stages. `api/` 201, two. `system/` 165, two. The
files directly in `src/`, 120, one.
- The three docblock needles (`ai/build-progress.test.ts:236`, `:237`,
`contracts/approval-service.test.ts:274`), one stage with their
docblocks.

## What each id became

34 literals (41 ids) now state a decision in words. 69 literals (73 ids)
drop a number the title already explains.

Every cited record was read with its comments through REST. 73 answer
200; `framework#2536` is counted there, because the repository was
renamed `framework` → `objectstack` (`apps/docs/lib/layout.shared.tsx`
records the rename). The old name answers 403 from this session, and the
same number under the current name is the compactLayout retirement that
the title names. Eight answer 404, and each decision was read from what
landed, by REST GET of the landing commit and its CHANGELOG entry: objectstack-ai#6571
(`2f3e79351e`), objectstack-ai#10165 (`8012960508`), objectstack-ai#10347 (`530c1df653`), objectstack-ai#10527
(`5649efbf93`), objectstack-ai#11195 (`b372318836`), objectstack-ai#11408 (`f11fc61c51`), objectstack-ai#13644
(`34ce8e7dbe`) and objectstack-ai#18012 (`176b03582e`).

| record(s) | literal (under `data/`) | now reads |
|:--|:--|:--|
| objectstack-ai#5685 | `filter.test.ts:77` | "string comparands — the ordering slots
take a string, which is what the date macros emit". `string` joined the
four ordering slots because the platform's own date-macro resolver
produces only strings. |
| objectstack-ai#14080 | `filter.test.ts:166` | "null comparand — refused in the four
ordering slots, like a null list member". Ruled A: refused at the same
entrance, in the same shape, as the null `$in` member. |
| objectstack-ai#6571 (404) | `filter.test.ts:364` | "string endpoints — `$between`
takes the ISO and clock strings the platform produces". The sibling half
of objectstack-ai#5685, from `2f3e79351e`. |
| objectstack-ai#7711 (2) | `filter.test.ts:549` | "the whole-filter face refuses
these field conditions too — green while the group branch was a
non-strict catch-all". The group branch became `.strict()`, so a refused
member has nowhere to land. |
| objectstack-ai#5222 | `filter.test.ts:576` | "leaves the four ORDERING slots taking
a reference — a column-to-column comparison, and the prescribed
alternative". `$field` compiles to a same-table column comparison on SQL
push-down. |
| objectstack-ai#5322 | `filter.test.ts:1541` | "leaves the empty-combinator
identities accepted — each reduces to its boolean unit". Ruled:
`{$and:[]}` is every row, `{$or:[]}` none, `{$not:{}}` none. |
| objectstack-ai#14104 | `filter.test.ts:1851` | "FieldReferenceSchema.addDays — a
whole-day offset on a reference, an integer or another column". Ruled A:
an offset on the field reference. |
| objectstack-ai#16923 | `filter.test.ts:1998` | "filter.zod.ts docblock @examples — a
`$field` comparand names a column of the same row". The relation-path
example was the wrong half; the same-table prose was right. |
| objectstack-ai#14010 | `hook.test.ts:352` | "runAs — a hook may run as `system` or
`user`, and inherits by default". Ruled: `runAs: 'system' \| 'user' \|
'inherit'`, default `'inherit'`. |
| objectstack-ai#13644 (404) | `hook.test.ts:529` | "Referential-Cleanup Marker —
declared, and true only on a reference-cleanup write by the engine".
Adopted by maintainer ruling, from `34ce8e7dbe`. |
| objectstack-ai#4269 | `hook.test.ts:924` | "defineHook — the authoring factory, so a
hook is validated where it is written". The `defineDatasource` pattern:
the convention-scan path got a parse at authoring time. |
| objectstack-ai#3493 | `hook.test.ts:1148` | "PRESERVES `preserveAudit` through a
parse (the opt-in a historical import uses to keep its audit stamps)". |
| objectstack-ai#5945 | `hook.test.ts:1286` | "HookContext.api typing — the minimum
scoped context the docs teach: `object()` and `transaction()`". Ruled
option C. |
| objectstack-ai#4173 (2) | `import-coercion.test.ts:23`, `:48` | "import boolean
tokens — one table for the server coercion and the Import Wizard
preview" and "import reference types — exported from spec, not copied at
each consumer". |
| objectstack-ai#8116 | `injected-system-column-provenance.test.ts:53` | "provenance
derivation at its spec home — where the author-time linter can reach
it". Ruled option 1: the derivation moved into the contract package,
which the linter may import. |
| objectstack-ai#5378 | `injected-system-columns.test.ts:17` |
"resolveInjectedSystemColumns — the injected columns, so author-time
validation resolves them too". |
| objectstack-ai#20045 | `inline-grid-column-currency-scale-refused.test.ts:172` |
"SHAPE PARITY with the currency FIELD refusal (its ruled text carried,
not reworded)". "ruling B" is the ruling that retired `scale` on a
currency field; the letter went with the id. |
| objectstack-ai#7521 | `managed-api-affordance.test.ts:57` | "is the exact shape the
boot only warned about, now named at authoring time (sys_environment /
sys_package)". Ruled: an authoring-time check, with boot left at warn
and strip. |
| objectstack-ai#16318 | `numeric-column-representation.test.ts:24` | "the numeric
physical-representation table — one table the driver and the migration
generators both read". Ruled C: one table in `packages/spec`, both
producers read it. |
| objectstack-ai#4001 (2) | `object-strictness-batch20.test.ts:93`, `:229` | "批 20,
unknown keys refused — …", as stage 15 wrote "batch D, unknown keys
refused". `:132` already says it and keeps only "批 20". |
| objectstack-ai#1535, objectstack-ai#4519, objectstack-ai#4522 | `object-strictness-batch20.test.ts:124` | "the
root itself was already closed, on parse as well as create() — this
batch is the level BELOW it". |
| objectstack-ai#5014 | `object-strictness-batch20.test.ts:187` | "⚠️ `systemFields`
is the batch's ONE union flattened to a bare `Invalid input` — …". |
| objectstack-ai#5677, objectstack-ai#6365 | `object-strictness-batch20.test.ts:380` | "… — since
the unit anchor is injected, the property governs
`owning_business_unit_id` too". objectstack-ai#6365 is dropped: the title already
states its correction. |
| objectstack-ai#11195 (404) | `object-strictness-batch20.test.ts:422` | "the two
`userActions` vocabularies stay disjoint, the three adopted view keys
included — …". `b372318836` adopted `group` / `hideFields` / `rowColor`
onto the view block. |
| objectstack-ai#4001, objectui#4772 | `object-strictness-batch20.test.ts:528` | "批 20
— `IndexSchema` is closed (the held 14th site, once the console index
editor converged on it)". |
| objectstack-ai#10527 (404) | `object.test.ts:188` | "retention + ttl + archive
triple — refused unless the ttl restates the age bound". From
`5649efbf93`. |
| objectstack-ai#10347 (404) | `object.test.ts:192` | "still accepts the ttl + archive
pair, whose ttl cutoff picks the rows to archive (no retention)". From
`530c1df653`. |
| objectstack-ai#2834 | `object.test.ts:264` | "accepts retention.onlyWhen with scalar
and $in predicates (mixed tables, where only terminal rows age out)". |
| objectstack-ai#10165 (404) | `object.test.ts:289` | "accepts ttl.onlyWhen with the
canonical null predicate — so rows whose value is absent are spared".
From `8012960508`. |
| objectstack-ai#3175 | `object.test.ts:1101` | "ownership record-model field —
declared, so the opt-out the registry reads can be authored". |
| objectstack-ai#11408 (404), objectstack-ai#10144 | `object.test.ts:1713` | "ObjectSchema editMode
(declared by maintainer ruling: the renderer reads it, so the spec
declares it)". From `f11fc61c51`, in objectstack-ai#10144's declare-or-rule-out
family. |
| objectstack-ai#14935, objectstack-ai#14637 | `object.test.ts:2135` | "isPublicSharingEnabled — the
one canonical standing share-link policy predicate". The runtime mirror
was retired. |

**Dropped where already stated** (69 literals, 73 ids). A number goes
only where the title already says its decision, for example "$field
members are refused (objectstack-ai#7596)", the four `crypto.hash` titles in
`hook-body.test.ts` (objectstack-ai#4391), the `objectstack-ai#20045 —` prefix on the four other
describes of the currency-scale file, and twenty-nine `object.test.ts`
titles such as "managedBy: retiring the overloaded `system` bucket
(objectstack-ai#3355)". `(ADR-0049)`, `(ADR-0066)`, `(ADR-0100)` and `(ADR-0087 …)`
stay: they cite decision records by number, not tracker ids.

**Three judgements, each declared:**
- `filter.test.ts:298` keeps "ruled 2026-08-31" and drops only
`(objectstack-ai#13357)`. The sibling title at `:656` names "the 2026-08-31 ruling",
so the date stays as its anchor.
- The `批 20` label stays on the four batch-20 describes, because the
file's own name and header carry it. Only `objectstack-ai#4001` and `objectui#4772`
went.
- `object-strictness-batch20.test.ts:563` drops "(objectstack-ai#5114 class)": the
title already says what is pinned, that the tombstones keep their
prescription through the strict close.

## Readers

- **Test-name filters:** none. No tracked script, workflow or config
passes `-t` / `--testNamePattern`.
- **Snapshots:** none. No `__snapshots__` directory is tracked under
`packages/spec`.
- **Projects:** none of the 17 files is listed in
`packages/spec/vitest.repo-tests.json`; all 17 run in the `local`
project.
- **By substring:** every old literal, plus a window around each id (311
needles), was searched across the tracked tree outside its own file. No
gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads
one. The 8 needle hits fall on 7 lines:
  - a code comment in `object.zod.ts:2532`;
  - two release-owned CHANGELOG lines;
- a sibling title in this card's `system/` stage
(`system/job.test.ts:471`);
- sibling titles in `cli` (`extract-hook-body.test.ts:179`),
`driver-sql` (`sql-driver-16318-numeric-representation.test.ts:64`) and
`objectql` (`engine.test.ts:831`).
  None reads a spec test title.
- **The files by name:** 96 references to these file names outside
CHANGELOGs. The gate ledgers among them (`test-typecheck-debt.json`,
`engine-double-contract.pinned.json`,
`objectql-double-limit.baseline.json`) key on the file and on error
signatures, not on a title, and each gate exits 0 at the head.
`scripts/check-org-identifier.mjs` counts `session: { … tenantId … }`
literals in `hook.test.ts`, which this PR does not touch. ADR-0129
quotes "name-as-identity", a title this PR does not change.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each changed string leaf must sit in a test-call title
position or on a declared line, must carry a tracker id before, and must
carry no `#` plus digits after. This stage declares no line: every
changed leaf is a title.

- **Result:** 17 of 17 files SAME on all three legs, with the per-file
counts predicted in writing before the run.
- **Totals:** 103 changed string leaves in 103 literals, all titles. The
diff's `+` and `-` lines are exactly the 103 planned lines, and every
file keeps its line count.
- **Controls (10 of 10 as predicted, on scratch copies, each anchor hit
once in the reported run):** identifier rename DIFF; numeric literal
DIFF; comment edit COMMENT DIFF; a non-title string given an id
VIOLATION; a rewritten title given a new id VIOLATION; a title that was
id-free at base edited VIOLATION; one title reverted to base SAME; an
`it.each` row name given an id VIOLATION; an undeclared `expect` message
changed VIOLATION; a title re-split into a `+` chain DIFF. The non-title
control's first anchor matched nothing (0 hits, so nothing ran). Its
anchor was corrected and all ten were run again.

**Test counts:** the 17 files were run at the base, in a separate base
worktree, and at the head, with `--project local --project repo`. Both
sides read 670 tests, all passed, with the same count and status
sequence per file in 17 of 17. 378 full test names change, and each
equals the base name with the planned replacements applied (0
mismatches). No full name repeats on either side.

## `main` since the base

Re-fetched just before this PR opened, `origin/main` was two commits
past the base (`9f9510f25e`: objectstack-ai#21858, objectstack-ai#21862). Neither touches any of the
17 files; the one `packages/spec` path among them is
`api/error-code-ledger.zod.ts`. So `main` was not merged. `git
merge-tree` onto `9f9510f25e` is clean. No open PR touches the 17 files.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the
17 touched files are in it, and no `*.test.ts` at all. The controls
`src/data/filter.zod.ts` and `dist/index.mjs` are in it.
- In the built `dist/`, a new phrase and an old literal each read in 0
files. The control `Unrecognized key` reads in 42.

So this PR publishes nothing, and no changeset is added.

## Verification (at `21f39afe57`)

- `pnpm turbo run build` over all packages: 71 / 71.
- `@objectstack/spec`:
  - `vitest run --project local`: 615 files, 18387 passed, 1 todo.
- `typecheck` exit 0, including `check:test-typecheck` (52 files / 246
errors / 135 pinned signatures held). Its program holds all 17 touched
files, counted with `tsc --listFilesOnly -p tsconfig.test.json`.
  - `check:generated`: all 15 generated artifacts up to date.
- **Gates:** `dispatch-gates --commands` derived 79 families, the same
set as stages 13 to 17, and all 79 exit 0. `--ran` reconciles: 79
derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit
code recorded.
- The five roster families whose rosters sit under a touched directory
were also run, and each exits 0: `check:meta-url-spelling`,
`check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`
and `check:filter-alias-parity`.
- The derivation printed `STALE TREE`:
`scripts/engine-double-contract.pinned.json` moved on `main` after the
base. This diff adds and changes no engine double,
`check:engine-double-contract` exits 0 on this tree, and the queue
re-runs it on the merged generation.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 17 files
reads 0 errors and 0 warnings. The population comes from ESLint's own
config: 17 configured, 0 ignored. No file sets `parserOptions.project`
or `projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 206 changed lines.

## Acceptance notes

- **No needle in this group.** Every id was a test title; no expected
value of an assertion over a source docblock was found. The three known
needles are untouched.
- **Same-id test titles in this card's later stages** go with those
stages: 38 lines in `packages/spec/src`, 9 in `system/` (for example
`system/job.test.ts:471`, the `job.timeout` twin of `hook.test.ts:1382`)
and 29 in `ui/` (for example the `objectstack-ai#4001 批 15` / `批 16` / `批 18` / `批 19`
describes and `ui/view-filter-rule-wire-id.test.ts`'s four `objectstack-ai#5114`
describes).
- **Same-id test titles in other packages** are their lanes' test-string
shares. A search of `describe` / `it` / `test` lines outside
`packages/spec/src` finds 140 lines citing ids this PR handled:
`objectql` 23 (13 files), `service-analytics` 18 (8), `lint` 17 (9),
`driver-sql` 11 (7), `cli` 10 (4), `runtime` 10 (7), `platform-objects`
5, `plugin-audit` 5, `plugin-security` 5, and fewer in 16 more places,
among them two `packages/spec/scripts/*.test.ts` titles (outside `src/`)
and one title each in `examples/app-crm` and `examples/app-showcase`.
- **Code comments still carry ids** in these files and their sources,
for example the `// [objectstack-ai#20150]` block at
`import-mapping-target.test.ts:17`, the `objectstack-ai#4001 批 20` header of
`object-strictness-batch20.test.ts` and `object.zod.ts:2532`. Comments
are not this card's share, and none is touched here.

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

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