Skip to content

Commit fa5d137

Browse files
Elon Muskclaude
andauthored
feat(devx,datasource,automation): gate undeclared workspace imports in published src (#10062) (#12942)
* wip(devx): seed the undeclared-workspace-import gate (#10062) * feat(devx): gate undeclared workspace imports in published src (#10062) * chore(changeset): declare the dependency-graph and export changes (#10062) * feat(devx): declare the gate's watch hints so a dispatch brief can name it (#10062) * docs(devx): state the type-only row's evidence as measured (#10062) * fix(datasource): keep the guarded optional driver arms undeclared, ledger them instead (#10062) * fix(devx): record the service-datasource driver-memory declaration's new field in the census ledger (#10062) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3404bd1 commit fa5d137

11 files changed

Lines changed: 1062 additions & 47 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/metadata-core": minor
3+
"@objectstack/objectql": patch
4+
"@objectstack/service-automation": patch
5+
"@objectstack/service-datasource": patch
6+
---
7+
8+
feat(devx,datasource,automation): published `src/**` may only import workspace packages it declares (#10062)
9+
10+
A package's non-test `src/**` was free to import any workspace package,
11+
declared or not, and nothing checked it. The class was filed with one member
12+
and a mitigation — the import was type-only, so nothing reached the emitted
13+
JavaScript and rollup-plugin-dts inlined the declaration rather than naming an
14+
unresolvable module. It grew to four members with no signal, and one of them
15+
killed the mitigation: `service-automation/src/flow-precedence.ts` **value**
16+
imports from `@objectstack/objectql`, which it does not declare, and because
17+
the shared tsup config externalises only `dependencies`/`peerDependencies`, the
18+
bundler answered by inlining objectql's implementation into
19+
`service-automation/dist/index.js` — a second copy of another package's code,
20+
kept correct by build configuration alone.
21+
22+
`pnpm check:undeclared-dep-imports` is the gate, and the per-member fixes here
23+
are decided one at a time rather than by a uniform policy — declaring makes a
24+
coupling real and installable, routing it away removes it, and the two are not
25+
interchangeable:
26+
27+
* **`@objectstack/service-datasource`** now declares `@objectstack/driver-sql`
28+
and `@objectstack/driver-memory` as **dependencies**. Both are loaded through
29+
an *unguarded* `await import(...)` on the postgres, mysql, sqlite and memory
30+
arms, so a consumer reaching one of those paths needed a package it was never
31+
told to install, and would have met `ERR_MODULE_NOT_FOUND` rather than a
32+
diagnosis. The three *guarded* driver arms — `@objectstack/driver-sqlite-wasm`,
33+
`@objectstack/driver-mongodb`, `@objectstack/driver-turso` — are deliberately
34+
left undeclared: each load sits in a `try`/`catch` that answers an absent
35+
package with the fault, the consequence and the install command, and each
36+
rides as an optional install. Declaring them would install them (turso drags
37+
`@libsql/client`'s native bindings) and, measured on this branch, takes a live
38+
assertion out of the tree: `default-datasource-driver-factory.test.ts` reaches
39+
the missing-package arm with no stub precisely because the package does not
40+
resolve from here.
41+
* **`@objectstack/metadata-core`** now owns the ADR-0029 D9.6 provenance pair,
42+
`isCodeArtifactBody` and `isTenantAuthored`, sunk out of
43+
`@objectstack/objectql`'s registry by the same criterion as the write-verb
44+
dispatch predicates and the audit governance table beside them: a second layer
45+
needs the answer and the reverse import would either close a cycle or make the
46+
consumer depend on the whole data engine for one predicate. `objectql`
47+
re-exports `isCodeArtifactBody` from its original path, so its public API is
48+
unchanged; `service-automation` imports it from `metadata-core`, which it
49+
already declared, and its bundle no longer carries a copy of objectql's code.
50+
51+
Two members stay recorded rather than remediated, because the tree already
52+
carries the decision not to declare them together with its reason
53+
(`@objectstack/runtime``@objectstack/driver-turso`, whose bare `import()` is
54+
a host-replaceable default thunk under #6268; `@objectstack/rest`
55+
`@objectstack/objectql`, whose absence must degrade to `501 NOT_IMPLEMENTED`
56+
rather than fail module load). Their ledger rows carry mechanical evidence and
57+
go red the moment that evidence stops holding — in particular, a `type-only`
58+
row reds on the day its import becomes a value import, which is exactly the
59+
transition nothing caught the first time.

.github/workflows/lint.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,27 @@ jobs:
390390
node scripts/check-comment-mask-adoption.mjs --self-test
391391
node scripts/check-comment-mask-adoption.mjs
392392
393+
# A package's published `src/**` may only import workspace packages its own
394+
# manifest DECLARES (#10062). The class was filed with one member and a
395+
# mitigation — "type-only, so nothing lands in the emitted JavaScript" —
396+
# and grew to four with no signal, one of them a VALUE import whose
397+
# implementation the bundler then INLINED into a second package's dist.
398+
# Both halves of the mitigation are now ledger EVIDENCE instead of prose:
399+
# a `type-only` row reds the moment its import stops being type-only.
400+
# Node builtins plus the shared comment mask only — no node_modules, so a
401+
# reviewer can run it in place. Its `--self-test` runs FIRST: the
402+
# production run over a fixed tree is green by construction, so it cannot
403+
# tell a working matcher from a dead one, and the specifier FLOOR is the
404+
# other half (a dead matcher finds nothing, and nothing is what success
405+
# looks like — so it refuses with exit 2 rather than passing).
406+
# Invoked as `node` rather than through a `pnpm check:*` alias: see the
407+
# GATE INVOCATION IDIOM note at the top of this file.
408+
# Scans ~2k non-test src files across 78 workspace packages, no spawns; ~1s.
409+
- name: Published src imports only declared workspace deps
410+
run: |
411+
node scripts/check-undeclared-dep-imports.mjs --self-test
412+
node scripts/check-undeclared-dep-imports.mjs
413+
393414
# The bash-3.2 floor, over every shell file the repo ships (#12221).
394415
# `/usr/bin/env bash` is bash 3.2.57 on macOS -- Apple ships no bash 4+,
395416
# for licensing reasons -- and THIS RUNNER IS BASH 5, where every construct

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"check:test-source-alias": "node scripts/check-test-source-alias.mjs --self-test && node scripts/check-test-source-alias.mjs",
125125
"check:refd-timer-probe": "node scripts/check-refd-timer-probe.mjs --self-test && node scripts/check-refd-timer-probe.mjs",
126126
"check:type-source-resolution": "node scripts/check-type-source-resolution.mjs --self-test && node scripts/check-type-source-resolution.mjs",
127+
"check:undeclared-dep-imports": "node scripts/check-undeclared-dep-imports.mjs --self-test && node scripts/check-undeclared-dep-imports.mjs",
127128
"check:published-files": "node scripts/check-published-files.mjs --self-test && node scripts/check-published-files.mjs",
128129
"check:published-readme-exports": "node scripts/check-published-readme-exports.mjs --self-test && node scripts/check-published-readme-exports.mjs",
129130
"check:published-readme-links": "node scripts/check-published-readme-links.mjs --self-test && node scripts/check-published-readme-links.mjs",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* "Does a code package ship this name?" — the ADR-0029 D9.6 provenance test.
5+
*
6+
* [#10062] Sunk here from `@objectstack/objectql`'s registry by the same
7+
* criterion as the write-verb dispatch predicates and the audit governance
8+
* table above it in `index.ts`: a second layer needs the answer, and the
9+
* reverse import would either close a cycle or make the consumer depend on the
10+
* whole data engine for one predicate.
11+
*
12+
* The second layer is `@objectstack/service-automation`'s ADR-0048 flow
13+
* precedence, which asks exactly this question to decide which contender for a
14+
* flow name wins. It reached it by importing `@objectstack/objectql` directly —
15+
* a package it does not declare — and because the shared tsup config
16+
* externalises only `dependencies`/`peerDependencies`, the bundler INLINED
17+
* objectql's implementation into `service-automation/dist/index.js`: a second
18+
* copy of another package's code, kept correct by build configuration alone.
19+
* `@objectstack/metadata-core` is the package both sides already declare and it
20+
* depends on neither, so the answer now has one home and one implementation.
21+
*
22+
* `objectql` re-exports `isCodeArtifactBody` from `registry.ts`, so its public
23+
* API is unchanged.
24+
*/
25+
26+
/**
27+
* Is this registered item a TENANT-authored overlay rather than a code-shipped
28+
* artifact? (ADR-0010 `_provenance`: `'package'` for loader-introduced items,
29+
* `'org'` for tenant-authored.)
30+
*
31+
* `_packageId !== 'sys_metadata'` alone cannot answer it. That sentinel marks
32+
* one thing only — an overlay row bound to no package. A row that IS bound to
33+
* one is keyed by its real package id on BOTH sides that register it: the save
34+
* path (#4636 PR1) and the boot-time rehydration of `sys_metadata` (#4636 PR2).
35+
* Either way the key is `app.<slug>`, which is exactly what every code-shipped
36+
* item carries too, so the sentinel test cannot tell them apart. A tenant's own
37+
* overlay came back from a kernel rebuild looking like a code
38+
* artifact, and the protocol's overlay gate refused the next write to it with
39+
* `not_overridable` — an app the user had just built through Studio/AI became
40+
* permanently un-editable at the first kernel rebuild (cloud#970). Provenance is
41+
* the axis that actually distinguishes the two, so ask it.
42+
*/
43+
export function isTenantAuthored(item: unknown): boolean {
44+
return (item as { _provenance?: unknown } | null | undefined)?._provenance === 'org';
45+
}
46+
47+
/**
48+
* [ADR-0029 D9.6] Is this registered body a CODE-shipped artifact?
49+
*
50+
* The exact test `SchemaRegistry.getArtifactItem` has always applied, factored
51+
* out so the object branch and the D9.8 hydration discriminator
52+
* (`SchemaRegistry.getPackagedObjectOwner`) cannot drift into two different
53+
* answers to one question — "does a code package ship this name?". Truthy
54+
* `_packageId`, not the `'sys_metadata'` rehydration sentinel, and not tenant
55+
* provenance.
56+
*/
57+
export function isCodeArtifactBody(item: unknown): boolean {
58+
const it = item as { _packageId?: unknown } | null | undefined;
59+
if (!it || !it._packageId || it._packageId === 'sys_metadata') return false;
60+
return !isTenantAuthored(it);
61+
}

packages/metadata-core/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ export * from './engine-findone-predicate.js';
4949
// reporting two.
5050
export * from './audit-field-governance.js';
5151

52+
// [#10062] The ADR-0029 D9.6 provenance pair, sunk here from
53+
// `@objectstack/objectql`'s registry by the same criterion as everything above:
54+
// `@objectstack/service-automation` needs the same "does a code package ship
55+
// this name?" answer for ADR-0048 flow precedence, and was reaching it by
56+
// importing objectql — a package it does not declare, so the bundler inlined a
57+
// copy of objectql's implementation into service-automation's dist. Both sides
58+
// already declare THIS package, and it depends on neither. `objectql`
59+
// re-exports `isCodeArtifactBody` from `registry.ts`, so its public API is
60+
// unchanged.
61+
export * from './code-artifact-provenance.js';
62+
5263
// [#6562] The served-document injection/strip pair over the injected-system-
5364
// column definition tables, sunk here by the same criterion and for the same
5465
// cycle as the governance table above. The DEFINITION tables themselves and the

packages/objectql/src/registry.ts

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ import {
1616
// from one place. Re-exported below under its original name.
1717
ITEM_KEY_DISCRIMINATORS,
1818
readDiscriminatorValue as discriminatorValue,
19+
// [#10062] The ADR-0029 D9.6 provenance pair, sunk into metadata-core for the
20+
// same reason as the table above: `@objectstack/service-automation`'s flow
21+
// precedence asks the same question ("does a code package ship this name?")
22+
// and reached it by importing this package, which it does not declare — so
23+
// the bundler inlined a copy of this implementation into ITS dist. Both names
24+
// keep their original spelling here, and `isCodeArtifactBody` is re-exported
25+
// below, so this package's public API is unchanged.
26+
isCodeArtifactBody,
27+
isTenantAuthored,
1928
} from '@objectstack/metadata-core';
2029
// [#8460] `scalarOverridesPackagedBase` is the #8284 comparison, imported rather
2130
// than re-spelled: the object FOLD asks the same question one layer down (has
@@ -1144,42 +1153,13 @@ export class NamespaceConflictError extends Error {
11441153
}
11451154
}
11461155

1147-
/**
1148-
* Is this registered item a TENANT-authored overlay rather than a code-shipped
1149-
* artifact? (ADR-0010 `_provenance`: `'package'` for loader-introduced items,
1150-
* `'org'` for tenant-authored.)
1151-
*
1152-
* `_packageId !== 'sys_metadata'` alone cannot answer it. That sentinel marks
1153-
* one thing only — an overlay row bound to no package. A row that IS bound to
1154-
* one is keyed by its real package id on BOTH sides that register it: the save
1155-
* path (#4636 PR1) and the boot-time rehydration of `sys_metadata` (#4636 PR2).
1156-
* Either way the key is `app.<slug>`, which is exactly what every code-shipped
1157-
* item carries too, so the sentinel test cannot tell them apart. A tenant's own
1158-
* overlay came back from a kernel rebuild looking like a code
1159-
* artifact, and the protocol's overlay gate refused the next write to it with
1160-
* `not_overridable` — an app the user had just built through Studio/AI became
1161-
* permanently un-editable at the first kernel rebuild (cloud#970). Provenance is
1162-
* the axis that actually distinguishes the two, so ask it.
1163-
*/
1164-
function isTenantAuthored(item: unknown): boolean {
1165-
return (item as { _provenance?: unknown } | null | undefined)?._provenance === 'org';
1166-
}
1167-
1168-
/**
1169-
* [ADR-0029 D9.6] Is this registered body a CODE-shipped artifact?
1170-
*
1171-
* The exact test {@link SchemaRegistry.getArtifactItem} has always applied,
1172-
* factored out so the object branch and the D9.8 hydration discriminator
1173-
* ({@link SchemaRegistry.getPackagedObjectOwner}) cannot drift into two
1174-
* different answers to one question — "does a code package ship this name?".
1175-
* Truthy `_packageId`, not the `'sys_metadata'` rehydration sentinel, and not
1176-
* tenant provenance.
1177-
*/
1178-
export function isCodeArtifactBody(item: unknown): boolean {
1179-
const it = item as { _packageId?: unknown } | null | undefined;
1180-
if (!it || !it._packageId || it._packageId === 'sys_metadata') return false;
1181-
return !isTenantAuthored(it);
1182-
}
1156+
// [#10062] `isTenantAuthored` and `isCodeArtifactBody` used to be defined here.
1157+
// They now live in `@objectstack/metadata-core`
1158+
// (`code-artifact-provenance.ts`), imported at the top of this file and
1159+
// re-exported immediately below, so every caller's spelling — including
1160+
// `@objectstack/objectql`'s own public export — is unchanged. Their full
1161+
// rationale moved with them.
1162+
export { isCodeArtifactBody };
11831163

11841164
// ============================================================================
11851165
// i18n bundles — metadata types whose identity is (name, <discriminator>)

packages/services/service-automation/src/flow-precedence.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,14 @@
4545
// larger change, and ADR-0048 does not ask for it — the ADR's answer for this
4646
// case is a precedence plus a warning, both of which are here.
4747

48-
import { isCodeArtifactBody } from '@objectstack/objectql';
48+
// [#10062] From `@objectstack/metadata-core`, which this package DECLARES —
49+
// not from `@objectstack/objectql`, which it does not. The predicate is the
50+
// same one (it was sunk into metadata-core and objectql re-exports it), but the
51+
// import used to be an undeclared workspace dependency, and because the shared
52+
// tsup config externalises only `dependencies`/`peerDependencies` the bundler
53+
// answered it by inlining objectql's implementation into this package's dist.
54+
// `pnpm check:undeclared-dep-imports` is the gate over that class.
55+
import { isCodeArtifactBody } from '@objectstack/metadata-core';
4956
import type { FlowContender, FlowShadowingRecord } from './engine.js';
5057

5158
/** One flow name's resolved winner, plus the receipt when it displaced others. */

packages/services/service-datasource/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@
2727
},
2828
"dependencies": {
2929
"@objectstack/core": "workspace:*",
30+
"@objectstack/driver-memory": "workspace:*",
31+
"@objectstack/driver-sql": "workspace:*",
3032
"@objectstack/spec": "workspace:*",
3133
"@objectstack/types": "workspace:*",
3234
"pg-connection-string": "^2.14.0"
3335
},
3436
"devDependencies": {
35-
"@objectstack/driver-memory": "workspace:*",
3637
"@objectstack/driver-mongodb": "workspace:*",
37-
"@objectstack/driver-sql": "workspace:*",
3838
"@objectstack/driver-sqlite-wasm": "workspace:*",
3939
"@objectstack/metadata-core": "workspace:*",
4040
"@objectstack/plugin-hono-server": "workspace:*",

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)