Skip to content

Commit cdb949a

Browse files
fix(adr-0087): predicate 2 reads a default export through its import, and a package name inside a path or URL (#27207)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBDFDmSVzwiQ3bKV9WbePMBf)_ Fixes #26324 Fixes #27022 Clause-②: no Metadata-protocol: none Skills-docs: none — no file under `skills/` names `runtime-export-retired`, `no-metadata-surface` or the ADR-0087 gate (`git grep` over `skills/`: 0 hits), so neither predicate is taught there ## 维护者速读 - 改了什么:ADR-0087 登记门禁的谓词 2 修两处。导出名为 `default` 的退役只看"有没有元数据面 import 了这个默认导出,或以命名空间导入、命名空间再导出、动态 `import()` / `require()` 加载了整个模块",不再按单词扫 `default`;整包退役(`PACKAGE#*`)把路径或 URL 字符串里出现的包名也算作引用,但 `@scope/plugin-x` 仍不算无作用域的 `plugin-x`。 - 为什么改:`default` 是元数据里的普通键(`{ value: 'technology', default: true }`、Zod `.default()`),单词扫描在每棵树上都命中,所以任何 `default` 导出的退役都无法如实申报,卡住了 #26206 的后半(移除 driver-memory 的旧默认导出)。路径/URL 那一处是 D9 原文"包名的任何其他出现,含字符串值"与门禁实际放行之间唯一已知的缺口。 - 风险与回滚:只动 `scripts/` 下一个不发布的门禁脚本。`default` 一侧放宽的范围只限导出名恰为 `default` 的操作数,其他导出名读法不变;路径一侧是收紧(多拒)。回滚即还原本 PR。 - 先看:`scripts/check-adr-0087-registration.mjs` 的 `defaultImportReference`(四条线性 import 正则,及 `metadataSurfaceReference` 开头的分流)、`packageReference` 的 `mention` 正则(新增一个排除 `@scope/` 段的负向后顾)、自测里的 DX 电池与 WP-U6–U9。 - 两分钟验证:`node scripts/check-adr-0087-registration.mjs --self-test`,预期打印 `705 assertions`、退出码 0。 - 需要你做的:无,席位落地 ## What changes - **#26324, predicate 2 for an export named `default`.** `metadataSurfaceReference` hands a `default` symbol to a new `defaultImportReference`, which reads the import arm only. A metadata surface reaches a default export through an import that binds it: a default binding (`import x from 'M'`, with named bindings or a namespace beside it, `type` or not) or `default` inside the braces of an `import` / `export … from` clause (`{ default as x }`, `export { default } from 'M'`). The specifier is attributed through the existing `specifierNamesModule`, and comment spans are blanked through the shared `maskComments`, as before. The bare-word arm is not read for `default`, because `default` is an ordinary key in exactly the files predicate 2 reads. Every other export name keeps today's reading, both arms. The shared primitive serves `runtime-interface-only` too, so a `PATH#default` there gets the same reading. - The import shapes are linear regexes anchored at a statement start. They deliberately do **not** reuse `importSpecifiersFor`'s `[\s\S]*?` clause scan. That scan runs today only on files that already contain the symbol's word, and over every metadata surface it is super-linear in the whitespace runs `maskComments` leaves: the first version of this fix reused it and the real-tree run had not finished after 12 minutes of CPU (one file alone, `packages/spec/src/api/discovery.zod.ts`, took 41 s). The linear version runs the whole real-tree gate in 7 s. - **Round 2 (contract review 6466959531, two should-fixes).** (1) A `default` operand is also refused by a metadata surface that binds the whole declaring module: a namespace import (`import * as ns from 'M'`, `ns.default` one read away), a namespace re-export (`export * as ns from 'M'`), or a dynamic `import('M')` / `require('M')`. The refusal rests on the binding alone, never on a `.default` read after it, so no member tracking is needed; every other export name already refuses these shapes through its bare-word arm, so this closes the admit gap round 1 opened. Both new readers are linear (statement-anchored, no `[\s\S]*?`). (2) `SELF_TEST_BATTERY_FLOOR` moves from 58 to 59 for the new DX battery. - **#27022, the `PACKAGE#*` mention regex.** Its negative lookbehind drops `/` from the excluded set, and a second negative lookbehind refuses an `@SCOPE/` segment just before the name (`@[\w.~-]+/`). A `/` may now precede the name, so a path or URL inside a string counts as a `mention`, but not a `/` that closes an `@scope` segment, so an unscoped `plugin-x` still does not match inside `@scope/plugin-x`. The name-boundary rules on both sides are unchanged (`plugin-xy` and `my-plugin-x` are other packages). - Header comments of `metadataSurfaceReference` and `packageReference` say the same. ## Tests - New battery `DX (#26324)`, floor 19, and the roster floor `SELF_TEST_BATTERY_FLOOR` 58 to 59: DX-G1 (the #26206 shape: a retired `default` export beside a `default: true` field option and a Zod `.default('')`) green, with DX-G1b naming the operand and its declaring entry; DX-G2 (the import quoted only in a comment) green; DX-G3 (another module's default imported) green; DX-R1 (a metadata surface that imports the package's default) red; DX-R2 (`{ default as x }`) red; DX-R3 (`import * as ns`, then `ns.default`) red; DX-R4 (a dynamic `import()`) red; DX-R5 (a `require()`) red; DX-R6 (`export * as ns from`) red; DX-U1 to U3 units (default beside named bindings, a type-only subpath default, a multi-line `export { default } from` after a `;`; named bindings, a `default:` key and a `.default()` call alone are not imports; a non-`default` export name keeps the bare-word arm). - WP battery floor 55 to 59: WP-U6 (the package name inside a path string is a `mention`, with its line), WP-U7 (inside a URL), WP-U8 (`@scope/plugin-x` is not `plugin-x`, bare, in a path and in a URL), WP-U9 (an unscoped `plugin-x` in a path and a URL is still a mention). WP-G3 (comment only) and WP-U4 (`plugin-xy` is not `plugin-x`) stay green unchanged. - Self-test: 682 to 705 assertions. <details><summary>Self-test under the verification lock, at `f402e245`</summary> ``` $ OS_VERIFY_LOCK_SLOT=issue-26324-selftest bash scripts/pm/os-verify-lock.sh -c 'node scripts/check-adr-0087-registration.mjs --self-test' ✓ check-adr-0087-registration --self-test: 705 assertions over real temp git repos (real scan()/assertInputs() path) os-verify-lock: VERDICT command-exit 0 · held the lock 62s (1m02s) · waited 0s ``` </details> ## Acceptance notes - **The driver-memory measurement (the real shape #26206 blocked on).** A local probe commit on `origin/main` `72629f38` deletes the `export default { … }` block from `packages/drivers/driver-memory/src/index.ts` and adds a changeset carrying `runtime-export-retired @objectstack/driver-memory#default` with the line `` - FROM `default` TO `InMemoryDriver`: … ``. The probe was never pushed. | Gate | Base | Command | Exit | Verdict | |---|---|---|---|---| | `origin/main` copy | `72629f38` | `node scripts/check-adr-0087-registration.mjs --base origin/main` | **1** | predicate 2 false: `examples/app-crm/src/objects/account.object.ts` mentions `default` (`{ label: 'Technology', value: 'technology', default: true }`), the card's reading exactly | | this PR (`2e1e09d5`) | `72629f38` | same | **0** | `verified: @objectstack/driver-memory#default (export, packages/drivers/driver-memory/src/index.ts)` | | this PR (`2e1e09d5`) | `72629f38` plus one object file `import memoryPlugin from '@objectstack/driver-memory'` | `--base PROBE_BASE` | **1** | predicate 2 false: `probe-host.object.ts` (an object definition) IMPORTS `default` from the module that declares it | | this PR, round 2 (`f402e245`) | `9dacccf9` (the same probe on the then-current `origin/main`) | `node scripts/check-adr-0087-registration.mjs --base origin/main` | **0** | `verified: @objectstack/driver-memory#default (export, packages/drivers/driver-memory/src/index.ts)`: the namespace and dynamic readers find nothing on main's metadata surfaces | - **Read for `default` on the binding, not on `.default`:** a namespace import, a namespace re-export and a dynamic `import('M')` / `require('M')` of the declaring module refuse on the module alone (round 2). The member spelling `.default` is never read, since it cannot be told from object keys and `.default()` calls; refusing on the binding is the conservative direction. - `importSpecifiersFor`'s super-linear clause scan is left as it is: its callers gate it on the word being present, so no current run is slow. Noted, not filed. - The order expected `Clause-②: n/a`; both claims carry `Clause-②: no`, and the body copies the claim. No published accept-set moves (`scripts/**` is not shipped), so `no` is accurate either way. - No changeset: the root package is private and `scripts/**` is not published. Labelled `skip-changeset`. - The ADR text needs no change: D9's second 2026-10-27 note already says "any other occurrence of the package name, a string value included", and the `default` reading is the D7 import primitive the note points to. ## Verification <details><summary>Derived gates (dispatch-gates --commands, 33 families) at `f402e245` (merge base `b322b5da`)</summary> ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:scripts-symbol-anchors :: exit 0 pnpm check:watch-hint-literal :: exit 0 $ node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt ✓ dispatch-gates --ran: 33 derived famil(ies) accounted for — 33 run, 0 NOT-MEASURED (a DERIVED zero — all 33 recorded an exit code and none of them is 3). ``` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufziGaNnGcwiYSC9Qzck)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c5ee99d commit cdb949a

1 file changed

Lines changed: 152 additions & 5 deletions

File tree

‎scripts/check-adr-0087-registration.mjs‎

Lines changed: 152 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
463463
'REX-L: the compiler is loaded only when a marker names the category': 6,
464464
'WS (#26220): ui workspace packages, `.tsx` modules, aliases and shims': 11,
465465
'IX (#26220): predicate 4 reads an `any` index signature; the entry may be `src/index.tsx`': 52,
466-
'WP (#26220): a deleted published package is retired whole, `<package>#*`': 55,
466+
'WP (#26220): a deleted published package is retired whole, `<package>#*`': 59,
467+
'DX (#26324): an export named `default` is reached through an import, never through the word': 19,
467468
'G6: a changeset that was ALREADY breaking at base is inherited': 1,
468469
'R15: a changeset RENAMED AND turned breaking in the same commit': 5,
469470
'G10: a PURE rename of an ALREADY-breaking stock changeset': 3,
@@ -496,7 +497,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
496497

497498
// DELETING an entry silences that battery's floor exactly as effectively as
498499
// zeroing it, so the roster's own size is pinned too.
499-
const SELF_TEST_BATTERY_FLOOR = 58;
500+
const SELF_TEST_BATTERY_FLOOR = 59;
500501

501502
// The key an assertion is filed under when no battery is open. It is not a
502503
// declared battery, so it reds by the same set difference rather than silently
@@ -3161,9 +3162,13 @@ export function verifyRuntimeInterfaceOnly(refs, { rev, cwd, packages }) {
31613162
* @param {Map<string, string>} surfaceTexts path -> text of every metadata surface
31623163
* @param {() => Map<string, {private: boolean, file: string}>} packages workspace
31633164
* manifests, read only when an import has to be attributed to a package
3165+
* An export named `default` is read through the import arm only: see
3166+
* `defaultImportReference`.
3167+
*
31643168
* @returns {{ kind: 'import', hitPath: string } | { kind: 'mention', hitPath: string, line: string } | null}
31653169
*/
31663170
export function metadataSurfaceReference(symbol, declPath, surfaceTexts, packages) {
3171+
if (symbol === 'default') return defaultImportReference(declPath, surfaceTexts, packages);
31673172
const word = new RegExp(`\\b${symbol}\\b`);
31683173
for (const [hitPath, rawText] of surfaceTexts) {
31693174
if (hitPath === declPath || !word.test(rawText)) continue;
@@ -3199,6 +3204,59 @@ export function metadataSurfaceReference(symbol, declPath, surfaceTexts, package
31993204
return null;
32003205
}
32013206

3207+
/**
3208+
* The two import shapes that bind a module's DEFAULT export, each anchored at a
3209+
* statement start and each linear in the text. ⛔ Not `importSpecifiersFor`'s
3210+
* `[\s\S]*?` clause scan: that one runs only on files that already name the symbol,
3211+
* and over every metadata surface it is super-linear in the whitespace runs
3212+
* `maskComments` leaves (measured: 41 s on one `*.zod.ts`).
3213+
*
3214+
* import x from 'm' a default binding, named bindings or a namespace
3215+
* import x, { y } from 'm' beside it or not, `type` or not
3216+
* import { default as x } from 'm' `default` named inside the braces, an
3217+
* export { default } from 'm' `export ... from` re-export included
3218+
*
3219+
* and the three that bind the whole MODULE, read as reaching its default too:
3220+
*
3221+
* import * as ns from 'm' a namespace import or re-export, `ns.default`
3222+
* export * as ns from 'm' being one member read away
3223+
* import('m') require('m') a dynamic load, whose `.default` likewise
3224+
*/
3225+
const DEFAULT_BINDING_IMPORT_RE =
3226+
/(?:^|;)[ \t]*import\s+(?:type\s+)?[A-Za-z_$][\w$]*\s*(?:,\s*(?:\{[^}]*\}|\*\s*as\s+[A-Za-z_$][\w$]*)\s*)?from\s*(['"])([^'"\n]+)\1/gm;
3227+
const BRACED_IMPORT_RE = /(?:^|;)[ \t]*(?:import|export)\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"\n]+)\2/gm;
3228+
const NAMESPACE_IMPORT_RE = /(?:^|;)[ \t]*(?:import|export)\s+(?:type\s+)?\*\s*as\s+[A-Za-z_$][\w$]*\s*from\s*(['"])([^'"\n]+)\1/gm;
3229+
const DYNAMIC_IMPORT_RE = /\b(?:import|require)\s*\(\s*(['"])([^'"\n]+)\1\s*\)/g;
3230+
3231+
/**
3232+
* `metadataSurfaceReference` for an export named `default` (#26324): the IMPORT arm
3233+
* only. `default` is an ordinary object key and member name in exactly the files
3234+
* this step reads -- a field option `{ value: 'technology', default: true }`, a Zod
3235+
* `.default(...)` -- so the bare-word arm hit on every tree, and no retirement of a
3236+
* default export could ever certify. A metadata surface reaches a default export
3237+
* through an import that binds it (the two shapes above), or through one that binds
3238+
* the whole module (the three after them). Those three are refused on the module
3239+
* alone, never on a `.default` read after them: that member spelling is the one
3240+
* that cannot be told from the object keys above, and refusing on the binding is
3241+
* the conservative direction -- every other export name refuses them too, through
3242+
* its bare-word arm. The specifier is attributed as for any other name
3243+
* (`specifierNamesModule`), and comment spans are blanked first, so a docblock
3244+
* quoting the import is not one.
3245+
*/
3246+
function defaultImportReference(declPath, surfaceTexts, packages) {
3247+
for (const [hitPath, rawText] of surfaceTexts) {
3248+
if (hitPath === declPath || !(rawText.includes('import') || rawText.includes('export') || rawText.includes('require'))) continue;
3249+
const code = maskComments(rawText);
3250+
const specifiers = [...code.matchAll(DEFAULT_BINDING_IMPORT_RE)].map((m) => m[2]);
3251+
for (const m of code.matchAll(BRACED_IMPORT_RE)) if (/\bdefault\b/.test(m[1])) specifiers.push(m[3]);
3252+
for (const re of [NAMESPACE_IMPORT_RE, DYNAMIC_IMPORT_RE]) for (const m of code.matchAll(re)) specifiers.push(m[2]);
3253+
if (specifiers.length === 0) continue;
3254+
const pkgs = packages();
3255+
if (specifiers.some((s) => specifierNamesModule(s, hitPath, declPath, pkgs))) return { kind: 'import', hitPath };
3256+
}
3257+
return null;
3258+
}
3259+
32023260
// ---------------------------------------------------------------------------
32033261
// The sixth category: `type-surface-only` (#13080)
32043262
//
@@ -4802,8 +4860,12 @@ function verifyWholePackageRetired(pkg, { base, head, cwd, packagesAt, kit }) {
48024860
* any occurrence counts -- a plugin id or package reference held as a VALUE as much
48034861
* as an import. An import specifier (`from '…'`, a bare `import '…'`, `import('…')`,
48044862
* `require('…')`, by the root or a subpath) is the case reported as `import`; every
4805-
* other occurrence is a `mention`. A name that merely starts with `pkg`
4806-
* (`plugin-xy` for `plugin-x`) is not it.
4863+
* other occurrence is a `mention`, one inside a path or URL included
4864+
* (`…/node_modules/@scope/pkg/dist/index.js`, `https://npm.im/@scope/pkg`, #27022).
4865+
* A name that merely starts or ends with `pkg` (`plugin-xy` for `plugin-x`) is not
4866+
* it, and neither is a scoped package whose last segment is an unscoped `pkg`
4867+
* (`@scope/plugin-x` for `plugin-x`): a `/` may precede the name, but not one that
4868+
* closes an `@scope` segment.
48074869
*
48084870
* @param {string} pkg
48094871
* @param {string} pkgDir the package's directory, no trailing slash
@@ -4813,7 +4875,7 @@ function verifyWholePackageRetired(pkg, { base, head, cwd, packagesAt, kit }) {
48134875
export function packageReference(pkg, pkgDir, surfaceTexts) {
48144876
const name = pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
48154877
const spec = new RegExp(`(?:\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\brequire\\s*\\(\\s*|^\\s*import\\s+)(['"])${name}(?:/[^'"\\n]*)?\\1`, 'm');
4816-
const mention = new RegExp(`(?<![\\w@./-])${name}(?![\\w.-])`);
4878+
const mention = new RegExp(`(?<![\\w@.-])(?<!@[\\w.~-]+/)${name}(?![\\w.-])`);
48174879
// `maskComments` blanks in place, so an offset in `code` is the same offset in the file.
48184880
const lineAt = (raw, code, at) => {
48194881
const end = code.indexOf('\n', at);
@@ -8262,6 +8324,91 @@ function selfTest() {
82628324
]));
82638325
assert(u5?.kind === 'mention' && u5.hitPath === 'packages/spec/src/d.zod.ts' && u5.line === `export const D = { plugin: '${WP_PKG}' };`,
82648326
`WP-U5: a string VALUE naming the package is a reference in code, reported with its line; a comment is not (${JSON.stringify(u5)})`);
8327+
// #27022: an occurrence inside a path or URL is "any other occurrence" (D9); a
8328+
// `/` may precede the name, but not one closing an `@scope` segment.
8329+
const mentionIn = (pkg, line) => packageReference(pkg, 'ui/packages/plugin-x', new Map([['packages/spec/src/e.zod.ts', `${line}\n`]]));
8330+
const u6Line = `export const E = { entry: '/repo/node_modules/${WP_PKG}/dist/index.js' };`;
8331+
const u6 = mentionIn(WP_PKG, u6Line);
8332+
assert(u6?.kind === 'mention' && u6.line === u6Line,
8333+
`WP-U6: the package name inside a PATH string is a mention, reported with its line (${JSON.stringify(u6)})`);
8334+
const u7Line = `export const E = { docs: 'https://npm.im/${WP_PKG}' };`;
8335+
const u7 = mentionIn(WP_PKG, u7Line);
8336+
assert(u7?.kind === 'mention' && u7.line === u7Line,
8337+
`WP-U7: the package name inside a URL string is a mention, reported with its line (${JSON.stringify(u7)})`);
8338+
assert(['@scope/plugin-x', '/repo/node_modules/@scope/plugin-x/dist/index.js', 'https://npm.im/@scope/plugin-x']
8339+
.every((v) => mentionIn('plugin-x', `export const E = { v: '${v}' };`) === null),
8340+
'WP-U8: `@scope/plugin-x` is another package than an unscoped `plugin-x`, bare, in a path or in a URL');
8341+
assert(['/repo/node_modules/plugin-x/dist/index.js', 'https://npm.im/plugin-x']
8342+
.every((v) => mentionIn('plugin-x', `export const E = { v: '${v}' };`)?.kind === 'mention'),
8343+
'WP-U9: an unscoped `plugin-x` in a path or a URL is still a mention (the `/` refused is only a scope\'s)');
8344+
}
8345+
8346+
// ---- DX (#26324): an export named `default` ------------------------------
8347+
//
8348+
// `default` is an ordinary key in exactly the files predicate 2 reads -- a field
8349+
// option `{ value: 'technology', default: true }`, a Zod `.default(...)` -- so the
8350+
// bare-word arm refused every `default` retirement on every tree (measured on
8351+
// #26206's `@objectstack/driver-memory#default`). The import arm is the reading:
8352+
// a metadata surface reaches a default export only through an import binding it.
8353+
battery('DX (#26324): an export named `default` is reached through an import, never through the word');
8354+
{
8355+
const DX_KEYS = {
8356+
'packages/platform-objects/src/crm/account.object.ts':
8357+
"export const Account = { fields: { industry: { options: [{ label: 'Technology', value: 'technology', default: true }] } } };\n",
8358+
'packages/spec/src/data/field.zod.ts': "import { z } from 'zod';\nexport const FieldSchema = z.object({ label: z.string().default('') });\n",
8359+
};
8360+
const DX_HOST = 'packages/platform-objects/src/ui/host.object.ts';
8361+
const dx = (surfaces = {}) => run(mk(REX_FIXTURE(['default'],
8362+
'- FROM `default` TO `EmailServicePlugin`: register `new EmailServicePlugin()` with the kernel.', {
8363+
baseFiles: { [rexPath('src/index.ts')]: `${REX_INDEX_BASE}export default { id: 'com.objectstack.plugin.email' };\n`, ...DX_KEYS, ...surfaces },
8364+
})));
8365+
const DX_P2 = /`runtime-export-retired @objectstack\/plugin-email#default` \[predicate 2: no-metadata-surface\] is false: packages\/platform-objects\/src\/ui\/host\.object\.ts \(an object definition\) IMPORTS `default`/;
8366+
8367+
const dxG1 = dx();
8368+
green('DX-G1 THE #26206 SHAPE: a retired default export, beside unrelated `default:` keys and `.default(...)` calls in metadata surfaces', dxG1);
8369+
assert(/default \(export, packages\/plugins\/plugin-email\/src\/index\.ts\)/.test(dxG1.judged[0]?.detail ?? ''),
8370+
`DX-G1b: the verdict names the \`default\` operand and the entry that declared it. Got: ${dxG1.judged[0]?.detail}`);
8371+
green('DX-G2 a metadata surface that quotes the default import only in a COMMENT', dx({
8372+
[DX_HOST]: `// import emailPlugin from '${REX_PKG}';\nexport const Host = { name: 'host', default: true };\n`,
8373+
}));
8374+
green('DX-G3 a metadata surface that imports ANOTHER module\'s default', dx({
8375+
[DX_HOST]: "import other from '@objectstack/spec';\nexport const Host = { name: 'host', plugin: other };\n",
8376+
}));
8377+
red('DX-R1 a metadata surface that IMPORTS the package\'s default', dx({
8378+
[DX_HOST]: `import emailPlugin from '${REX_PKG}';\nexport const Host = { name: 'host', plugin: emailPlugin };\n`,
8379+
}), [DX_P2]);
8380+
red('DX-R2 ...and the same default named inside the braces, `{ default as x }`', dx({
8381+
[DX_HOST]: `import { default as emailPlugin } from '${REX_PKG}';\nexport const Host = { name: 'host', plugin: emailPlugin };\n`,
8382+
}), [DX_P2]);
8383+
// A binding of the whole MODULE reaches its default one member read away; every
8384+
// other export name refuses these through its bare-word arm, so `default` must too.
8385+
red('DX-R3 a NAMESPACE import of the package (`ns.default` one read away)', dx({
8386+
[DX_HOST]: `import * as email from '${REX_PKG}';\nexport const Host = { name: 'host', plugin: email.default };\n`,
8387+
}), [DX_P2]);
8388+
red('DX-R4 a DYNAMIC `import()` of the package', dx({
8389+
[DX_HOST]: `export const Host = { name: 'host', load: () => import('${REX_PKG}').then((m) => m.default) };\n`,
8390+
}), [DX_P2]);
8391+
red('DX-R5 a `require()` of the package', dx({
8392+
[DX_HOST]: `export const Host = { name: 'host', plugin: require('${REX_PKG}').default };\n`,
8393+
}), [DX_P2]);
8394+
red('DX-R6 a NAMESPACE re-export, `export * as ns from`', dx({
8395+
[DX_HOST]: `export * as email from '${REX_PKG}';\nexport const Host = { name: 'host' };\n`,
8396+
}), [DX_P2]);
8397+
8398+
const DX_DECL = rexPath('src/index.ts');
8399+
const dxPkgs = () => new Map([[REX_PKG, { private: false, file: rexPath('package.json') }]]);
8400+
const dxRef = (text) => metadataSurfaceReference('default', DX_DECL, new Map([['packages/spec/src/a.zod.ts', text]]), dxPkgs);
8401+
assert(dxRef(`import email, { LogTransport } from '${REX_PKG}';\n`)?.kind === 'import' &&
8402+
dxRef(`import type email from '${REX_PKG}/sub';\n`)?.kind === 'import' &&
8403+
dxRef(`import { z } from 'zod'; export {\n default,\n} from '${REX_PKG}';\n`)?.kind === 'import',
8404+
'DX-U1: a default binding beside named bindings, a type-only default import of a subpath, and a ' +
8405+
'multi-line `export { default } from` after a `;` on the same line, are imports');
8406+
assert(dxRef(`import { LogTransport } from '${REX_PKG}';\nexport const A = { default: LogTransport };\n`) === null &&
8407+
dxRef(`import type { T } from '${REX_PKG}';\nexport const A = z.string().default('');\n`) === null,
8408+
'DX-U2: named bindings alone, a `default:` key and a `.default(...)` call never reach the default export');
8409+
assert(metadataSurfaceReference('LogTransport', rexPath('src/log-transport.ts'),
8410+
new Map([['packages/spec/src/a.zod.ts', 'export const A = { LogTransport: true };\n']]), dxPkgs)?.kind === 'mention',
8411+
'DX-U3: every other export name keeps the bare-word arm (a key named after it is still a mention)');
82658412
}
82668413

82678414
// ---- G6: a changeset that was ALREADY breaking at base is inherited -------

0 commit comments

Comments
 (0)