Skip to content

test(spec): the first api/ file group's test titles state each cited decision in words instead of a tracker number (stage 24) - #21961

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20749-test-strings-o
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20749-test-strings-o

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20749
Clause-②: no

Stage 24 of this card: the next area of class (e), the test strings shipped under packages/spec/src, as ruled in 5902360492 on #20513. This stage takes the first name-ordered api/ group: the 27 id-bearing test files directly under packages/spec/src/api/ from ai-agents-envelope.test.ts to package-lifecycle.test.ts. Those files carried 100 messages and 106 tracker ids, citing 65 records. All 106 now either state what their record decided, in words (form D), or are dropped where the title already says it. No needle sits in this group. Text only: no assertion, identifier, test count or code comment changes, and no file is renamed.

Census at the base (a3bd157730)

Instruments: census10.cjs (md5 9d08602ab972b4b8643c90d64d40fa41), census.cjs (md5 6e42a45a926d375013c32d62f16a296e), census-wide.cjs (md5 c98410a19529c439adb0afbfb00026a2) and dirtable.cjs (md5 dda605c54745b4a60cc14c9a686e4eff), byte-identical to the copies stages 10 to 23 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 worktree was cut from origin/main at a3bd157730, the claim's base and stage 23's landing. Both instruments read 471 messages / 498 ids in 111 files, the seat's reading and stage 23's head reading.

directory files messages / ids titles other
api/ (this PR: 27 of the 40 files) 40 189 / 201 181 / 193 8 / 8
system/ 34 154 / 167 128 / 138 26 / 29
(files directly in src/) 30 118 / 120 117 / 119 1 / 1
ui/ 5 7 / 7 0 7 / 7
ai/ 1 2 / 2 0 2 / 2
contracts/ 1 1 / 1 0 1 / 1
total 111 471 / 498 426 / 450 45 / 48

The group reads 100 messages / 106 ids in 27 files, the seat's figures file for file:

file (under api/) messages / ids titles other
ai-agents-envelope.test.ts 1 / 1 1 / 1 0
analytics.test.ts 3 / 3 3 / 3 0
api-entry-graph.pin.test.ts 1 / 1 1 / 1 0
api-error-code-type.test.ts 1 / 1 1 / 1 0
apis-publish-gates.test.ts 12 / 12 12 / 12 0
auth-endpoints.test.ts 2 / 2 2 / 2 0
auth.test.ts 2 / 2 1 / 1 1 / 1
automation-api.zod.test.ts 4 / 5 4 / 5 0
batch.test.ts 2 / 2 2 / 2 0
contract.test.ts 3 / 3 3 / 3 0
dataset-selection.test.ts 5 / 5 5 / 5 0
discovery-auth-families.pin.test.ts 2 / 2 2 / 2 0
discovery-environment-subset.pin.test.ts 2 / 2 1 / 1 1 / 1
discovery.test.ts 10 / 11 10 / 11 0
dispatcher.test.ts 2 / 2 2 / 2 0
endpoint.test.ts 4 / 4 4 / 4 0
envelope-violations.test.ts 1 / 1 1 / 1 0
error-code-ledger.test.ts 7 / 11 7 / 11 0
errors.test.ts 3 / 3 3 / 3 0
export-job-family-retirement.test.ts 6 / 6 3 / 3 3 / 3
export.test.ts 3 / 3 3 / 3 0
meta-item-response-shapes.test.ts 2 / 2 2 / 2 0
metadata.test.ts 1 / 1 1 / 1 0
odata-orderby-dual-declaration.test.ts 1 / 1 1 / 1 0
package-api.test.ts 10 / 10 10 / 10 0
package-install-one-authority.test.ts 1 / 1 1 / 1 0
package-lifecycle.test.ts 9 / 9 9 / 9 0
27 files 100 / 106 95 / 101 5 / 5

Five more test files sit in the same name range and carry no id (documentation.test.ts, error-catalog-docs.test.ts, events.test.ts, http-cache.test.ts, odata.test.ts). The five "other" strings are expect messages, rewritten and declared to the text-only tool: auth.test.ts:155, discovery-environment-subset.pin.test.ts:65 (one leaf of a + chain) and export-job-family-retirement.test.ts:112 (a template literal), :158 and :349.

  • Controls. Lit: ui/notification.test.ts (1 id) and api/protocol.test.ts (50 ids), outside the group, read the same at the base and at the head. Dark: package-api.test.ts reads 0 at the head while 30 of its comment lines still carry a number. Planted in a scratch tree: an id put into a package-lifecycle.test.ts title reads 1 / 1 (title:describe), and an id put into a batch.test.ts comment reads 0.
  • A wider pattern (any # plus digits) reads the same as the gate pattern in all 27 files at the base, and 0 in all 27 at the head.
  • At the head: 371 messages / 392 ids in 84 files. The 27 files read 0 / 0, api/ reads 89 / 95 in 13 files, and no other file moved.

How the area was chosen

api/ has no subdirectory, so it is taken in name-ordered file groups near the ~100-id bound, the rule stages 20 to 23 used. Stage 23's cut named this group at 106 ids, and this census reads 106, so no re-cut was needed.

Named for the next stages (cut from the head census, 371 / 392):

  • The second api/ group: plugin-rest-api.handler-status-retirement.test.ts through zod-issues-to-fields.test.ts, 13 files, 89 messages / 95 ids (86 / 92 titles, 3 / 3 other), protocol.test.ts alone 46 / 50 and rest-server.test.ts 19 / 19. That finishes api/.
  • system/ 167, two stages. The files directly in src/, 120, one.
  • The needles: the three docblock needles, the kept ui/component-props-unknown-members.pin.test.ts:322 and stage 22's two. One stage, with an at-tier review. The four colour literals stay, as stage 21 decided.

What each id became

  • 18 literals (22 ids) now state a decision in words.
  • 10 literals (10 ids) get their subject back in words, where the number stood for a thing.
  • 72 literals (74 ids) drop a number the title already explains.

Every cited record was fetched with all its comments through REST (357 comments, #4052's included), and its decision was read from its ruling, ACCEPT and landing comments. 65 records are cited: 59 answer 200 and 6 answer 404. Two of the 200s are PRs (#4049 and #20218), read from their bodies. One citation is objectui's and was read from objectui: objectui#6593. The six that answer 404 were read from what landed, through the commits endpoint (this checkout is shallow), each named by the commit the stage-2 re-anchoring of api/ comments gave it:

One citation names a different record. batch.test.ts:78 read "(#3963 follow-up)"; #3963 is the api.requireAuth retirement. The validateOnly tombstone is #4052's decision, read too: never implemented, so tombstoned rather than half-built. The title already says that ("rejects the retired validateOnly key with its prescription"), so the number is dropped.

Where a record's decision was refined later, the title follows the refined one:

Stated in words:

record literal (under api/) now reads the decision
#18576 api-entry-graph.pin.test.ts:77 "… stays off the assembled package body (ruled: split the entry rather than watch its weight)" Ruling B (batch #145 item 1, maintainer 2026-09-17): the cost is removed, not watched; ./api is split and the assembled-package declarations move to @objectstack/spec/api-assembled.
#4936 apis-publish-gates.test.ts:152 "still accepts an EMPTY and an ABSENT apis: — never refused, even while a non-empty one was" Maintainer ruling 2026-08-04: v17 loudly refuses a non-empty apis: and keeps the vocabulary; an empty or absent one stays publishable, then and after #5111's narrowing.
#4910 apis-publish-gates.test.ts:568 "keeps endpoint-level rateLimit in the vocabulary (ruled: left to the endpoint executor, not the server-level seam)" Q2 = B (2026-08-03): that card wires the server level only; the endpoint-level keys stay, and #4936's ruling has the endpoint executor wire them.
#5189 apis-publish-gates.test.ts:597 "still refuses D6 — the gate with no runtime counterpart, so the per-item publish path runs it too" Triage disposition (E7b, 2026-08-04): publishPackage reuses the same gate function, because D6 alone has no runtime counterpart.
#7481 auth-endpoints.test.ts:112 "AuthFeaturesConfig retired flags (ruled: stop advertising them)" Maintainer ruling 2026-08-11: passkeys / magicLink leave the /api/v1/auth/config payload.
#14788 auth.test.ts:88 "SessionUser.language retirement (ADR-0049 — ruled: gone, with no replacement field)" Maintainer ruling D (2026-09-03): retired under ADR-0049, no producer and no consumer; no replacement field until a real producer exists.
#9378, #9510 automation-api.zod.test.ts:327 "… status, runId and the screen (a pause is the third state, not a failure)" #9510's ruling (2026-08-18): a pause is not a failure, and callers learn the third state deliberately; status: 'paused' + runId + screen is the trigger contract's third state.
#4828 discovery.test.ts:1167 "scoping (ruled: declare what REST actually emits)" Maintainer ruling 2026-08-05, item 3: scoping is declared on DiscoverySchema as an optional key.
#4828 discovery.test.ts:1207 "resolveDiscoveryEnvironment (ruled: an enum, not a passthrough)" Item 4: the schema is authoritative, so every producer's environment is mapped into the declared enum.
#8211 error-code-ledger.test.ts:68 "standard-synonym detection (ruled: refused unless waived)" Option C (triage adjudication, 2026-08-12): the admission gate refuses a semantic synonym of a standard member unless a recorded waiver admits it; the four existing ones are waived.
#10025, #11504 error-code-ledger.test.ts:220 "accepts the definition-level input-schema refusal code (ruled non-retryable: a never-dispatched exit)" Maintainer ruling B (2026-08-20): the refusal is non-retryable and becomes a never-dispatched exit with its own ADR-0112 code.
#16449, #16404 error-code-ledger.test.ts:234 "accepts the nine-code batch — every code that ships in dist, door or no door (ruled: the ledger is the published face)" #16404 option D (batch #62, 2026-09-07): the ledger is the published face, so every code in dist is registered; #16449 registered the nine.
#16649, #16404 error-code-ledger.test.ts:308 "accepts the fourteen remaining door:none codes, each under its stamping package (ruled: the ledger is the published face)" The same ruling; 613bfbd3db registered the fourteen.
#17158 export-job-family-retirement.test.ts:158, :349 (expect messages) "… the retirement is being undone — nothing served, bound or consumed the family" Ruling A (batch #122 item 3, 2026-09-12; landing route A, batch #221 item 2): ADR-0049 retires a declared API that nothing serves, binds or consumes.
#12038 package-api.test.ts:603 "package-rollback-response retirement (ruled: it described the wrong operation on the live path)" Ruling 3A (2026-08-27): the published version-rollback schema, bound to the live commit-rollback path, is retired first.
#12038 package-lifecycle.test.ts:25 "the ruled re-export of PackagePublishResultSchema into the /api namespace" Ruling 5A: re-export the existing schema into the namespace the ledger resolver searches, never a second copy.
#12038 package-lifecycle.test.ts:140 "RollbackToPackageCommitResponseSchema declares the COMMIT-rollback body (ruled: authored once the wrong-operation schema was retired)" Ruling 3A's binding sequence: retire the false declaration, then author the true commit-rollback schema.

Subject back in words (10 literals): "the pre-#4053 bare body" becomes "the bare body from before the envelope relocation" (#4053's end state: both producers relocated the payload under data); "(#3891 shim dialect)" becomes "(the degraded shim dialect)"; "the duplicate-payload drift #4049 removed" becomes "the duplicate-payload drift the /share-links domain stopped emitting", the PR's own title; "zero holders after #17158" becomes "after the export-job family retirement"; "the #10330 TS2353 repro" becomes "the original TS2353 repro"; the three "since PR #20218" titles become "since the door parses its whole body" (twice) and "so does the door, which parses the whole body", the PR's own title; the #17534 title now names "the reverse-domain id rule", that card's ruling A; "the objectui#6593 confusion" becomes "the envelope-vs-payload success confusion", the defect objectui#6593 measured.

Dropped where already stated (72 literals, 74 ids). A number goes only where the title already says its decision. Examples: the eight [#5111] describes ("the flip — a well-formed apis: publishes", "gate (a)" to "gate (e)", …), [#5310], [#19920], the two [#21046], [#5676], [#5672], [#5679] and [#6287] prefixes; the four #17551 / #17550 section prefixes in dataset-selection.test.ts, which keep the file's own §1 to §5; #5384 —, #5227 —, #5950, #5882, #17518, #18058 — and #18605 —; the four #15677 citations on the "→ …Seconds" renames; and the tails (#3878), (#6442), (#19543) x2, (#7359), (#3939), (#18124), (#3842) x3, (#10338), (#6704), (#10330), (#4587), (#17667), (#19116), (#17431), (#19441), (#8211), the five (#12038) and the one (#12038 4A) after "declares the four fixed keys and stays open". (federated ledger, #4805), (ADR-0076 D12, #2462) and (ADR-0112 amendment 2026-08-18, #9266) keep their words and lose the number. The ADR-0087 conversion id api-endpoint-cache-ttl-to-cache-ttl-seconds stays: it is not a tracker id.

No file is renamed.

Readers

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 the five expect-message lines named above.
  • Result: 27 of 27 files SAME on all three legs, with the per-file counts predicted in writing before the run.
  • Totals: 100 changed string leaves in 100 literals: 95 titles and 5 declared. The diff's + and - lines are exactly the 100 planned lines as multisets, and every file keeps its line count.
  • Controls (14 of 14 as predicted on the first run, on scratch copies, each anchor hit once): 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 given an id VIOLATION; an undeclared expect message changed VIOLATION; a title re-split into a + chain DIFF; a declared expect message reverted to base SAME; a declared expect message given a new id VIOLATION; a declared +-chain leaf given a new id VIOLATION; a template-literal message given a new id VIOLATION.
  • Templates and tables: no .each title and no $name placeholder changes. The one template literal, export-job-family-retirement.test.ts:112, changes only its text after ${name}.

Test counts: the 27 files were run at the base, in a separate base worktree, and at the head, with --project local --project repo. Both sides read 831 tests in 27 files, all passed, with the same count and status sequence per file in 27 of 27. 325 full test names change, and each changed name equals the base name with the planned replacements applied: 0 mismatches once the plan's text is read the way the source writes it (the comparison tool reads the plan's — escape at errors.test.ts:439 literally, so its first pass reports that title's three names as mismatches; decoding the escape, as vitest does, reads 0). No full name repeats on either side.

Changeset: skip-changeset

Measured, not assumed:

  • npm pack --dry-run of @objectstack/spec lists 2068 files. 0 of the 27 touched files are in it, and no *.test.ts at all. The controls src/api/package-lifecycle.zod.ts, src/api/error-code-ledger.zod.ts and dist/index.mjs are in it.
  • In the built dist/, a new phrase and an old one each read in 0 files. The control Unrecognized key reads in 42.

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

Verification (at dffd240655)

  • pnpm turbo run build over all packages: 71 / 71, through the shared verify lock (VERDICT command-exit 0).
  • @objectstack/spec:
    • vitest run --project local: 619 files, 18471 passed, 1 todo.
    • typecheck: exit 0, including check:test-typecheck (52 files / 246 errors / 135 pinned signatures held). Its program holds all 27 group files, counted by path with tsc --listFilesOnly -p tsconfig.test.json.
    • check:generated: all 15 generated artifacts up to date, against the dist/ the build above wrote.
  • Gates: dispatch-gates --commands derived 80 families: stage 23's 79 plus check:error-code-casing, which the two touched files it names bring in. All 80 exit 0. --ran reconciles: 80 derived, 80 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. The same 80 derive from origin/main 01e0f71ad8 with this diff applied. The roster families stage 23 also ran (check:meta-url-spelling, check:spec-changes, check:authz-resolver, check:filter-alias-parity) each exit 0.
  • ESLint, a proven narrowing: --no-inline-config over the 27 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 27 configured, 0 ignored. No file sets parserOptions.project or projectService, so no untouched file's verdict can move.
  • check-governed-merges --test: NOT governed, 200 changed lines (+100 / -100).
  • A control-byte scan over the 27 changed files finds none.

main since the base

Re-fetched just before this PR opened, origin/main was two commits past the base (01e0f71ad8: #21940, #21953). They touch 31 files, none of the 27 and none under packages/spec, so main was not merged and the census on that tree is the base's. git merge-tree onto 01e0f71ad8 is clean, and none of the 5 open PRs touches any of the 27 files.

Acceptance notes

  • Same-id test titles in this card's later stages go with those stages: 23 lines in packages/spec/src, among them api/protocol.test.ts ([#5672] x2, (#12038) x5, (#12038 1C), (#19543, door ③)), api/plugin-rest-api.test.ts, api/router.test.ts and api/websocket.test.ts ((#15677)), stack-json-stage-package-body.test.ts (#17518 x4), system/book.test.ts ((#12038)) and three system/ titles citing (#18124).
  • Same-id test titles in other packages stay: 96 lines in 12 packages (runtime 37, rest 24, client 9, metadata-protocol 6, service-automation 6, metadata 5, cli 3, objectql 2, and one each in examples/app-showcase, core, plugin-hono-server and verify), each package's share under the [finding] runtime warnings outside the migration ledger print tracker numbers to authors and operators: the AutomationEngine resumeAuthority boot warning (#3801 / #5561 / #3823) and two objectql data-event warnings (#4639 / #4626) #20513 lane children.
  • Code comments with live ids remain in these files and their sources, among them the // package-rollback-response retirement (#12038 3A) banner above its describe, the [#5111 / #5040 E7] and [#5189 / #5040 E7b] headers in apis-publish-gates.test.ts, and the [#17158] header in export-job-family-retirement.test.ts. Code comments are not this card's share.

Generated by Claude Code

…decision in words instead of a tracker number (stage 24)

Stage 24 of the spec lane's share of the runtime-string burn-down: the
test strings shipped under packages/spec/src, the first name-ordered
api/ group (ai-agents-envelope.test.ts through package-lifecycle.test.ts).

100 literals in 27 files (95 test titles and 5 declared expect messages)
lose 106 tracker ids: 18 literals (22 ids) now state the cited decision
in words, 10 (10 ids) get their subject back in words, and 72 (74 ids)
drop a number the title already explains. No needle sits in this group.

Text only: one line per literal, no assertion, identifier, test count,
code comment or file name changes.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 6, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 6, 2026
@github-actions github-actions Bot added the tests label Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 01e0f71ad8518ed208dc95eb7bb6bf2b2fc2fa05 → packageMentionDocs.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 07:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 07:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 5a22eb5 Oct 6, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20749-test-strings-o branch October 6, 2026 07:58
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ce row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host default (objectstack-ai#21965)

Part of objectstack-ai#21922
Fixes objectstack-ai#21944
Clause-②: no (narrowing)

## What changes

A code-defined datasource (a `*.datasource.ts` the installed artifact
declares, or the host's own `default`) is read-only by published
contract: `DatasourceSchema.origin` says "code — authored as
`*.datasource.ts`, GitOps-owned, read-only in the UI", the datasource
registry entry in `metadata-plugin.zod.ts` says code-defined datasources
"win on name collision", and `datasource-admin-service.ts` says "A
runtime datasource never shadows a code one (code wins on collision)".
The datasource-admin plugin's boot restore broke all three, and the
metadata door could not see `default` at all.

The fix is the one host-owned set of code datasources the two cards'
triage asked for ("One set serves both, so do not build two"):

- **The set** (`packages/runtime/src/code-datasource-names.ts`, new).
One in-memory `Set` of the datasource names the host registers from
code, on the kernel service `code-datasource-names`.
`contributeCodeDatasourceNames` registers it on first use and adds to it
after that, the shape `seed-summary` uses.
- **Its producers, both in `init()`.** `AppPlugin.init()` adds every
datasource the artifact declares: the same list its `start()` registers
in the MetadataService, now memoized so the two phases read one answer.
`DefaultDatasourcePlugin.init()` adds `default`. Phase 1 completes
before any `start()`, so the set is whole before the restore runs,
whatever order the plugins were composed in.
- **The restore** (`restoreRuntimeDatasources`,
`packages/services/service-datasource/src/datasource-admin-plugin.ts`).
A stored row under a name in the set is not registered over the code
definition. It is kept, and one boot warning names it with the repair.
The warning goes to the host's `options.logger`, or to the kernel logger
when the host passes none (`os serve` passes none).
- **The resolver** (`isDeclaredCodeDatasource`,
`packages/metadata-protocol/src/protocol.ts`, nothing else in that
file). It reads the same set beside the installed packages, so the
metadata door answers `default` the way it answers every code-defined
datasource since PR objectstack-ai#21942.

⛔ "Code" is never read from a stored row's `origin`, the MetadataService
slot's `origin`, the connection service's `ConnectResult`, or a request
body's `origin`.

## Measured on a booted showcase

The harness is the `@objectstack/verify` `bootStack` with the
datasource-admin routes mounted the way `serve.ts` mounts them, in a
temp cwd. The stored rows assert `origin: 'runtime'` and their own
`config.filename` (the cards' case (b)). They were written through the
metadata door's repository on the runtime-only intent, then the stack
restarted. BEFORE is `76fec88b16`; AFTER is this branch at `1d840709ae`.
The readings come from a throwaway probe that was never committed; the
committed pins below assert the AFTER column.

| Reading after the restart | BEFORE | AFTER |
|---|---|---|
| admin list, `showcase_external` | `origin: runtime`, label "Shadow
21922" | `origin: code`, "External Analytics (SQLite)" |
| `PATCH /api/v1/datasources/showcase_external` | 200 | 400
`DATASOURCE_ADMIN_ERROR` "… is code-defined and cannot be edited at
runtime." |
| live pool named `default` | a second pool opened on the stored row's
file; verdict `already-registered` became `connected` | none; verdict
stays `already-registered` |
| `showcase_ext_customer` read | 3 rows (code fixture) | 3 rows (code
fixture) |
| boot warning naming each stored row | none | one per row |
| `PUT /api/v1/meta/datasource/default` | 200 "Saved datasource
'default'" | 403 `NOT_OVERRIDABLE` |
| `DELETE /api/v1/meta/datasource/default`, no stored row | 200 | 403
`NOT_OVERRIDABLE` |
| `DELETE /api/v1/meta/datasource/showcase_external` (repair), then meta
`GET` in the same boot | 200, but the meta `GET` kept serving the stored
edit until the next restart | 200, and the meta `GET` serves the code
definition |

## The dispatch's mechanism hypotheses

- **H1, start order.** In all three compositions that load
`service-datasource` (`serve.ts`, `standalone-stack.ts`, the verify
harness), `DefaultDatasourcePlugin` and `AppPlugin` are `use()`d before
`DatasourceAdminServicePlugin`. None of the three declares an ordering
edge to another, so their `start()`s run in insertion order: the code
registrations did land before the restore, but by list position alone,
which ADR-0116 says proves nothing. The answer is the first branch: the
set is filled by a phase that precedes the restore (`init()`). It is
pinned by a boot whose reader plugin is composed first, ahead of every
producer. The restore pin also covers a code registration that lands
after it.
- **H2, the seam.** It is a kernel service read through the services
registry the protocol already resolves (`getServicesRegistry()`), with
no `packages/spec` change. The ObjectQL registry was rejected: the
engine's datasource definitions mix both origins, and a host package
record would be a fabricated provenance.
- **H3, the admin refusal.** Measured, not assumed. The pins' stored
rows carry `origin: 'runtime'`, and the slot the refusal reads holds
AppPlugin's explicit `origin: 'code'`. Under ablation A the admin door
served the stored row as `origin: runtime`, so the refusal cannot come
from the admin read's `origin ?? 'code'` default.
- **H4, the live pool.** For `default`: yes. The restored row reached
`rehydratePools`, which opened a second pool named `default` on the
row's file. Routing did not move, because the engine never routes to a
driver named `default`; the default driver keeps its natural name. It is
the same defect and the same decision fixes it, pinned by
`getDriverByName('default')` and the connect verdict. For
`showcase_external`, nothing was re-pointed at boot in this composition:
AppPlugin's connect ran first, so the rehydrate answered
`already-registered`. The admin `PATCH` 200 was the open door to a
re-point (an update that changes connectivity rebuilds the pool), and it
is now refused.

## Seam and the Clause-② limb (for the seat)

- **Published exports added: none.** `code-datasource-names.ts` is not
re-exported from `packages/runtime/src/index.ts`. `service-datasource`
and `metadata-protocol` spell the service name privately and read the
value structurally as `has(name)`, the way `'datasource-connection'` is
read today.
- **What the seam does add is one kernel service entry**,
`code-datasource-names`, which two packages read by name. Whether that
is the claim's "service contract" limb is the seat's call. The line
above stays as the claim wrote it, and the changeset grades all three
packages `minor`, which holds under either reading.

## Named gap: the metadata door's read while a stored row exists

`GET /api/v1/meta/datasource/:name` still serves a stored row under a
code-defined name for as long as the row exists. The door reads its
stored overlay first (ADR-0005's read order), whatever the
MetadataService holds. The AFTER boot measured it: the admin door served
the code definition while the meta `GET` served the stored row, for
`showcase_external` and for `default`. So triage's pins "both doors
serve the code definition" and "removes it with no change to what is
served" hold for the admin door. For the metadata door they hold once
the repair `DELETE` has run, in the same boot. That read lives in
`getMetaItem`'s overlay step, outside `isDeclaredCodeDatasource`, and
`protocol.ts` is held by objectstack-ai#21934 in other regions, so it is left to the
seat. `meta-door-code-datasource.dogfood.test.ts` already pins that read
as it is.

objectstack-ai#21922 stays open for that read: this PR is `Part of` it, and the seat
routes the remaining half through triage when it merges.

## Landing beyond the claim's named files

`packages/runtime/src/app-plugin.ts` is the producer of the packages'
half of the set, in the declared `runtime` package. The memo also makes
its residual-owner warning print once instead of once per phase.
`packages/runtime/src/code-datasource-names.ts` is new in the same
package.

## Patch round 1 (head `d77e150701`)

Review `6011282321` on objectstack-ai#21922. The Tests, Ablations and Gates sections
below are round 0's, at `80fbcfdea6`; this section carries the readings
on the current head.

- **The plugin-dev pin (CI red on round 0).** `AppPlugin.init()` now
contributes its datasource names as a function that the host's
code-datasource set resolves at its first read. The set is still
contributed to only in Phase 1, before any `start()`. The resolution is
deferred because the names come from the artifact's `collections`, which
walk `packages[]`. `AppPlugin.init()`'s manifest registration is the one
thing in `init()` allowed to touch `packages[]`, pinned by
`plugin-dev`'s malformed-stack falsifier, which this PR's first head
turned red. The kernel service now holds a `CodeDatasourceNames`, a set
with pending contributions; readers still use `has(name)` only. A
contribution that throws stays pending and rethrows to every reader.
- **The `default` refusal names what defines it**: the host's database
configuration (the database URL the server starts with). It names no
`*.datasource.ts`, because none declares `default`. Every
package-declared datasource's sentence is byte-identical, and `code`,
`status` and the refused set are unchanged (`packaged-base-regime.ts`,
the datasource row's `hostOwned`).
- `const listOf = (` spacing restored in `app-plugin.ts`. Merged
`origin/main` `80f9f7e6ba` as `49421a8fe6`.
- **Ablation D:** the old sentence put back for `default` turned 6 unit
cases and 1 dogfood case red, and was restored by blob.
- **Tests at `d77e150701`** (each `VERDICT command-exit 0`):
  - `service-datasource`: 748 / 748;
  - `metadata-protocol`: 27978 passed, 19 skipped;
  - `runtime` local: 4668 passed, 19 skipped;
  - `plugin-dev`: 86 / 86;
  - the two dogfood files: 11 / 11;
- downstream consumers of `runtime` (cli, client, verify,
http-conformance, cloud-connection): all passed;
  - typechecks for the five packages: green.
- **Gates at `d77e150701`:** `dispatch-gates --commands` derived 72,
reconciled with `--ran` as 72 run and 0 not measured, plus
`check:init-service-contract` and `check:startup-registry-verdict`. All
exit 0.
- **Docs:** the 18 hand-written pages the Docs Drift Check lists were
read page by page. None states anything this PR makes false, so no docs
are edited.

## Tests (head `80fbcfdea6`)

- `pnpm --filter @objectstack/service-datasource exec vitest run
--maxWorkers=2`: 41 files, 748 passed.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2`: 218 files passed, 3 skipped; 27976 tests passed, 19
skipped.
- `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2
--project local`: 331 files, 4658 passed, 19 skipped.
- Dogfood, `--project isolated`:
`datasource-restore-code-wins.dogfood.test.ts` (new) and
`meta-door-code-datasource.dogfood.test.ts`, 2 files, 11 passed.
- `typecheck` green for `service-datasource`, `metadata-protocol`,
`runtime` (including its `check:test-typecheck` ledger, held) and
`dogfood`. `tsc --listFiles` counts each touched test file once in its
program.
- Each package's run includes its new pins: 5 in
`datasource-admin-plugin.test.ts` (the restore), 4 in
`code-datasource-names.test.ts` (the set and its phase), and 2 resolver
plus 8 door cases in `protocol.code-defined-datasource-door.test.ts`
(`default`, on both kernel shapes).

## Ablations

Each one was committed first and mutated with
`scripts/ablation-replace.mjs` in wrap mode, under a shell trap. For
subjects resolved through `dist/`, the package was rebuilt and
`ablation-dist-preflight.mjs` proved the marker was present. The restore
leg was rebuilt and proven `--absent`, the blob equalled HEAD, `git diff
HEAD` was empty, and the tree was clean.

- **A, the restore registers over a code name**
(`datasource-admin-plugin.ts`; marker in 2 files of
`service-datasource/dist`). Unit: 3 failed, 16 passed. The slot served
the stored row, the warning was not called, and the order-independent
case registered the row. Dogfood: 2 failed, 3 passed. The admin list
served `showcase_external` as the stored row, and the repair case read
the same.
- **B, the resolver does not know the set** (`protocol.ts`; marker in 2
files of `metadata-protocol/dist`). Unit: 5 failed, 30 passed (the
resolver case, and `PUT` plus no-row `DELETE` of `default` on both
kernels). Dogfood: `PUT /meta/datasource/default` answered 200. The next
case then failed as a cascade, because the row that `PUT` stored made
the seed conflict. The repair `DELETE` and runtime controls stayed
green, as expected.
- **C, AppPlugin's `init()` contribution deleted** (`app-plugin.ts`; the
subject resolves from source). The reader-first boot saw `['default']`,
not `['app_wh', 'default']`. So the set comes from `init()`; the
`start()` registration never fills it.

## Gates (head `80fbcfdea6`)

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 72 commands on the actual change, a
superset of the 52 at dispatch. All 72 ran with exit codes captured
before any pipe. `--ran` printed "72 derived famil(ies) accounted for —
72 run, 0 NOT-MEASURED".

- `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET`
(exit 3) because eight packages outside this diff had no `dist/`. After
building them it measured green.
- Two families the derivation does not name were also run, both green:
`check:init-service-contract` ("34 declared / 1 self-provided / 3
without a workspace provider") and `check:startup-registry-verdict`
("none recording a verdict the boot can contradict").
- `check-changeset-no-major`'s level axis needs a PR payload, so CI
reads it.
- Lint is a proven narrowing, not the repo-wide run. `eslint
--no-inline-config --format json` on the 9 touched source and test files
reported 9 files, 0 errors and 0 warnings. `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, no typed rules,
as its own comment states), so this diff cannot move any untouched
file's verdict.

## Acceptance notes

- **The `default` refusal's remedy** names the host's database
configuration (patch round 1).
- **Cluster convergence.** `convergePool` reads a stored row directly
and is unchanged. Its signals come from peer admin writes, and the admin
door now refuses those for code names.
- **The restore's other warnings** (a failed read, a failed register)
still go only to `options.logger`, which `os serve` does not pass. They
are unchanged here.
- **objectstack-ai#21923 remains open.** This diff does not touch
`listDatasourceRecords`, `getDatasourceRecord` or
`persistDatasourceRow`. One interaction: a metadata-door-created
datasource with no `origin` still restores, and is still read as `code`
by the admin door's default.
- **Main drift.** `origin/main` gained objectstack-ai#21956, objectstack-ai#21964 and objectstack-ai#21961 (spec
and docs-qa only) after the round-1 merge. None touches a file here, and
the queue's merged generation is the check.

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

---------

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

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants