Skip to content

feat(lint): startup open-vocabulary verdicts enter the lint vocabulary (#4776) - #6791

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-4776-registry-verdict-vocabulary
Aug 8, 2026
Merged

feat(lint): startup open-vocabulary verdicts enter the lint vocabulary (#4776)#6791
os-zhuang merged 2 commits into
mainfrom
claude/issue-4776-registry-verdict-vocabulary

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4776

Implements the maintainer-approved 2026-08-06 ruling, quoted verbatim:

「裁 B 先行——lint 词表门禁立项(「提供方尚未注册」与「没有提供方」的同形误判模式进词表,发单人自陈权限内);C(kernel 三态契约,动 47 插件)挂议不启动,如 B 落地后同形缺陷仍复发再提请。A(个案修复)已派发,不在本裁范围。」

Scope is packages/lint/src/** + .changeset/. Nothing in packages/objectql / core / kernel; scripts/check-startup-registry-verdict* and its gate wiring are untouched.


Premise check (the ruling's own dispatch precondition)

The ruling required confirming that the vocabulary shape covers the three cases' actual fix forms before starting. All three fixes are in-tree and took three different routes; each is a negative fixture in the test suite, and the rule passes all three:

case fix form, as it shipped where it is in this PR
#4771 flow node-type validation ran ~0.8s before ApprovalsServicePlugin registered the approval executor, asserting "will fail at execution time" seal, then judgeAutomationEngine.sealNodeTypeVocabulary() (packages/services/service-automation/src/engine.ts:4353), called by the plugin at kernel:bootstrapped; warnUnknownNodeTypes now runs only after the seal negative fixture cure 2, and the SEAL_MARKERS escape
#4772 auth probed cache 21ms before CacheServicePlugin registered it, froze the undefined, and advised "you need Redis" lazy resolutioncreateLazyCacheRateLimitStorage() (packages/plugins/plugin-auth/src/rate-limit-storage.ts) resolves inside the accessor it hands out negative fixture cure 1 (lazy)
#4769 the ADR-0104 attestation was written during the same boot that was still seeding rows contradicting it reorder the verdict after the mutation — the counterexample ledger and "tear up a creation attestation this boot has just contradicted" (packages/objectql/src/engine.ts:4414) negative fixture cure 3

Premise holds, with one honest correction to the dispatch's framing. The hypothesis A1 described a rule that re-covers the kernel service registry (getService('cache') in init()). Re-implementing that here would be a second vocabulary for one verdict — the thing AGENTS.md forbids and #5841 was filed about — because deciding it correctly needs the ADR-0116 plugin-manifest model (dependencies / requiresServices are what make "absent" a fact) that check:startup-registry-verdict already carries. So this rule targets the complement instead, and the boundary is asserted by a test rather than only claimed in prose (what this rule delegates, and to whom).

A2 — the existing gate's coverage boundary (measured, not assumed)

pnpm check:startup-registry-verdict --list, run on this branch's merge base (2026-08-08):

✓ startup registry verdicts: 40 startup/open-registry seam(s) across 1501 file(s),
  34 read-only (legal), none recording a verdict the boot can contradict.

Breakdown of those 40 seams:

region seams phase
Rule A — service-registry probe, literal service name 34 init() only (28 of 34 cleared by an ADR-0116 declaration)
Rule B — open capability registry, matched on the owning class's this.nodeExecutors / this.actionDescriptors 6 all inside service-automation/src/engine.ts
anything in a start() 0

What the gate declares out of reach, in its own header: start() for the service registry (sound: every init() has completed by then); helpers beyond the same file; non-literal service names; #4769's database-as-registry; and — implicitly — everything outside this repo, since it is a scripts/*.mjs that is never published.

This rule takes exactly that complement:

check:startup-registry-verdict this rule
corpus packages/** of this repo any source string a caller hands it — a plugin in a user's app, an AI-authored extension, cloud graph-lint
phases constructor + init; the registry's owning class constructor + init + start, from any consumer package
registry kernel SERVICE registry (literal name) + two open registries by this.<prop> open capability vocabularies matched by accessor name, so a consumer that reaches the same vocabulary through its public API is visible
evidence the shape of the record the shape of the record and the wording of the diagnostic

start() is the phase this rule exists for. The gate's reasoning for stopping at init() is correct for the service registry and false for an ADR-0018 vocabulary: a sibling plugin registers its executor from its own start(), and sibling start()s have not all run. That is literally where #4771 sat.

What landed

packages/lint/src/lint-startup-registry-verdict.tsfindStartupRegistryVerdicts(source, { file }), a pure decision procedure (parses via a lazily-loaded typescript; never executes, never type-checks, touches no filesystem). Two rule ids:

  • startup-open-vocabulary-verdict — all three of: (1) a read of an open capability vocabulary (OPEN_VOCABULARY_PROBES, 13 accessors, each carrying the reason its answer is not final), (2) inside a pre-seal phase (PRE_SEAL_PHASES: constructor / init / start), (3) with the conclusion recorded — announced in a warn/error/fatal log, cached in an instance field or module-level let, or persisted. All three, or it is not a finding.
  • startup-verdict-assertive-wording — emitted only at a site the first rule already flagged, so it can add no false positive of its own. Fires when the diagnostic asserts a terminal outcome about a world that has not finished forming, and is cleared by any hedge that keeps the two worlds apart ("not yet", "as of", "may still").

The three cures are recognised by shape, not by allowlist: nested function bodies are never descended into (so a kernel:ready handler and a lazy accessor are both invisible), and a scope mentioning a seal identifier is skipped.

STARTUP_VERDICT_HINT obeys the card's own lesson — it prescribes all three fix shapes by the change that shipped each, names #4769/#4771/#4772 as precedents, and makes no assertive claim of its own about whether the flagged site misbehaves at runtime. A hint that said "this will fail" would be the defect wearing the rule's badge. There is a test for that.

A1 — corpus measurement

The rule run over every .ts under packages/ (same file filter the CI gate uses):

count
files scanned 1502
parse-eligible after the cheap accessor pre-filter 14
startup-open-vocabulary-verdict 0
startup-verdict-assertive-wording 0
wall clock 792 ms

Zero new hits, so there is no triage table to fill in — and zero is exactly the reading a green ratchet does not earn on its own (#4690). Two things make the zero mean something:

  1. The three fixed sites do not flag, for the declared reason. service-automation/src/engine.ts is parse-eligible and clean because every judgement sits behind nodeTypeVocabularySealed; plugin-auth is clean because the read moved inside the lazy accessor; objectql's attestation is clean because the verdict now follows the mutation. Each is a negative fixture in the suite, so a regression in the escape logic fails a test rather than quietly flagging the cure.
  2. The rule demonstrably fires. lint-startup-registry-verdict.corpus.test.ts pushes a reconstructed bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 through the same sweep function the corpus goes through and asserts both rule ids come back. A change that broke matching fails there instead of turning the corpus green.

One limitation stated rather than hidden: this session's clone carries 50 commits, so the ideal proof — pointing the rule at the pre-fix commits of 2026-08-03 and watching it report — was not reachable. The reconstructed fixtures stand in for it, and they are reconstructions of the shipped defect text, not of the rule's own matcher.

Where it is enforced

packages/lint/src/lint-startup-registry-verdict.corpus.test.ts sweeps packages/** on every pnpm test in the package, behind a shrink-only, hand-edited LEDGER (empty; no --update flag, deliberately) with a companion staleness check. It refuses two false greens: a corpus that was never fully read (the root is resolved up front, the walk carries no catch, and a floor is pinned on the file count) and a rule that matches nothing (the non-vacuity case above).

This is also the answer to "who consumes an exported rule" — the closure authoring-rule-wiring.test.ts draws around Prime Directive #10. The rule is not a (stack) => Finding[] member of AUTHORING_RULES because it does not take a stack; the barrel comment says where its enforcement lives instead of leaving it advertised-only.

Deliberate non-coverage

#4769's shape is out of reach for any syntactic rule, and this PR says so in a test rather than in a comment. Its "registry" is the sys_migration table, and "no rows → write the row" is exactly what legitimate first-boot seeding looks like — a rule that flagged it would flag every seeder, and a rule people switch off is worth less than no rule because it also reports success. This is the same admission the CI gate makes; #4769 is in the family because its fix is instructive.

Reverse verification (predicted → actual)

# prediction before running actual
1 The three reconstructed defect shapes flag with both ids where wording applies #4771 → both ids; consumer-package variant → structural only (no assertive text); connector/remedy variant → both; persisted variant → structural only
2 The three fixed shapes pass ✅ all three negative fixtures clean
3 Removing the seal guard from the cure 2 fixture makes it flag ✅ that is the sweep can still fire case — same source minus the seal, both ids returned
4 Whole-packages/** sweep is clean ✅ 0/1502
5 Sweep costs enough to matter in a unit-test suite wrong — predicted seconds, measured 792 ms. The accessor pre-filter leaves only 14 of 1502 files to parse, so the ~9 MB typescript load dominates and the walk is free. That is why the sweep could land as a test rather than as another scripts/check-*.
6 lazy-deps.test.ts passes — the new module must not eagerly import typescript ✅ passes; the module uses the same deferred createRequire anchor as validate-hook-body-writes.ts
7 rule-id-barrel-exports.test.ts fails until both ids are re-exported ✅ predicted and observed — both ids are barrel-exported from index.ts
8 authoring-rule-wiring.test.ts's unwired-rule closure would demand a ledger entry wrong — it matches `/^(?:validate

Verification

  • packages/lint1680 tests / 67 files, all passing (includes the 24 new rule tests, the 4 corpus-sweep tests, and the lazy-deps / rule-id-barrel-exports / authoring-rule-wiring contracts).
  • tsc --noEmit on @objectstack/lint — clean.
  • Full workspace build (turbo run build --filter='!@objectstack/docs') — 71/71 successful.
  • Repo gate list enumerated fresh from .github/workflows/lint.yml — results in a follow-up comment on this PR.

Out of scope, per the ruling

Option C (three-state ServiceLookup, ~47 plugins) stays shelved: 「C(kernel 三态契约,动 47 插件)挂议不启动,如 B 落地后同形缺陷仍复发再提请。」 The restart condition is a recurrence after B, and this rule plus the corpus ledger is what will make a recurrence visible.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WRyk75SSknS2WriyJvA5sC


Generated by Claude Code

#4776)

A boot fills its registries incrementally, so "is X registered?" asked while
one is still filling has an answer that is simply not final yet. Turning that
not-yet into a verdict AND RECORDING the verdict is the defect: the provider
registers a moment later and nothing goes back to undo the record. One
showcase cold start produced three instances in three unrelated subsystems
(#4769 / #4771 / #4772), all since fixed individually.

This is the maintainer-ruled option B — the same-shape misdiagnosis enters the
lint vocabulary — landing where the vocabulary lives rather than as a second
copy of the CI gate's:

- `findStartupRegistryVerdicts(source, { file })`, a pure decision procedure
  over plugin source, reporting `startup-open-vocabulary-verdict` (a read of an
  ADR-0018-open capability vocabulary during constructor/init/start whose
  conclusion is announced, cached or persisted) and
  `startup-verdict-assertive-wording` (emitted only at a site the first rule
  flagged, when the diagnostic asserts a terminal outcome about a world that
  has not finished forming).
- The three sanctioned cures are recognised by shape and pass: deferral to a
  `kernel:ready` handler, lazy re-resolution, and seal-then-judge.
- `lint-startup-registry-verdict.corpus.test.ts` sweeps every `.ts` under
  `packages/` with it (1502 files, 0 findings) behind a shrink-only ledger, and
  pushes a reconstructed #4771 through the same sweep so a green ratchet can be
  told apart from a dead one (#4690).

The kernel SERVICE-registry half stays with `pnpm check:startup-registry-verdict`
(#4777 / PR #4833) and is untouched; the rule module states the measured
division of labour — that gate reported 40 seams across 1501 files, 0 of them
in a `start()`, which is the phase this rule exists for.

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

vercel Bot commented Aug 8, 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 8, 2026 5:57pm

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

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.

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

Copy link
Copy Markdown
Contributor Author

Local gate sweep — enumerated fresh from .github/workflows/lint.yml

Every pnpm check:* / pnpm lint invocation the workflow runs, extracted from the file on this branch (not from memory) and run one by one. check:type-check-debt ran after a full turbo run build --filter='!@objectstack/docs' --concurrency=2 (71/71 tasks successful). NODE_OPTIONS=--max-old-space-size=4096 throughout.

Repo-level (39): adr-anchors · app-nav-i18n · authz-resolver · doc-authoring · docs-audit-scope · driver-conformance · driver-memory-census · durability-log-level · engine-double-contract · error-code-casing · i18n · i18n-coverage · init-service-contract · kernel-hook-pairs · merge-driver · meta-type-normalized · node-version · nul-bytes · objectui-changeset · org-identifier · platform-checklist · published-files · query-options-erasure · quick-reference-counts · release-body · release-notes · resume-authority-declared · role-word · route-envelope · service-providers · shard-attestation · skill-compatibility · skill-frame-sync · slot-lookup · spec-parsed-alias · stall-guard · startup-registry-verdict · type-check-coverage · type-check-debt · verify-stand-in · wildcard-fallthrough · workflow-status-functionsall PASS.

Package-filtered (15): @objectstack/lint check:doc-formula-expressions · @objectstack/spec × {authorable-surface, docs, generated --reconcile-only, react-blocks, skill-docs, skill-refs, spec-changes, upgrade-guide, api-surface, dual-source-exports, exported-any, skill-examples, tsc --noEmit} · @objectstack/downstream-contract typecheckall PASS.

pnpm lint (ESLint) — PASS. pnpm check:adr-0087-registration — PASS (the changeset is minor, not declared-breaking, so no disposition marker is required).

Tests

suite result
@objectstack/lint 1680 passed / 67 files — includes the 24 new rule tests, the 4 corpus-sweep tests, and the lazy-deps / rule-id-barrel-exports / authoring-rule-wiring contracts
@objectstack/metadata-protocol (the other @objectstack/lint consumer) 671 passed / 60 files
tsc --noEmit on @objectstack/lint clean

Rebased onto current main

origin/main had moved 5 commits ahead (e1e76294bb6f01) while this work was in flight. Merged in and re-verified against the widened corpus:

  • rule + corpus tests: 28 passed
  • corpus sweep: 1505 files, 0 findings, 848 ms (was 1502 / 0 / 792 ms — the three new source files carry no vocabulary accessor, so they are pre-filtered out before parsing)

Generated by Claude Code

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

Projects

None yet

2 participants