Skip to content

fix(devx): init-service 门禁认全部服务访问器,不只 getService (#4835) - #4902

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4835-init-contract-getserviceasync
Aug 3, 2026
Merged

xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4835-init-contract-getserviceasync

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Collaborator

Fixes #4835

为什么是「词表漏了一个入口」,而不是别的

scripts/check-init-service-contract.mjs(#4471 / ADR-0116)问的是一个问题:插件在 init() 期间解析了别的 workspace 插件提供的服务,却没有声明排序,对不对?它只对一个 callee 名字问这个问题:

if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'getService') {

而 kernel 解析具名服务有三个入口(读 packages/core 实际导出,不是照议题措辞抄):

访问器 声明处 说明
getService(name) PluginContext(core/src/types.ts) 同步读注册表 —— 原有唯一词条
getServiceAsync(name, scopeId?) ObjectKernel(core/src/kernel.ts:505) 插件经 ctx.getKernel() 可达;#4772 里直接对 ctx 做了 duck-typing
getServiceScoped(name, scopeId) PluginContext(core/src/types.ts:53) kernel 实现体与 getServiceAsync 逐字相同:this.pluginLoader.getService< T >(name, scopeId)

排序风险是注册表的属性,不是某个方法名的属性;ADR-0116 的 dependencies / optionalDependencies / requiresServices 对三者一视同仁。所以修的不是某个调用点、不是判定语义、也不是加宽豁免 —— 是词表本身少了两条,门禁在三分之一的面上回答了自己的问题。

#4772 正是从这里溜过去的。 修复前的 AuthPlugin.init()(f2eb85007^,auth-plugin.ts:346):

cache = await (ctx as { getServiceAsync?: (n: string) => Promise< unknown > }).getServiceAsync?.('cache');

cache 由 CacheServicePlugin(providesServices = ['cache'])提供;AuthPlugin 当时的声明是 requiresServices = ['data', 'manifest'] + dependencies = ['com.objectstack.engine.objectql'],没有任何一条覆盖 cache。这是本门禁存在的意义所对应的那个判定,而它连这条边都没有构造出来。代价见 #4772:21ms 的冷启顺序差把 undefined 冻进 better-auth 配置,限流计数永远到不了共享存储(ADR-0069 D2 宣称了运行时没交付的能力)。

双向证明

只观察到绿的门禁,与一个什么都匹配不上的门禁,从外部无法区分(#4690、#4804 两次先例)。所以下面两段是同一个 fixture、同一条真实命令,只有脚本词表不同。

fixture 是把 f2eb85007^ 的 AuthPlugin.init() 原样放回 packages/plugins/plugin-auth/src/__issue4835-repro.ts(可选调用 + 强转 ctx + best-effort try/catch,一字不改),验证完即删除,不在本 PR 的 diff 里。

之前(main 的词表,只有 getService)——判绿

$ git stash push scripts/check-init-service-contract.mjs   # 脚本回到 main
$ node scripts/check-init-service-contract.mjs
✓ init-service contract: 41 declared / 1 self-provided / 3 without a workspace provider (75 plugin unit(s) scanned).
exit=0

$ node scripts/check-init-service-contract.mjs --list | grep -c "__issue4835-repro"
0

注意 75 plugin unit(s)(基线是 74):fixture 文件被扫到了,类也被识别成了插件单元 —— 只是那次调用完全不可见,一条边都没构造。这正是 #4772 当时的处境:门禁跑了,绿了,什么都没看见。

之后(本 PR 的词表)——判红,且指得出插件、服务、行号

$ git stash pop
$ node scripts/check-init-service-contract.mjs
✗ init-service declaration guard (#4471, ADR-0116)

  packages/plugins/plugin-auth/src/__issue4835-repro.ts:17 — AuthPlugin
    init() resolves getServiceAsync('cache') (directly or via a helper init() calls),
    'cache' is provided by 'com.objectstack.service.cache', and NOTHING declares that ordering.
    This is the #4085/#4420 failure class: it works only under lucky composition order,
    and the miss hides inside best-effort logging. Declare it (ADR-0116):
      - dependencies: ['com.objectstack.service.cache']           if this plugin cannot run without it;
      - optionalDependencies: ['com.objectstack.service.cache']   if it degrades on purpose when the
        provider is not composed (declared tolerance — the #4460 reference shape);
      - requiresServices: ['cache']                           if the service must exist at init regardless
        of which plugin provides it.
    If the consumption can wait until every init() has finished, move it to start().

1 undeclared init-time service consumption(s).
exit=1

插件(AuthPlugin)、服务(cache)、提供者(com.objectstack.service.cache)、行号(:17,即调用行而非类声明行)四项齐全。

内建 self-test 也做同一个证明

self-test 从 12 例扩到 19 例。case 13 就是上面那个形状的内存版,并断言消息里含插件名、accessor 原文、提供者名和调用行号。把词表缩回 ['getService']:

$ sed -i "s/…'getService', 'getServiceAsync', 'getServiceScoped'…/…'getService'…/" scripts/check-init-service-contract.mjs
$ node scripts/check-init-service-contract.mjs --self-test
✗ self-test: #4772 pre-fix getServiceAsync shape is caught (got 0 problems)
exit=1

也就是说,这几条 case 是真的在判别词表,而不是陪跑。

--list 输出的修正

边名此前把调用点硬编码写成 getService('X'),与实际 callee 无关。词表一扩,这个输出就会把 getServiceAsync 的调用点标成 getService —— 一个会说谎的机器可读面(Route & surface ownership §4)。现在每条边记录自己的 accessor,--list 与报错文案都按原文引用:

undeclared   packages/plugins/plugin-auth/src/__issue4835-repro.ts:17  AuthPlugin → getServiceAsync('cache')

self-test case 18 钉住这一点(三种 accessor 混排,断言记录顺序 getService,getServiceAsync,getServiceScoped)。

哪些没有加进词表,以及为什么

议题提示「hasService 之类如果确认存在就一并加」。核对 packages/core 的实际导出后,没有加:

  • hasService —— 不在插件可达面上。ObjectKernel.hasAnyService 是 private(kernel.ts:602);PluginLoader.hasService 虽然随 export * from './plugin-loader.js' 导出,但 kernel 的 loader 实例是私有的,插件拿不到。加它只会误伤别的对象上同名的方法,而覆盖不到任何真实边。self-test case 19 把这条「不臆造」钉住。
  • getServices() —— 枚举整张表,调用点没有服务名字面量,静态无从判定(与既有的动态服务名 case 11 同理)。
  • replaceService(name, impl) —— 是写不是读。它确实有自己的排序要求,但判定与补救都不同,混进来会让一条消息回答两个问题。

顺带把 scan() 的预过滤从硬编码 'getService' 改为从词表派生:今天三个名字碰巧都以 getService 开头,下一个加进来的未必,那会在 AST 看到它之前就把文件过滤掉 —— 与本 issue 是同一类静默洞。

现存代码仍然全绿

$ pnpm check:init-service-contract
✓ self-test: 19 cases
✓ init-service contract: 41 declared / 1 self-provided / 3 without a workspace provider (74 plugin unit(s) scanned).

边数与 main 一致(41/1/3),没有因为扩词表而把现存代码判红 —— 今天 packages/** 里的 getServiceAsync 调用点(rest/src/rest-server.ts、runtime/src/http-dispatcher.ts、runtime/src/dispatcher-plugin.ts)都在请求期路径上,不在任何插件的 init() 里;getServiceScoped 在 packages/** 里只出现在 core 自身的定义与转发处。这是补一个潜伏的洞,不是报出一个现存的红。

npx eslint scripts/check-init-service-contract.mjs 干净。脚本是 .mjs,不进 tsc --noEmit 覆盖面,typecheck 不受影响。附纯 tooling changeset(空 frontmatter,按 adr-anchors-guard.md / check-i18n-fails-on-undeclared-authoring-key.md 先例,不发版)。


Generated by Claude Code

…getService (#4835)

`scripts/check-init-service-contract.mjs` (#4471, ADR-0116) matched one callee
name — `getService` — while the kernel resolves named services through three:
`getService` (PluginContext), `getServiceAsync` (ObjectKernel) and
`getServiceScoped` (PluginContext, whose kernel body is the same
`pluginLoader.getService(name, scopeId)` call `getServiceAsync` makes). The
ordering hazard belongs to the registry, not to a method name, so the guard was
answering its own question about a third of the surface.

#4772 went through that gap: pre-fix `AuthPlugin.init()` (`f2eb85007^`) resolved
the workspace-provided `cache` service via
`(ctx as { getServiceAsync?: … }).getServiceAsync?.('cache')` with nothing in its
declarations covering it — the exact verdict this guard prints — and the edge was
never constructed.

- `SERVICE_LOOKUP_CALLEES` is now the named vocabulary, with membership argued
  per accessor from `packages/core`. `hasService` stays out: `hasAnyService` is
  private and `PluginLoader.hasService` is not reachable from a plugin.
- The `scan()` pre-filter derives from that set instead of hardcoding a
  substring that only accidentally covers today's names.
- Each edge records the accessor it was made through; `--list` and the failure
  message quote it as written instead of normalising every reader to
  `getService('X')`.
- Self-test grows to 19 cases. 13 is the #4772 pre-fix shape verbatim (optional
  call, cast `ctx`, best-effort try/catch) and asserts plugin, provider and call
  line in the message; narrowing the set back to `['getService']` turns it red.

The repo audit stays green — today's `getServiceAsync` call sites are all on
request-time paths, in no plugin's `init()`.

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

vercel Bot commented Aug 3, 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 3, 2026 3:07pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling size/m and removed documentation Improvements or additions to documentation tooling labels Aug 3, 2026
@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 15:10
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit b29ead6 Aug 3, 2026
18 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4835-init-contract-getserviceasync branch August 3, 2026 15:14
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ntity (objectstack-ai#8968)

The publish smoke went straight from install to `objectstack dev`, so it never
ran `npm run build` in either mode. That is where every "published scaffold is
broken" incident so far actually surfaced (objectstack-ai#4902, objectstack-ai#7644, objectstack-ai#8677): in all of them
scaffold and install exited 0 and only the build exited 2, so the gate was
structurally unable to see them. Scheduled registry-canary run objectstack-ai#1932 concluded
success on 2026-08-10 against a create-objectstack whose scaffold failed first
build on four templates.

Both modes now run the project's own build script and assert exit 0, plus a
non-empty dist/objectstack.json so a build that emits nothing cannot read as a
pass. Scope is the bundled `blank` template — the scaffolder's whole catalog
since the five remote templates were delisted.

Also assert the scaffolded namespace differs from the template's own. The
identity rewrite only does anything when the two differ, and objectstack-ai#7644 was a
rewrite that silently did nothing: measured against create-objectstack@16.1.0,
the build reports 4 namespace-prefix errors when they differ and 0 when the
project is named after the template. `smoke-app` differing from `blank` was
accidental; it is asserted now.

Measured cost on GA 17.0.0: build 3s on top of an install already being paid.

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…kages/create-objectstack/src to the commits that decided them (objectstack-ai#20748)

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

## What changed

This is stage 11 of the `domain:cli` lane of the dead-citation sweep:
`packages/create-objectstack/src`. Every comment site there that cited a
tracker number answering 404 now cites, in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), the commit in this repository's history that
decided what the line describes, and keeps saying in its own words what
that commit decided. PR objectstack-ai#20533 is the method, and stages 1 to 10 of this
card (PR objectstack-ai#20624, PR objectstack-ai#20632, PR objectstack-ai#20656, PR objectstack-ai#20673, PR objectstack-ai#20689, PR objectstack-ai#20703,
PR objectstack-ai#20713, PR objectstack-ai#20723, PR objectstack-ai#20735, PR objectstack-ai#20741) are the precedents. The card
stays open for the lane's remaining packages, so this PR says `Part of`.

That is **25 sites on 25 lines in 10 files, covering 9 numbers**,
rewritten to **8 distinct commits**:
- the census's **3 sites**: `src/banner.ts` 2, `src/index.ts` 1 (2
numbers);
- **22 test-file comment sites** in 8 test files (the census defers
`*.test.ts`; stages 1 to 10 took test comments too):
`starter-comments-self-contained.test.ts` 9,
`scaffold-e2e-boot-probe.test.ts` 3, `banner-version.test.ts` 2,
`blank-readme-validate-disclosure.test.ts` 2,
`scaffold-next-steps-pm.test.ts` 2, `template-consistency.test.ts` 2,
`scaffold-skills-single-copy.test.ts` 1, `template-ci-workflow.test.ts`
1.

Only comments changed: **25 lines out, 25 in**, every one of them a site
(no companion line), and every touched file keeps its line count (147 /
67 / 58 / 617 / 910 / 261 / 357 / 328 / 221 / 745), so no line citation
into these files moves. **No citation number is added**: the added lines
carry no tracker number at all, and no PR number stands on an added
line. No ADR or ruling-record file in `docs/adr/` or
`scripts/adr-anchors/` records any of these 9 decisions (a grep for the
9 numbers there reads 0 hits; the control number `objectstack-ai#7329` reads 2 in the
same tree), so every anchor is a commit.

**No changeset; `skip-changeset`.** The rewritten comments do not reach
the published `dist` (measured below), as in stages 5 and 7 (PR objectstack-ai#20689,
PR objectstack-ai#20713).

**Scaffold output is untouched.** No site sits inside a template literal
or in a file the scaffolder copies: `src/templates/**` carries zero
tracker citations in either projection, and all 25 sites are `//` or
JSDoc comment prose outside any string. A real scaffold run at base and
at head emits a byte-identical project (below).

## Census: `packages/create-objectstack`, before and after

**Instrument.** The gate's own `node scripts/check-issue-citations.mjs
--census --json`, read-only and unchanged, run under `with-fleet.sh
--read` for the token. The count is its `allocated-but-absent` findings
under `packages/create-objectstack/`. Both runs enumerated the whole
board.

| reading | tree | board | whole-repo `allocated-but-absent` | package
sites | lines | numbers | files |
|---|---|---|---|---|---|---|---|
| before | base `01e78dceef`, run 2026-09-30T01:14:08Z to 01:19:48Z |
enumerated, 186 pages, frontier objectstack-ai#20742, 18,569 numbers | 1,077 | **3** |
3 | 2 | 2 |
| after | `4ed638093d`, run 01:30:22Z to 01:35:31Z | enumerated, 186
pages, frontier objectstack-ai#20745, 18,572 numbers | 1,074 | **0** | 0 | 0 | 0 |

The whole-repo drop of 3 is exactly these sites: a site-by-site diff of
the two JSON outputs has 3 findings gone (`banner.ts:10`,
`banner.ts:17`, `index.ts:441`) and none added. The other three tallies
(`resolves` 33,014, `resolves-as-pull-request` 1,984,
`cross-repo-unjudged` 995) are equal in both runs.
`packages/create-objectstack` is byte-identical at `4ed638093d` and at
the head (the one merge brought no file under it).

**Supplementary scan (test files, strings and files outside `src/`
included).** The gate's exported `extractCitations` and
`classifyCitation` over all 53 tracked files of the package
(`CHANGELOG.md` excluded), comment-prose and whole-file projections,
with the board from the gate's own `probeBoard`: 77 citations and 33
dead before, 52 and 8 after. Under `src/`: comments 3 dead to 0, test
comments 23 to 1, test strings 7 unchanged; `src/templates/**` 0
citations of any kind. Outside `src/`, one citation
(`vitest.config.ts:24`, `objectstack-ai#10374`) answers 200. Its before list of `src/`
comment sites equals the census's. The 8 left are 7 test strings and 1
test comment with no deciding commit (see "The site left" and Acceptance
notes).

## Per-number table

`git blame` at the base ties each line to the commit that wrote it, and
each anchor was read in its message, changeset or diff, not only its
subject.

| number | sites (base line) | anchor: what it decided |
|---|---|---|
| `objectstack-ai#10325` | `banner.ts:10`; `banner-version.test.ts:3` | `cec9d239d`:
the startup banner reads the real version from `package.json` through
the new `renderVersionBanner()`, and sizes the box from the version's
plain length, widening and never truncating, instead of the hardcoded
`v6.x`. Both lines blame to it; its message carries the closing trailer
for this number. New anchor. |
| `objectstack-ai#10322` | `banner.ts:17`; `index.ts:441`;
`banner-version.test.ts:17`;
`blank-readme-validate-disclosure.test.ts:3`, `:50`;
`scaffold-next-steps-pm.test.ts:3`, `:7` | `8d21f7a76`: detect the
package manager once, up front, and name it in the install line, the
install-failure remedy and every "Next steps" line (labels padded to the
longer of the two instead of hand-kerned for `npm`), and name `validate`
in the blank README's "Getting started". Its message carries the closing
trailer for this number. `index.ts:441` and the two test headers blame
to it; `banner.ts:17` and `banner-version.test.ts:17` blame to
`cec9d239d`, whose message calls this "the sibling bug fixed one
function away in the same file"; `scaffold-next-steps-pm.test.ts:7`
blames to `c6c7feccd`, a re-wrap that keeps the sentence. New anchor. |
| `objectstack-ai#19424` | `scaffold-e2e-boot-probe.test.ts:397`, `:679`, `:816` |
`c27e16059`: the boot-probe neighbour announces its own listener (or its
bind error), asks the kernel for its port with `listen(0)`, and the
harness names five distinct outcomes instead of one "never came up"; the
controls block pins each. All three lines blame to it; its message
carries the closing trailer for this number. New anchor. |
| `objectstack-ai#16331` | `scaffold-skills-single-copy.test.ts:3` | `fd75728bc`:
install the skills bundle for one agent (`--skill '*' --agent
claude-code -y`) so a scaffolded project's first commit stages it once,
with no symlinks. The line blames to it, and its diff is what added the
number; its message names none. New anchor. |
| `objectstack-ai#10990` | `starter-comments-self-contained.test.ts:41`, `:283` |
`21756b325`: converge the shipped template files on the ruled canonical
docs origin and pin that convergence as assertion 4 over
`shippedFiles()`. Both lines blame to it; its message carries the
closing trailer for this number. New anchor for this number. |
| `objectstack-ai#11022` | `starter-comments-self-contained.test.ts:50`, `:91`,
`:122`, `:221` | `21756b325`: rewrite the blank README's two
monorepo-only references, add the fifth `MONOREPO_ONLY` pattern (the
framework's own name next to a "repo" word), retire the self-retiring
`EXCLUDED` entry and add the README's two RATIONALE facts. All four
lines blame to it. Stage 3 (PR objectstack-ai#20656) gave this number the same anchor.
|
| `objectstack-ai#15150` | `starter-comments-self-contained.test.ts:72`, `:133`,
`:141` | `cc986c913`: the sixth `MONOREPO_ONLY` pattern, for a reference
written as a relative path that climbs out of the project, anchored on
bare `../` rather than on a depth judgement. All three lines blame to
it; its diff is what added the number (8 times, across both scaffolders'
pins), its message names none. New anchor. |
| `objectstack-ai#16330` | `template-ci-workflow.test.ts:3`;
`template-consistency.test.ts:376` | `4998efa71`: ship
`.github/workflows/ci.yml` in the blank template (the template's first
dot-directory) so a scaffolded project has gates from its first push.
Both lines blame to it; its diff added the number, its message names
none. New anchor. |
| `objectstack-ai#10326` | `template-consistency.test.ts:498` | `675ab574e`: declare
the two benign peer skews a clean first install reported as scoped pnpm
`allowedVersions` inside the scaffold. The line blames to it. Stage 3
(PR objectstack-ai#20656) gave this number the same anchor. |

**Anchor checks.** Every cited sha matches exactly one object (`git
rev-parse --disambiguate`, count 1 for each of the 8), is a commit, has
one parent, and is an ancestor of `main` (`merge-base --is-ancestor`
against `01e78dceef`, exit 0 for all 8). The checkout is not shallow.
The control leg `2aca1bc4c0` (the parent of the oldest anchor
`675ab574e`, 2026-08-20) exits 0 against the base, and the negative
control (the base as an ancestor of `675ab574e`) exits 1. Two anchors
reuse the landed stages' (`21756b325`, `675ab574e`); six are new.

**Numbers.** All 9 dropped numbers answer 404 by REST (probed
2026-09-30T01:11:14Z and again at 01:50:21Z). The one number kept on a
line beside the changed ones, `objectstack-ai#9779`
(`scaffold-e2e-boot-probe.test.ts:673`), answers 200. The anchor
commits' own PR numbers are not cited: three of them (objectstack-ai#11030, objectstack-ai#11013,
objectstack-ai#11191) answer 404 as well, which is the reason the ruling cites
commits.

## The site left

**No deciding commit (1 site, a test comment, so not in the census):**
`template-consistency.test.ts:153` (`objectstack-ai#11048`): "admitting them is a
support decision (objectstack-ai#11048), not a value to drift here". The number names
an open support decision (whether to admit pnpm 10.0 to 10.4). The only
commit naming it, `568de194e`, files it unassigned; no later commit
decides it, and the floor is still pnpm 10.15 or later at the base.
Stage 3 (PR objectstack-ai#20656) left the sibling site
`packages/cli/src/commands/init.ts:267` for the same reason.

## Mechanical guard: no code token moves, and nothing emitted moves

**H2 holds on both readings: the parser-token diff is empty, and the
emitted `dist` and the scaffolded project are byte-identical.**

**Token guard.** It compares the TypeScript parser's leaf tokens
(TypeScript 6.0.3, `getChildren` walk, JSDoc nodes excluded) of the 10
touched files at base `01e78dceef` and at `4ed638093d`. Controls mutate
the head text in memory only.
- Real run: 16,198 base tokens, 0 files differing, exit 0.
- Comment-insertion control: 0 differing, exit 0.
- Code-insertion control: all 10 files differ, exit 1.
- String control (the first character of the first import specifier
flipped in each file): all 10 files differ, first differing kind
`StringLiteral`, exit 1.

All 50 changed lines (25 out, 25 in) are `//` or `*` comment lines.

**Emitted `dist`.** `pnpm --filter create-objectstack build` at base
(before any edit) and at `4ed638093d`, after the same dependency build.
All 24 `dist` files (`index.js`, `chunk-ZIUW7UEA.js`,
`created-summary.js`, `created-summary.d.ts` and the 20 copied template
files) have equal sha256 at base and head, and `diff -r` is empty. None
of the dead numbers appears in the base `dist` at all: tsup drops these
comments.
- Code-mutation control (`scripts/ablation-replace.mjs`, wrap mode,
anchor `Dependency installation failed.` hit 1 to 0, planted marker 0 to
1, blob `b68538942c96` to `860de8778f10`;
`scripts/ablation-dist-preflight.mjs` found the marker in
`dist/index.js`): `index.js` differs from the head build. The blob was
restored to HEAD `b68538942c96` with `git diff HEAD` empty, `dist` was
rebuilt, the preflight in `--absent` mode reads the marker absent from
all 24 files with a clean tree, and the 24 sha256 values equal the first
head build.
- The whole-workspace builds (below) left `create-objectstack`'s `dist`
equal to the same 24 values.

**Scaffold output.** `node
packages/create-objectstack/bin/create-objectstack.js demo-app
--skip-install --skip-skills`, run in an empty directory from the base
build and again from the head build: both emit the same 21 files with
equal sha256, `diff -r` is empty, and the printed output differs only in
the absolute target directory line.

A raw scan of the 10 changed files for ASCII control bytes finds none (a
positive probe on a scratch file with one such byte reads 1), and
`check:nul-bytes` exits 0.

## Changeset

**None; `skip-changeset`.** The package's `files[]` is `dist`,
`README.md` and `CHANGELOG.md`; the build above emits a byte-identical
`dist` at base and head, and the code-mutation control proves that build
does move when code moves. The two other shipped files are untouched, so
this diff publishes nothing.

## Gates (head `a84b73af13`)

This host has no `flock`, so `os-verify-lock.sh` ran in its declared
unlocked mode. Its official wording, verbatim (printed by every run; the
command line differs per run and is listed in the verdicts below):

> **Declared narrowing — verification ran UNLOCKED.**
`scripts/pm/os-verify-lock.sh`
> could not take the shared verify lock on this host: no usable `flock`.
The shared
> verify lock is declared Linux-only (`flock` is util-linux, and a stock
macOS does
> not ship it), so the command below was run directly, without the lock
—
> a declared narrowing, not a silent one. No serialization guarantee
held for this
> run, nor for any sibling agent in this container while it ran.

Its verdict line from each run (the closure build and the base build at
`01e78dceef`; the head build, the first whole-workspace build, the
tests, the boot-probe file and the typecheck at `4ed638093d`; the second
whole-workspace build, tests and typecheck at this head after the
merge):

```text
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 22s · declare it in the PR body · pnpm --workspace-concurrency=2 --filter 'create-objectstack^...' build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter create-objectstack build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 1s · declare it in the PR body · pnpm --filter create-objectstack build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 134s (2m14s) · declare it in the PR body · pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 8s · declare it in the PR body · pnpm --filter create-objectstack exec vitest run --maxWorkers=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter create-objectstack exec vitest run --maxWorkers=2 src/scaffold-e2e-boot-probe.test.ts
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter create-objectstack typecheck
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 35s · declare it in the PR body · pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 7s · declare it in the PR body · pnpm --filter create-objectstack exec vitest run --maxWorkers=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter create-objectstack typecheck
```

- **Build:** `create-objectstack`'s dependency closure
(`@objectstack/spec`, its only workspace dependency), then the package,
then the whole workspace, `turbo run build --filter=./packages/*
--filter=./packages/*/* --concurrency=2`, 71 of 71 tasks, before and
again after the merge. The tree was clean after each.
- **Tests:** `vitest run --maxWorkers=2`: 16 files, 247 tests: 233
passed and 14 skipped, at this head and at `4ed638093d`. The 14 skipped
are the whole of `scaffold-e2e-boot-probe.test.ts` (run alone: 1 file
skipped, 14 tests skipped), which its own `RUNNABLE` gate
(`process.platform === 'linux'`, plus `bash`, `curl`, `openssl`) skips
on this macOS host. **NOT MEASURED locally:
`scaffold-e2e-boot-probe.test.ts`, reason: Linux-only by its own gate;
CI runs it.** Its diff is 3 comment lines with identical parser tokens.
- **Typecheck:** `pnpm --filter create-objectstack typecheck` (`tsc
--noEmit`) exits 0 at this head and at `4ed638093d`. `--listFiles`
reaches 26 `src/` files outside `src/templates/`, including all 16 tests
and all 10 touched files.
- **Spec artifacts:** not run. Neither `origin/main`'s one incoming
commit nor this diff touches `packages/spec`.
- **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`)
exits 0 at this head (2026-09-30T02:00:09Z to 02:00:43Z), and at
`4ed638093d` (01:49:31Z to 01:50:04Z).
- **Citation judging:** after merging `origin/main` (`697845d19f`),
`node scripts/check-issue-citations.mjs --base origin/main` reports "no
issue citations added against 697845d (2 file(s) read)" (exit 0).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 52 families, the same
list at `4ed638093d` and at this head. All 52 exit 0 at this head in one
pass, and `--ran` with the exit-coded record reads "52 derived, 52 run,
0 NOT-MEASURED, 0 UNRUN" (a derived zero). Among them:
`check:issue-citations`, `check:doc-authoring`, `check:nul-bytes`,
`check:published-files`, `check:cross-package-test-inputs`,
`check:dts-closure`, `check:dual-build-cjs-loads`,
`check:type-check-debt`, `check-changeset-no-major`.
- **Artifact rosters:** 36 of the 39 non-self-test roster rows exit 0 at
this head, among them `check:scaffold-emission-policy` and the three the
derivation marks as keeping their roster under one of this diff's paths
(`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`). The other three need a pull request's
context; they are run against this PR once it exists and reported on the
card. The 18 self-test-only rows grade their checkers' fixtures and
cannot judge this diff.

## Hypotheses (measured first)

- **H0 holds.** At base `01e78dceef` the filtered census answers 3 sites
on 3 lines, 2 numbers, 2 files, as on the seat's `0be898499f`. The
whole-repo count is 1,077.
- **H1 holds.** After the rewrite, the filtered census answers 0 for
`packages/create-objectstack`. No census site was left for an open PR
(the file lists of all open PRs were read at 2026-09-30T01:21:44Z and
again at 01:52:53Z, 8 PRs each time: only the Version Packages PR objectstack-ai#20639
touches the package, in `CHANGELOG.md` and `package.json`) or for an
unfound anchor. The one site left for an unfound anchor is a test
comment, outside the census.
- **H2 holds.** The parser leaf-token diff of all 10 touched files is
empty with its controls firing, and, independently, the emitted `dist`
and the scaffolded project are byte-identical at base and head, with a
code-mutation control that changes `dist`.

## Acceptance notes

- **Strings, the form-D stage.** Seven dead numbers remain in string
literals, all test titles in `src/`: `banner-version.test.ts:66` and
`:96` (`objectstack-ai#10325`), `blank-readme-validate-disclosure.test.ts:25`
(`objectstack-ai#10322`), `scaffold-e2e-boot-probe.test.ts:829` (`objectstack-ai#19424`),
`scaffold-next-steps-pm.test.ts:173` and `:197` (`objectstack-ai#10322`),
`template-consistency.test.ts:503` (`objectstack-ai#10326`). They stay on the card for
its form-D stage; no string moved here. None is an assertion text or
scaffold output.
- **Outside `src/**`:** nothing dead. The one citation there,
`vitest.config.ts:24` (`objectstack-ai#10374`), answers 200; `README.md` and `bin/`
carry none.
- **Live but misdirected numbers, a different class.** Two numbers in
this package answer 200, but as unrelated pull requests. `objectstack-ai#4902`
(`index.ts:165`, `:239`; `rewrite-identity.ts:36`;
`runtime-image.ts:140`; `rewrite-identity.test.ts:3`, and the test title
at `:123`) was written by `8d41998b0`, whose own message names `objectstack-ai#4926`
(the remote-template object-name rewrite being silently skipped), and
`f2f09e4e3` repeated it at `runtime-image.ts:140`; `objectstack-ai#4902` itself is an
unrelated `init-service` guard PR. `objectstack-ai#3120` (`template-copy.ts:20`;
`template-consistency.test.ts:259`) was written by `3b6ef8a32` (the
scaffolded `.gitignore`), and `objectstack-ai#3120` is an unrelated approvals-docs PR.
The census reads both as `resolves-as-pull-request`, a reading and not a
finding, and this card is about 404s, so neither moved here. Noted, not
filed.
- **Card-word residue, cited nowhere.** Some rewritten test headers
still say "the card" or "per triage" nearby
(`banner-version.test.ts:13`,
`blank-readme-validate-disclosure.test.ts:3`). They cite no dead number,
so they were left, as the landed stages left theirs.
- **The moving `origin/main`.** The branch merged `origin/main` once
(`a84b73af13`, merging `697845d19f`: PR objectstack-ai#20742, the `service-package`
citation re-anchoring). Nothing under `packages/create-objectstack` or
`packages/spec` changed, so the package's tests, typecheck, every
derived gate, the roster rows and lint were rerun at the merge head and
all read as before.

## Deviations

- **Three derived gates first read NOT MEASURED.**
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt` exited 3 (PREREQUISITE NOT MET: built output
absent) in the first pass, before the whole-workspace build. Rerun after
it, each exits 0, and all 52 exit 0 in the single pass at this head.
- **The first code-mutation attempt was void.** Its replacement text
contained the anchor, so the anchor count could not fall;
`ablation-replace.mjs` refused it (anchor 1 to 1, exit 1) and restored
the blob to HEAD before anything was built. The second attempt, with a
replacement that does not contain the anchor, is the one reported above.
- **The two builds inside the code-mutation control** (the mutate leg
and the restore leg) ran directly, not through `os-verify-lock.sh`. On
this host that wrapper runs unlocked anyway, so nothing was serialized
either way.
- **Commit trailers** are AGENTS.md's model-free pair (`Claude-Session`
plus `Co-authored-by: Claude`), and the pre-push trailer check passed on
every push. The harness's attribution reminder asked for a model-named
trailer and a different PR footer, and AGENTS.md overrides it. The merge
commit carries git's default message.

---
_Generated by [Claude
Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_

Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check:init-service-contract 只认 getService,不认 getServiceAsync —— #4772 就是从这个洞里溜过去的

2 participants