Skip to content

spec(lint): honour the datasource runtime-create declaration at the publish door — the reading says group C, dispatched and silent - #19596

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-19568-datasource-runtime-create-classification
Sep 21, 2026
Merged

os-steve merged 2 commits into
mainfrom
claude/issue-19568-datasource-runtime-create-classification

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19568

Clause-②: yes

The reading, first — and it is neither of the two arms the card offered

The card asks which group datasource belongs to: A (wire the rule) or D (retire the declaration). Measured against the ruling's own group criteria, it is neither — it is group C, the ledger-driven arm, which that same ruling folds into group A's card. Both offered arms are refuted, and the second refutation is the one that decided the shape of this PR.

Group D is refuted by its own criterion. That arm is for a declaration where 「no stack collection exists to create into, so the declaration is a promise nothing can keep」. ObjectStackDefinitionSchema.datasources is a first-class stack collection (packages/spec/src/stack.zod.ts:275), the type has a live runtime create path (ADR-0015 Addendum, origin: 'runtime'), and RUNTIME_CREATE_ALLOWED_TYPES in packages/metadata-protocol/src/protocol.ts is derived straight from the registry entry, so PUT /api/v1/meta/datasource/NAME really does mint one. Retiring the flag would withdraw a capability the platform ships.

The skill hold-out is refuted, and this is the precedent the dispatch asked me to test against. skill stayed out of the sibling landing #19542 because validateAiToolReferences reads a skill's tool references against stack.tools and stack.actions — collections the runtime door's snapshot does not carry — so the door produced that rule's unresolved finding while the same rule over the whole stack produced none. Wiring it would have shipped a false advisory into Studio; the hold-out is written up on card #19527. datasource's only candidate rule reaches into no collection at all: lintLivenessProperties judges each written item's own top-level keys against that type's liveness ledger, so the door's verdict and the whole-stack verdict are the same value by construction. That is asserted in this PR as a comparison, not argued: the same written datasource judged once in the shape a per-write snapshot has and once with every collection the door omits present and populated, with an anti-vacuity leg so two empty lists cannot pass for agreement.

⇒ wiring is honest here, and it lands as the email_template / mapping shape: dispatched and silent.

Evidence, re-measured on origin/main 1f69917c5c (after PR #19517 landed), not adopted from the card

leg reading
registry entries parsed with a multi-line-aware scanner 27 entries · 22 carry allowRuntimeCreate: true
of those 22, entries written MULTI-LINE 1 — datasource, at metadata-plugin.zod.ts:930. The card's mechanism holds at this commit.
allowRuntimeCreate: true types named by no live runtimeTypes declaration in packages/lint/src 6 — datasource, external_catalog, translation, doc, tool, skill
lit control, same scan declarations naming object: 12
dark control, same scan declarations naming zzznotatype: 0
candidate rules naming datasource anywhere in packages/lint/src 1 — lint-liveness-properties.ts:502, the row { type: 'datasource', key: 'datasources' }, carried since #4487
packages/spec/liveness/datasource.json 12 props, 0 authorWarn rows (the three textual hits are prose inside notes)
lit control, same instrument object.json warns on externalSharingModel

Of the six, datasource was the only one with no carrier: the others are the ruling's group B readings, its group D retirement, and the #19527 hold-out.

What landed

⚠️ Dispatched and silent, on purpose. With 0 warn keys no datasource document can be advised at this door today, so the behavioural acceptance the sibling card used — a real write refused, a good write passing — is not available for this type, exactly as it was not for group C. The same fence applies: the wiring is the whole deliverable and ⛔ no ledger-population work rides with it. The silence is pinned beside a lit control on the same instrument in the same process, so it can never be read as a broken dispatch or an unresolvable ledger directory.

One place this proof is LARGER than its two siblings'. Group C's stack keys rest on a single string assertion, because with an empty warn map no behavioural case can tell 'mappings' from a 'mapping' typo. This file closes that gap for its own row: it drives the real rule through its ledger-directory seam over a stack built at stackKeyForType('datasource') itself, with the wrong-key leg asserted beside it. Ablated (ablation-replace, on-disk blob change proven, restore proven byte-identical to HEAD):

mutation result
runtimeTypes loses 'datasource' 3 red — the dispatch pin, the door pin, the roster pin
datasource: 'datasources' becomes datasource: 'datasource' 2 red — including the behavioural stack-key case, which is what group C could not manage

What this does NOT reach, stated rather than left to be discovered

The Setup wizard's own route — POST /api/v1/datasources — persists through DatasourceAdminService.createDatasource, which writes via metadata.register plus a direct sys_metadata row, and never reaches saveMetaItem. So this crossing honours the /meta door (REST, MCP, an AI author), not the wizard's dedicated route. The two doors enforce disjoint check sets today; that asymmetry is real, is outside this card's file surface, and is written up under Acceptance notes rather than repaired here.

Verification

  • pnpm --filter @objectstack/lint test — 108 files, 4079 passed, 5 skipped.
  • pnpm --filter @objectstack/lint typecheck — green, test layer included.
  • pnpm --filter @objectstack/spec test — 509 files, 14895 passed.
  • The publish-gate blast radius, not the rule registry's package graph: pnpm --filter @objectstack/metadata-protocol test (185 passed, 3 skipped), pnpm --filter @objectstack/objectql test (303 files, 5050 passed), pnpm --filter @objectstack/rest test (194 files, 3254 passed, 1 skipped) — the packages that drive saveMetaItem and declare no dependency on @objectstack/lint.
  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated — all 15 generated artifacts up to date; the registry comment moved none.
  • pnpm lint (repo-wide, eslint . --no-inline-config) — exit 0.
  • Derived gate families (scripts/pm/dispatch-gates.mjs, reconciled with --ran): 86 derived, 83 run green, 3 NOT MEASURED, 0 unrun. The three are check:dual-build-cjs-loads, check:i18n and check:type-check-debt, each exiting 3 — PREREQUISITE NOT MET, a whole-repo build this run did not have. ⛔ Not failures and ⛔ not passes; CI builds first and runs all three. All measured at a2596caebf.

Acceptance notes

  • The two runtime-create doors for datasource enforce disjoint check sets. assertDatasourcePoolSupported's own docblock says it is 「Called at every door a pool block can come in through — the Setup wizard's create/update, the boot-time auto-connect pre-pass, and the driver factory itself」, and the /meta write door is a door it does not list: a datasource minted there is Zod-parsed and gated by the authoring rules, but never sees assertValidConfig, assertDatasourcePoolSupported or the code-origin collision refusal that createDatasource performs before persisting. The record lands in sys_metadata and is met at connect time instead, which is the outcome createDatasource's own comment says it exists to prevent. Outside this card's file surface (packages/services/service-datasource, packages/metadata-protocol) and a different defect class, so it is reported for filing rather than repaired here.
  • Group D's criterion is measurably false for one of its two members. The ruling retires doc and external_catalog because 「no stack collection exists to create into」. At 1f69917c5c there is no externalCatalogs collection, so that holds for external_catalog — but ObjectStackDefinitionSchema.docs exists (stack.zod.ts:431). Whoever takes the group D retirement should re-read the premise for doc before flipping the flag. Carrier: the group D retirement card. Noted, not filed.
  • lintLivenessProperties re-reads the ledger directory on every call — resolveLivenessDir() plus one readFileSync per governed type, memoised only within a call, and the gate runs its rules twice per write. Bounded today to the three types that dispatch it, so it is an observation, not a card.

Generated by Claude Code

… publish door

`DEFAULT_METADATA_TYPE_REGISTRY` declares `datasource` with
`allowRuntimeCreate: true`, and no rule named it in `runtimeTypes`, so a
runtime datasource write built no snapshot and dispatched no rule at all.

The type was outside the ten the ADR-0049 ruling graded, so its group is
measured here rather than inherited. Retirement is refuted (a real stack
collection exists to create into, and the ADR-0015 Addendum create path is
live). The `skill` hold-out is refuted too: its bridge resolved into two
collections the door does not carry, while this one judges each written item's
own keys against that type's liveness ledger and resolves into nothing.

So the honest group is the ledger-driven one: `lintLivenessProperties` gains
`datasource` beside `email_template` / `mapping`, with the matching
`TYPE_TO_STACK_KEY` row. The shipped datasource ledger carries 0 warn keys, so
the rule is dispatched and silent today — pinned as such, with a lit control,
and under the same no-ledger-population fence.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
…red, and add the changeset

The registry entry is the one place an author meets `allowRuntimeCreate: true`
for this type, and it said nothing about which door judges such a write. It now
names the rule, states that the rule is dispatched and silent by ledger, and
records that the multi-line entry shape is what hid the type from a line-wise
census.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 5 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/email-templates.mdx (via email_template (literal, a string literal in runtimeTypes))
  • content/docs/concepts/metadata-lifecycle.mdx (via DEFAULT_METADATA_TYPE_REGISTRY (symbol, a top-level const object), email_template (literal, a string literal in runtimeTypes))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), runtimeTypes (symbol, a field of const object AUTHORING_RULES))
  • content/docs/plugins/adding-a-metadata-type.mdx (via DEFAULT_METADATA_TYPE_REGISTRY (symbol, a top-level const object))
  • content/docs/protocol/objectui/concept.mdx (via DEFAULT_METADATA_TYPE_REGISTRY (symbol, a top-level const object))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 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 1f69917c5c76c065256a9bbaac3afc587279b9ba → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 94e91bc14065d883fad13c9799c0a265dfc77f4f — the merge of head a2596caebf45a9c3f8e6713fc04188c7c6dada75 into base 1f69917c5c76c065256a9bbaac3afc587279b9ba, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 94e91bc14065d883fad13c9799c0a265dfc77f4f && git checkout 94e91bc14065d883fad13c9799c0a265dfc77f4f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1f69917c5c76c065256a9bbaac3afc587279b9ba a2596caebf45a9c3f8e6713fc04188c7c6dada75 && git checkout -B drift-repro 1f69917c5c76c065256a9bbaac3afc587279b9ba && git merge --no-ff a2596caebf45a9c3f8e6713fc04188c7c6dada75

node scripts/docs-audit/affected-docs.mjs --json 1f69917c5c76c065256a9bbaac3afc587279b9ba

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1f69917c5c76c065256a9bbaac3afc587279b9ba → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a2596caebf45a9c3f8e6713fc04188c7c6dada75

① Derived judgments

Re-taken in my own worktree at the head, exit codes captured before any pipe. ⛔ Nothing below is adopted from the os-dev-report.

1 · The census — a different instrument, not a re-run of the dev's. I read DEFAULT_METADATA_TYPE_REGISTRY through the TypeScript compiler AST (createSourceFile → the declaration's ArrayLiteralExpression → each element's PropertyAssignments). An AST has no concept of a line, so it cannot inherit a line-wise reader's blind spot — the failure that produced this card — by construction. Leg 2 the same way: every runtimeTypes: PropertyAssignment under packages/lint/src (non-test), with comments structurally invisible rather than filtered out.

leg mine dev's
registry entries 27 27 ✓
allowRuntimeCreate: true 22 (false 5 · absent 0 · non-boolean 0) 22 ✓
entries whose object literal spans >1 line 1 — datasource, :948-966 1 ✓
runtimeTypes declarations, packages/lint/src non-test 28 —
allowRuntimeCreate: true named by none of them, at base 1f69917c5c 6 — datasource, external_catalog, translation, doc, tool, skill 6 ✓
lit control — declarations naming object 12 12 ✓
dark control — zzznotatype 0 0 ✓
reverse leg (unasked): declared in runtimeTypes but not allowRuntimeCreate 0 —

Exact on every leg. At the head the undeclared set is 5.

2 · 「Same value at both doors」 — driven through the REAL gate, not through the seam. The PR pins this with lintLivenessPropertiesFromLedgerDir over a synthetic directory. That is the rule, but it is not the door: it skips buildRuntimeWriteSnapshots, the baseline/candidate differential, fingerprint and nameKeyFindingPath. So I built the case that would break the claim if it were false. I lit the shipped packages/spec/liveness/datasource.json — one top-level row (origin) and one CHILD row (pool.min, so the dotted getNested fan-out is exercised, which the PR's fixture does not do) — then ran runRuntimeAuthoringRules against lintLivenessProperties over a whole stack carrying every collection the door omits, populated (tools, actions, skills, flows, emailTemplates, mappings, dashboards).

Door and whole stack returned the identical two findings, non-vacuously:

liveness-dead-property · datasource 'probe_wh' · sets `origin` but this datasource property has no runtime effect (liveness: dead).
liveness-dead-property · datasource 'probe_wh' · sets `pool.min` but this datasource property has no runtime effect (liveness: dead).

Ledger restored; git diff empty afterwards.

Why it holds structurally — which matters more than the fixture: walkStack's TYPE_COLLECTIONS loop calls checkItem(type, item, …), the item's own top-level and dotted keys against that type's warn map, and authoring-rules.ts maps the finding with path: f.where, not a collection index. So the fingerprint (rule, where, where, message) is a pure function of the written item's name and the ledger row: there is no universe term in it at all. That is a real structural difference from validateAiToolReferences, whose collectToolUniverse unions stack.tools ∪ stack.actions ∪ every object's actions and therefore loses two of three limbs at a door that carries only objects. The two cases are not the same shape, and the dev is right about which one datasource is.

3 · The stack-key proof — reproduced, then pushed past where the dev stopped. Baseline green: 45 cases across the two files.

ablation reds whose
runtimeTypes loses 'datasource' 4 (report says 3) dev's
datasource: 'datasources' → 'datasource' 2, incl. ⭐ the behavioural stack-key case dev's
TYPE_COLLECTIONS loses { type: 'datasource', key: 'datasources' } 2, incl. ⭐ the door-vs-stack case mine
datasource: 'datasources' → 'emailTemplates' — a real collection the rule DOES read 2, incl. ⭐ the stack-key case mine

Each: anchor asserted unique before writing, on-disk blob hash change proven, restore proven byte-identical to HEAD. Two notes. The report's 3 for the first is low — it is 4 across both files, so that ablation is stronger than claimed, not weaker. And the third is the one that earns the claim: it proves the new file's behavioural case depends on the rule reading stack.datasources, not only on the gate's table. The seed: 'data' class is closed from both ends here, which is genuinely more than group C's two existing rows have.

4 · The silence, and its control. datasource.json = 12 props, 0 authorWarn, 0 experimental — and, as a leg worth taking separately because loadWarnMap flattens them into the same map, 0 warned children under the three container props (pool 4 children, ssl 5, external 5). The three textual authorWarn hits are all authorWarn'd inside _note prose. In one process on one instrument: authorWarnedProperties('datasource') → [], authorWarnedProperties('object') → ['externalSharingModel'], and the real gate over a maximal datasource document returns rulesRun: ['lintLivenessProperties'], errors: [], advisories: []. resolveLivenessDir() resolves to packages/spec/liveness — source, not a stale dist. The zero is a reading, not a dead instrument.

And the claim the report only asserted — 「the day a property earns a row the door lights up with no second edit」 — is confirmed behaviourally by leg 2: two rows added to the shipped ledger, two advisories at the door, zero lines of code changed.

5 · Blast radius — run, not skipped. @objectstack/objectql and @objectstack/rest declare no @objectstack/lint dependency (verified in their manifests), so --affected would not select them; metadata-protocol does. All five green, exit 0 each: lint 108 files / 4084 · spec 509 / 14895 · metadata-protocol 188 files (3 skipped) / 2645 · objectql 303 / 5050 · rest 194 / 3254 (+1 skipped). I also confirmed the crossing needs no second-package edit: runtimeAuthoringGateVerdict calls runRuntimeAuthoringRules for any type and filters inside runtimeAuthoringRulesFor, and CLOSURE_CONTEXT_KEY_BY_TYPE routes only context collections, which datasources is not. The door is live at /meta as shipped.

6 · Gates. Green here: check:adr-links, check:adr-anchors, check:adr-symbol-anchors, check:cross-package-test-inputs, check:empty-changeset (1 declaring changeset added, none modified or deleted), check:changeset-gate-self-tests, check:issue-citations, check:doc-authoring, check:pm-widening-tells, and @objectstack/spec check:generated — all 15 generated artifacts up to date, tree clean afterwards, so the registry comment moved none.

② Semver level

@objectstack/lint: minor. Correct, and correctly the only package declared. Clause-②: yes (widening) is the right arm: purely additive — one string into an existing runtimeTypes array, one row into TYPE_TO_STACK_KEY.

The contract-surface question, answered mechanically rather than by reading the diff. I printed the DEFAULT_METADATA_TYPE_REGISTRY AST with comments stripped at base and at head: byte-identical, 7897 characters both. All 18 added lines in packages/spec/src/kernel/metadata-plugin.zod.ts are // comment lines. So the spec's accepted set is neither widened nor narrowed, and the changeset is right not to declare a @objectstack/spec bump. No BREAKING banner is owed and no ADR-0087 marker: the arm that would demand one — (narrowing) — is correctly not claimed, and check:adr-0087-registration's self-test passes.

Can this refuse a write that previously passed? No, and I tried to make it. The rule is tier: 'advisory', its findings are mapped severity: 'warning' as const, and runRuntimeAuthoringRules admits only severity === 'error' into errors. I also drove three malformed bodies (pool: 'not-an-object', name: 42, pool: null) through the real gate: no authoring-rule-threw, no errors. The changeset's 「nothing previously admitted is refused」 is true.

③ Boundary flags

On the arm choice — the central question — the chain holds, and I could not break it. Group D is refuted on its own criterion: ObjectStackDefinitionSchema.datasources exists at stack.zod.ts:275 as z.array(DatasourceSchema), and RUNTIME_CREATE_ALLOWED_TYPES is derived by iterating the registry and adding every allowRuntimeCreate type plus its plural, so PUT /meta/datasource/NAME really does mint one. The skill hold-out is refuted on the distinction that actually matters, not on a resemblance: skill was held out of #19517 because wiring it would have shipped a false advisory into a surface Studio renders; datasource ships no advisory, because the rule it reaches has no universe term to lose. A rule that reaches a wrong verdict at a partial door and a rule that reaches the same verdict at every door are different defects, and only the first is a reason to hold out.

So: 「dispatched and silent」 is an honest discharge here, not an unenforced declaration in a new coat — but the reason is narrower than 「it matches group C」, and it is worth stating in the terms that would distinguish the two. What makes it honest is that the circuit is live and load-bearing, and that is now measured rather than asserted: adding two rows to the shipped ledger lit two advisories at the real door with no code change. An unenforced declaration in a new coat would not have lit. The switch is off; the wiring is not decorative. datasource resembles email_template / mapping, and it is a stronger member of that group than either, because their stack keys rest on one string assertion while this one survives three independent ablations including one against the rule's own collection table.

Flags, none blocking:

  1. One sentence in the PR's own permanent record contradicts the PR's own finding. packages/lint/src/runtime-gate.datasource-writes.test.ts:9 opens: 「allowRuntimeCreate: true — Studio's wizard, REST /meta and an MCP/AI author may all mint one at runtime」, naming the wizard as a door this crossing serves. The PR body's 「What this does NOT reach」 says the opposite, and I confirmed it: POST /api/v1/datasources → DatasourceAdminService.createDatasource → putDatasourceRecord → metadata.register + persistDatasourceRow; saveMetaItem is nowhere on that path. The fixture comment at :128 — 「of the shape Setup's wizard persists」 — inherits the same slip. This is the file the PR designates as carrying the reading 「in the only way that cannot rot」, so it should name /meta (REST · MCP · AI author) and say plainly that the wizard route is NOT reached. Fix before landing; ⛔ not a reason to fail the contract.
  2. The changeset carries a softer version of the same: 「a datasource minted through Studio, REST /meta or an MCP/AI author」 — and that text ships to consumers as CHANGELOG.md. It is defensible, since registerMetadataTypeActions('datasource', …) puts the type on Studio's metadata-admin surface whose generic save is PUT /api/v1/meta/:type/:name → saveMetaItem; but against the PR's own finding it reads as more than it is. One qualifying clause would settle it.
  3. ADR-0015 Addendum as the provenance for the runtime-create path does not resolve. ADR-0015's only Addendum at this head is §18, 「Federation read path honours remoteName / remoteSchema」; there is no §3.5 and no datasource-lifecycle section. The pointer is propagated from admin-routes.ts:68, which already carries it, so this PR copied an in-tree citation rather than inventing one, and all three ADR gates pass because they resolve numbers, not sections. The underlying fact is independently true. Flagged for the ADR's owner, not for this PR.
  4. Cost, with the number the acceptance note left out. walkStack asks warnMapOf for 3 bespoke types + 18 TYPE_COLLECTIONS types = 21 ledger loads, memoised only within a call, and the gate runs two passes — so a datasource publish now performs 42 ledger reads to judge nothing, and this PR grows the population paying that by half (2 types → 3). The report calls it 「an observation, not a card」; with the number attached it is closer to a card, and it reads worse with every further group C crossing.
  5. The one place door and whole stack genuinely diverge, which the PR's comparison cannot reach because it compares the rule rather than the gate: datasources is not a RuntimeStackContext collection, so the baseline cannot carry the written datasource's stored self. Measured at the door on a lit ledger — re-publishing an unchanged stored datasource re-raises its advisory as newly introduced. This is the standing semantics for every non-context written type (report, position, app, email_template, mapping all share it) and it states nothing false — the property really is dead in the document being written — so it does not touch the arm choice. Recorded so the next reader need not rediscover it.
  6. The corpus control at :300 hand-copies three datasources and calls them 「the whole authored population of this type across the example apps」. True today — I verified exactly three (showcase_external, crm_primary, crm_analytics) — but it is a literal, not a read, so it will stop being the population silently.
  7. Both out-of-scope findings verified accurate. The two runtime-create doors really do enforce disjoint check sets: createDatasource runs assertValidName / assertValidConfig / assertDatasourcePoolSupported / the code-origin collision refusal before persisting, and the /meta door runs none of them while now running the authoring gate the wizard never sees. And group D's premise really is false for one member: ObjectStackDefinitionSchema.docs exists at stack.zod.ts:431, while no externalCatalogs collection does. Correctly reported rather than repaired.

What I tried that did NOT break it: the four ablations above; the door-vs-stack comparison through the real gate on a lit shipped ledger, with a dotted child path and every omitted collection populated; three malformed bodies against the throw-catcher; a caller-supplied datasources context (dropped by the snapshot builder — the door cannot be handed siblings, so it cannot inherit one's verdict); the reverse census leg (no type is declared in runtimeTypes without allowRuntimeCreate: true); and the no-other-widening claim (runtimeAuthoringRulesFor('datasource') is exactly ['lintLivenessProperties'], no reference-integrity member claims the type, and external_catalog / translation / doc / tool / skill all still return []). None of them moved it.

Implemented-by: claude/issue-19568-datasource-runtime-create-classification
Reviewed-by: session_01AmH9bKvGoLjiY86Q4Z3og2

VERDICT: PASS


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 21, 2026 15:52
@os-steve
os-steve enabled auto-merge September 21, 2026 15:52
@os-steve
os-steve added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit f77b806 Sep 21, 2026
44 checks passed
@os-steve
os-steve deleted the claude/issue-19568-datasource-runtime-create-classification branch September 21, 2026 16:13
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

2 participants