Skip to content

fix(adr-0087): predicate 2 reads a default export through its import, and a package name inside a path or URL - #27207

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-mvhzud
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-mvhzud

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

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 导出的退役都无法如实申报,卡住了 packages: 4 ESM-only subpaths answer require() with ESM, and driver-memory's default export is lost under CJS (attw CJSResolvesToESM / FalseExportDefault) #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

  • check-adr-0087-registration: runtime-export-retired can never certify an operand named default (predicate 2 word-scans default across every metadata surface) #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.
  • ADR-0087 gate: a whole-package #* operand passes when a metadata surface names the package only inside a path or URL string #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 packages: 4 ESM-only subpaths answer require() with ESM, and driver-memory's default export is lost under CJS (attw CJSResolvesToESM / FalseExportDefault) #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.
Self-test under the verification lock, at `f402e245`
$ 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

Acceptance notes

  • The driver-memory measurement (the real shape packages: 4 ESM-only subpaths answer require() with ESM, and driver-memory's default export is lost under CJS (attw CJSResolvesToESM / FalseExportDefault) #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

Derived gates (dispatch-gates --commands, 33 families) at `f402e245` (merge base `b322b5da`)
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).

Generated by Claude Code

…t, and a package name inside a path or URL

An export named `default` is read through the import arm only: `default` is an
ordinary key in metadata surfaces, so the bare-word arm refused every default
retirement. A whole package's mention regex now admits a `/`-preceded
occurrence (a path or URL in a string) while still refusing a scope segment,
so an unscoped `plugin-x` is not `@scope/plugin-x`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziGaNnGcwiYSC9Qzck
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: df862934541eca57449da99ad633e7a6fa746f83
Local-runs: probe — one node run of a scratch script holding the three regexes copied verbatim from the head (DEFAULT_BINDING_IMPORT_RE, BRACED_IMPORT_RE at L3217-3219; packageReference's mention at L4867, beside main's) against 27 crafted import shapes and 27 crafted string values; nothing built, no gate or self-test re-run, no worktree, no ablation
Scope: scripts/check-adr-0087-registration.mjs (the one changed path; reviewFace returns no hits for scripts/**, so the schema-face floor is the diff itself) + the cited code read unchanged at the head: metadataSurfaceReference (L3170-3205), importSpecifiersFor (L2973), specifierNamesModule / owningPackageName (L2982-3009), metadataSurfaceKind / metadataSurfaceFilesAt (L2783-2797), the runtime-export-retired predicate-2 caller (L5176-5262), parseExportOperand / PACKAGE_NAME_RE (L4501-4540), the self-test ledger (SELF_TEST_BATTERIES, SELF_TEST_BATTERY_FLOOR L500, the floor check L9735-9775, red/green/assert L5960-6085, the REX and WP fixtures L7653-7760, 8216-8235); scripts/js-comment-mask.mjs (maskComments → blank, spaces for comment bytes, newlines kept, strings left standing); docs/adr/0087-metadata-protocol-upgrade-contract.md on main 4265176c (D7 step 4, D9 predicate 2, the second 2026-10-27 note L1146-1183); packages/drivers/driver-memory/src/index.ts and examples/app-crm/src/objects/account.object.ts on main, and the 418 metadata-surface paths main's classifier selects, grepped read-only for the shapes judged below. No remainder: the diff is one file and it is in scope.

Inputs read (GET only): card #26324 body and all 4 comments (triage 6428250876, serial-wait 6430725274, claim 6465577081, os-dev-report 6466700067); card #27022 body and both comments (triage 6459401704, claim 6465589776); the director's contract review 6458009682 on PR #26766; PR #27207 body, file list (1 file, +126/−4), commits (2e1e09d5 fix, df862934 merge of main), zero issue comments, zero review comments, zero reviews; the net diff against main; check-runs on df862934 read at 10:35:02Z (12 in_progress), 10:40:57Z (1 in_progress), 10:42:31Z (1 in_progress, Test Core aggregator) and 10:43:10Z: 45 check-runs, 26 success, 19 skipped, 0 failure, 0 in_progress. The head had not moved at 10:43Z (draft: true, mergeable_state: clean).

Gate verdicts on the head: Lint · docs & code guards (where pnpm check:changeset-gate-self-tests runs this gate's --self-test), Lint · scoped · tooling self-tests, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · ESLint, every Type Check lane, Test Core and its two shards, PR-body guards, Governed Surface Queue Guard, Check PR Size: all success. Check Changeset success (the skip-changeset label is on). The gate's --base scan judged nothing on this head (no changeset in the diff), as expected. The dev's 33 derived families at exit 0 and --ran: 33 run, 0 NOT-MEASURED are the dev's report; the CI lanes above are what this record measured.

① Derived judgments

1. #26324, the default operand — the three ordinary shapes are read, right, and other names are untouched. metadataSurfaceReference (L3171) routes symbol === 'default' to defaultImportReference (L3236-3247) before the bare-word arm; every other name falls through to the unchanged word scan, declaresLocally, member-offset clearing and importSpecifiersFor (L3172-3204 are byte-identical to main). The new reader masks comments with the shared maskComments (spaces, newlines kept, so the m-flag ^ anchor and [ \t]* land where a docblock stood), collects every specifier of a default-binding import (DEFAULT_BINDING_IMPORT_RE) and of a braced import/export … from whose brace content holds the word default (BRACED_IMPORT_RE), and attributes each through the existing specifierNamesModule (relative resolution; bare name equal to the declaring package or a subpath of it, the coarse refuse-direction match). Probe, head regexes, specifier @objectstack/driver-memory: import x from, import x, { y } from, import x, * as ns from, import type x from 'pkg/sub', a multi-line import x, {\n y,\n} from, comment spans blanked inside the statement, import { default as x } from, import {\n type default as X,\n} from, export { default } from, export { default as x } from, indented and double-quoted, after ; on the same line, after CRLF, with with { type: 'json' }: all read as import. A named-bindings-only import, a default: key and a .default('') call: none read (DX-U2 pins this). import type from 'pkg' (a default binding named type) is read.

2. #26324 — the shapes the reader does NOT see, graded. The brief asked for one case per shape where the gate now passes while a metadata surface really reaches the default export. Found, by probe:

  • (a) import * as ns from '@objectstack/driver-memory' then ns.default — not read, admitted (the dev declares it, PR body "Not read for default, by design").
  • (b) const m = await import('@objectstack/driver-memory') / require(…) then m.default — not read, admitted (declared likewise).
  • (c) export * as ns from '@objectstack/driver-memory' (re-exports the namespace, default included) — not read, admitted, not declared.
  • (d) import{default as x}from'pkg' with no whitespace after import/export — not read (\s+ is required); importSpecifiersFor has the same limit, so other names fall to the bare-word arm here while default has no fallback.
  • (e) A BOM before a first-line import x from — [ \t]* does not cross U+FEFF (the old arm's \s* does). No metadata surface on main carries a BOM (grepped, 0 files).
    For every other export name, shape (a) is REFUSED today as a mention (the word scan hits ns.Foo, no specifier names Foo, so the branch returns mention), and so is (b): the default reading is laxer than every other name's on exactly these two shapes, and D9 predicate 2 → D7 step 4 ("no metadata surface references it in code") refuses them by text. ⚠️ The dev's reason for leaving (a) and (b) unread — "both reach the export through a member named default, the very spelling that cannot be told from the object keys" — does not hold for (a): the namespace binding is what tells it (import * as NS from 'DECLARING-MODULE' followed by \bNS\s*\.\s*default\b is linear and unambiguous), and for (b) the conservative D7 reading is a dynamic import('DECLARING-MODULE') in a metadata surface at all, exactly as packageReference's spec already treats import('…') as an import for PACKAGE#*. Graded should-fix, not blocking, on the same calibration the director applied to the path/URL residual on PR feat(adr-0087): retire a deleted published package whole (PACKAGE#*, D9 amendment) #26766 (6458009682 ①-1): on main's 418 metadata surfaces there are 0 namespace imports, 0 default imports, 0 import{ spellings, and every import( / require( hit is inside a comment or a .describe() string; the shapes the card measured and triage bound ("read the import arm only … import x from 'PKG'") are all refused and pinned (DX-R1, DX-R2, DX-U1). A card should carry (a), (b) and (c): one ns.default red, one dynamic-import red, both read through the declaring specifier.
  • False-refuse side (the second-worst direction): export { x as default } from 'DECLARING-MODULE' is read as reaching default (the brace content holds the word) and a default import written on its own line inside a template literal is read as a statement (strings are left standing, the same posture as the old arm). Both unrealistic in a metadata surface; noted.

3. The performance claim. importSpecifiersFor's pattern (?:^|\n)\s*(?:import|export)\s+([\s\S]*?)\s*from\s*['"]… carries a lazy [\s\S]*? behind a \s* that can start at every newline of a masked file, so the super-linear behaviour the dev measured on discovery.zod.ts is the shape the code predicts; the two new regexes are anchored at ^/; and have no lazy any-char scan ([^}]* and [^'"\n]+ are bounded). The dev's "left as it is: its callers gate it on the word being present" is accurate (L3198 runs it only after three word tests pass). Not re-measured here.

4. #27022, the mention regex — right, and the brief's checks all hold. Head: (?<![\w@.-])(?<!@[\w.~-]+/)NAME(?![\w.-]); main: (?<![\w@./-])NAME(?![\w.-]). Probe on the two regexes side by side, unscoped operand plugin-x: '@scope/plugin-x', '/repo/node_modules/@other/plugin-x/dist/index.js', 'https://npm.im/@other/plugin-x' → null on both (the second lookbehind refuses a / that closes an @scope segment, scope characters [\w.~-]); 'plugin-xy', 'my-plugin-x' → null (boundaries unchanged); 'plugin-x@1.0.0' → mention on both (the version spec stays refused, @ is not in the lookahead set); '/repo/node_modules/plugin-x/dist/index.js', 'https://npm.im/plugin-x', 'https://x.dev/plugin-x?v=1&q=2', 'node_modules/.pnpm/plugin-x@1.0.0/node_modules/plugin-x/index.js' → mention on head, null on main (the #27022 gap, closed); Windows 'C:\\…\\node_modules\\plugin-x\\dist' → mention on both. Scoped operand @object-ui/plugin-x: '/repo/node_modules/@object-ui/plugin-x/dist/index.js', 'https://npm.im/@object-ui/plugin-x', the pnpm store path node_modules/.pnpm/@object-ui+plugin-x@1.0.0/node_modules/@object-ui/plugin-x/dist → mention on head, null on main; '@object-ui/plugin-xy', 'x@object-ui/plugin-x' → null. The comment-only case stays green by construction: mention runs on maskComments(rawText) and WP-G3 is unchanged in the diff. Imports are still reported as import because spec is tried first (unchanged). The widening is refuse-direction only, as the body says.

  • Refuse-direction residuals, all for an UNSCOPED operand only: a relative specifier or path to a local file of the same name ('./plugin-x', now a mention; was null on main), a bare @/ alias segment ('@/plugin-x', the scope lookbehind needs one character after @), Windows '@scope\\plugin-x' and the pnpm store spelling @scope+plugin-x (both already mentions on main). The workspace's only unscoped published package is create-objectstack (every other published name is @objectstack/* or @object-ui/*), and a metadata surface naming a local file ./create-objectstack is not a realistic shape. Noted, not blocking.

5. Battery — the new cases pin what they claim; nothing existing was edited or removed. The diff has no − line inside selfTest() other than the WP floor 55 → 59; every other hunk in the self-test is an addition. Counted from the diff text (one per green, one plus the regex count per red, one per assert): DX = 3 greens + 2 reds × (1 + 1) + 4 asserts = 11, equal to the pinned floor; WP adds 4 asserts, 55 → 59; 682 + 15 = 697, the figure the body and the dev report print and the Lint lane measured success. DX-G1's fixture retires default for real: base REX_INDEX_BASE + export default {…}, head REX_INDEX_HEAD (no default), beside a metadata surface carrying default: true and one carrying .default('') — the #26206 shape; DX-G1b asserts the verdict names default (export, packages/plugins/plugin-email/src/index.ts). DX-R1/R2's regex matches the caller's refusal text at L5244-5247 exactly (… (an object definition) IMPORTS \default`). DX-G2 (comment-quoted import), DX-G3 (another module's default via @objectstack/spec, refused by specifierNamesModule), DX-U1 (the ;-anchored multi-line export {\n default,\n} from), DX-U2, DX-U3 (another name keeps the bare-word arm) each pin the branch they name. WP-U6/U7 assert kind === 'mention'AND the reported line; WP-U8 covers bare, path and URL; WP-U9 pins that the refused/` is only a scope's.

  • ⚠️ Should-fix: SELF_TEST_BATTERY_FLOOR stays 58 while the roster grows to 59. Every prior battery addition on main bumped it (55→56 46255b19, 56→57 e890bedc, 57→58 4bce21da); the floor's own comment says a battery deleted from the roster "takes its own floor with it", and at 58 the DX entry is the one battery whose deletion, block and roster line together, would trip nothing. One line: const SELF_TEST_BATTERY_FLOOR = 59;. Not blocking: the per-battery floor DX: 11 is live and is what catches cases that stop running.

6. The driver-memory probe — the code supports the dev's three readings. On main 72629f38's gate, metadataSurfaceReference('default', 'packages/drivers/driver-memory/src/index.ts', …) word-scans \bdefault\b; examples/app-crm/src/objects/account.object.ts:27 carries { label: 'Technology', value: 'technology', default: true } (an object definition under main's classifier), is not declaresLocally, has no import → mention → exit 1, the card's reading. With this PR's gate: defaultImportReference reads the 418 surfaces; none on main imports @objectstack/driver-memory at all (grep: the name occurs only in comments and as a string key in error-code-ledger.zod.ts:1406, which this operand's reader does not read) → null; predicate 1 holds (export default { at index.ts:64 on main, deleted in the probe head; the symbol's declaration file is index.ts, the verified: line's path) → exit 0. With a base-side .object.ts carrying import memoryPlugin from '@objectstack/driver-memory': DEFAULT_BINDING_IMPORT_RE yields the specifier, owningPackageName(declPath) is @objectstack/driver-memory, equal → { kind: 'import' } → exit 1 with "IMPORTS default from the module that declares it". Not re-run (read-only).

7. Declarations. Clause-②: no — right: scripts/** is unpublished, the root is private, no published accept set moves; the claim carried no and the body copies it, so the order's expected n/a is a spelling difference the dev surfaced, not a deviation. Metadata-protocol: none — right, no metadata shape moves. Skills-docs: none — confirmed: git grep over skills/ on main for runtime-export-retired, no-metadata-surface, check-adr-0087, adr-0087: 0 hits; neither predicate is taught there. skip-changeset — right. ADR text: D9's second 2026-10-27 note already reads "any other occurrence of the package name, a string value included" (L1174-1175), which the path/URL mention now satisfies, and the default reading is D7's import primitive the note points to; no ADR line needs to move. Fixes #26324: the card's Expected (import arm only; a green with an unrelated default: key, a red with the import) is met by DX-G1/DX-R1; its "Also seen (not filed)" item (predicate 3's FROM reader on a two-span line) is noted-only and outside the card. Fixes #27022: the card's Expected (path/URL mention with a U-case, WP-G3 and WP-U4 still green) and triage's two asks (a path U-case and an @scope/ non-match U-case) are met by WP-U6/U8. Nothing on either card is left undone.

② Semver level

No release: skip-changeset is correct (root private, scripts/** unshipped, no package version moves). Clause-②: no, Metadata-protocol: none. Accept set of the gate: narrowed for PACKAGE#* (more refused), widened for an export operand named default only (the bare-word arm no longer refuses it); every other export name's reading is unchanged, read off L3172-3204.

③ Boundary flags

  • (a) ns.default and dynamic import() / require() not read for default (dev flag, PR body "Not read for default, by design"). Accepted as declared, should-fix, not blocking — see ①-2: the namespace shape IS tellable by its binding name and the dynamic shape by its specifier, both shapes are refused today for every other export name, and the ADR text refuses them; 0 occurrences on main's metadata surfaces. Escalated to the seat as a card to file against this gate (three reds: namespace, dynamic import, export * as ns from).
  • (b) importSpecifiersFor left super-linear, "noted, not filed". Accepted: its callers gate it on three word tests (L3173, 3184, 3197), so no current run is slow; the dev's out_of_scope_findings line carries it.
  • (c) Clause-②: no where the order expected n/a. Accepted: both claims carry no, the body copies the claim, and no is accurate (no published accept set moves).
  • (d) Roster floor not bumped (not a dev flag; found here). Should-fix, one line, ①-5. Not blocking.
  • (e) Refuse-direction residuals for an unscoped operand (./plugin-x, @/plugin-x). Noted in ①-4; no unscoped published package but create-objectstack, and no realistic metadata surface writes the shape. Not blocking, no card asked.
  • (f) The branch name. claude/project-thread-mvhzud is the platform-assigned thread branch, written as measured in both claims; it is the PR's head ref. Accepted.
  • (g) Gates. All 45 check-runs completed success or skipped at 10:43:10Z, none failed; the gate's own --self-test ran success in Lint · docs & code guards. The 33 derived families at exit 0 are the dev's report, consistent with the CI lanes, not re-run here.
  • (h) Approval state. draft: true, zero reviews on the PR; Tier H approval is not this record's to give. The seat's ACCEPT may proceed on this head.
  • (i) Probe disclosure. One local run, declared on the Local-runs: line: a scratch script with the three regexes copied from L3217-3219 and L4867, run once against crafted strings. No build, no gate run, no self-test run, no worktree.

Should-fix, none blocking: a card for ③-(a); SELF_TEST_BATTERY_FLOOR = 59 (①-5), which the dev may land on this PR or the next touch of the file.

Implemented-by: claude/project-thread-mvhzud
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

VERDICT: PASS

…ic load of its module

A namespace import, a namespace re-export, or a dynamic import()/require()
of the declaring package from a metadata surface now refuses a `default`
operand, as the bare-word arm refuses them for every other export name.
The battery roster floor moves to 59 for the DX battery.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziGaNnGcwiYSC9Qzck
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f402e245ca3693c32d0ec7059a075d2d0d40c0e1
Local-runs: probe — one node run of a scratch script holding the two new readers copied verbatim from the head (NAMESPACE_IMPORT_RE, DYNAMIC_IMPORT_RE, L3222-3223) against 18 crafted strings and four 1-2 MB pathological inputs timed; nothing built, no gate or self-test re-run, no worktree, no ablation
Scope: scripts/check-adr-0087-registration.mjs (the one changed path, read as the delta df862934...f402e245, +34/−9, one commit) + the cited code read unchanged at the new head: defaultImportReference whole (L3240-3253), DEFAULT_BINDING_IMPORT_RE / BRACED_IMPORT_RE (L3219-3221), specifierNamesModule / owningPackageName (L2982-3009), the runtime-export-retired predicate-2 caller, SELF_TEST_BATTERIES and SELF_TEST_BATTERY_FLOOR (L423-500) at main 4265176c, df862934 and f402e245, the floor check (L9760-9800); scripts/js-comment-mask.mjs (maskComments); the 418 metadata-surface paths main's classifier selects, grepped read-only for namespace imports, namespace re-exports and quoted dynamic import( / require( calls. No remainder.

Round 2 on the head that answers the PASS record on df862934 (comment 6466959531, its two should-fixes ③-(a) and ①-5). Inputs read: the compare diff df862934...f402e245 and the commit message of f402e245 ("fix(adr-0087): a default operand is reached by a namespace or dynamic load of its module"); the PR body as the seat applied it (diffed line by line against the round-1 body: the 「维护者速读」 bullets, the "Round 2" bullet, the DX roster line, 697 → 705, the 9dacccf9 probe row, the rewritten acceptance note, the derived-gates block at f402e245); zero new PR comments or reviews; check-runs on f402e245 read at 11:13:17Z (1 in_progress, PR-body guards) and 11:14:47Z: 44 check-runs, 26 success, 18 skipped, 0 failure, 0 in_progress; the head had not moved at 11:14Z (draft: true, mergeable_state: clean). Everything round 1 read stands and is not re-read.

Gate verdicts on the head: Lint · docs & code guards (where pnpm check:changeset-gate-self-tests runs this gate's --self-test) and Lint · scoped · tooling self-tests success; every other Lint lane, every Type Check lane, Test Core and its shards, PR-body guards, Governed Surface Queue Guard, Check Changeset, Check PR Size: success. The dev's 33 derived families at exit 0 at f402e245 are the dev's report, consistent with the lanes.

① Derived judgments

1. Should-fix ③-(a) — fixed, and it closes the admit gap round 1 named. Two readers are added beside the two of round 1 and feed the same specifiers list, attributed through the same specifierNamesModule (L3251): NAMESPACE_IMPORT_RE — statement-anchored ((?:^|;)[ \t]*), import or export, optional type, * as IDENT from 'spec'; DYNAMIC_IMPORT_RE — \b(?:import|require)\s*\(\s*'spec'\s*\). The prefilter admits a file holding require as well (L3242). Probe, head regexes: import * as ns from 'M', import type * as ns from 'M', export * as ns from 'M', import('M'), import("M/sub"), require('M') → each yields the declaring specifier; export * from 'M' (which does not re-export default) yields nothing, right. The refusal rests on the binding of the module, never on a .default read, exactly as the body says, and that is the posture every other export name already has on these shapes (round 1 ①-2). The three shapes round 1 listed — (a) namespace import, (b) dynamic import()/require(), (c) export * as ns from — are all read; DX-R3 to DX-R6 pin each with the predicate-2 refusal regex DX_P2, in the full run() path.

2. Over-refusal — the questions asked, answered.

  • A comment: both readers run on maskComments(rawText) (L3243), so a comment-quoted import * as ns from 'M' or import('M') is spaces; probe on a blanked line yields nothing. DX-G2 keeps pinning the comment case for the import arm.
  • A different package sharing the prefix: import * as ns from '@objectstack/driver-memory-foo' and import('@objectstack/driver-memory-foo') are collected as specifiers but refused by specifierNamesModule (specifier === owner || specifier.startsWith(owner + '/')), so pkg-foo never names pkg. Right.
  • A string literal: NAMESPACE_IMPORT_RE is statement-anchored, so 'import * as ns from "M"' inside a string on a code line yields nothing; DYNAMIC_IMPORT_RE is NOT anchored, so a string literal quoting import("M") — a .describe('… import("@objectstack/driver-memory") …'), say — would refuse, and so would a method call named import (registry.import('M')), which \b does not keep out. Refuse-direction residuals, graded noted: main's 418 metadata surfaces carry 0 quoted-specifier import( or require( calls of any kind (grep \b(import|require)\s*\(\s*['"]: 0 lines; the five import( hits of round 1 are prose in comments and one .describe() string that names @libsql/client, not a workspace package), 0 namespace imports and 0 namespace re-exports. A (?<![.\w$]) lookbehind on the dynamic reader would close the method-call shape; a card is not owed for it.
  • Admit-direction residual, noted: a dynamic import( with a template-literal specifier (import(`M`)) is not read (the quote set is '"), the same set packageReference's spec uses. Not a realistic metadata-surface shape.

3. The readers are linear. Neither carries a lazy [\s\S]*?; the only unbounded classes are \s+/\s* runs between fixed tokens and [^'"\n]+ inside a quote pair, and each alternation is anchored by a literal keyword, so a failed match retreats by at most one token run. Probe timings on the head regexes over pathological inputs: 400 lines of import followed by 5000 spaces (2.0 MB) 24 ms; 400 unclosed import( … 'x lines (1.6 MB) 15 ms; 400 unterminated import * as x from + 5000 spaces lines (2.0 MB) 18 ms; 50 runs of 20000 ; before import * as x (1.0 MB) 4 ms. No catastrophic shape found.

4. The driver-memory probe stays green — the code supports it. On main, no metadata surface imports @objectstack/driver-memory by any of the four shapes: the name occurs only in comments, in .describe() / message strings (discovery.zod.ts:72, filter.zod.ts, query.zod.ts, error-code-ledger.zod.ts:2057) and as a string key (error-code-ledger.zod.ts:1406), none of which is a statement-anchored import or a quoted import(/require( call; so defaultImportReference returns null and the dev's exit 0 with verified: @objectstack/driver-memory#default (export, …/src/index.ts) on 9dacccf9 is what the code predicts. Not re-run (read-only).

5. Should-fix ①-5 — fixed, and the bump matches the roster. SELF_TEST_BATTERY_FLOOR 58 → 59 (L500). Counted from the file text: SELF_TEST_BATTERIES holds 71 entries on main 4265176c and 72 at both df862934 and f402e245 — one battery added (DX), one floor step, the convention every prior addition followed (55→56→57→58). The DX entry's own floor moves 11 → 19: 4 new red × (1 + 1 regex) = 8, 11 + 8 = 19, equal to the pin; the self-test total 697 + 8 = 705, the figure the body prints and the Lint lane measured success. The delta removes no existing assertion (the diff's only − lines are the two pins, the prefilter line and the docblock paragraph it rewrites).

6. Declarations and body. Clause-②: no, Metadata-protocol: none, Skills-docs: none, skip-changeset, Fixes #26324 / Fixes #27022: unchanged and still right (the delta is the same unpublished script). The body now says four linear regexes, names DX-R3 to R6, prints 705, carries the round-2 probe row and replaces the "Not read for default, by design" note with the binding-not-member note; the 「两分钟验证」 promise is 705 assertions, matching the pin. The ADR needs no line: D7 step 4's "no metadata surface references it" is what the readers now enforce for default.

② Semver level

Unchanged from round 1: no release, skip-changeset correct, Clause-②: no, Metadata-protocol: none. Accept set of the gate for a default operand narrows against df862934 (three more shapes refused), still wider than main only in that the bare-word arm no longer refuses it; every other export name's reading is unchanged.

③ Boundary flags

  • (a) Namespace and dynamic loads refused on the binding, not on .default (dev's round-2 note). Accepted: the conservative direction, the one every other export name takes on these shapes, and the one D7 step 4's text reads. Round 1's ③-(a) is closed; no card needed.
  • (b) DYNAMIC_IMPORT_RE is not statement-anchored (a string literal quoting import("M"), or a method named import, would refuse a default operand). Refuse direction, 0 occurrences on main's surfaces, noted — not blocking, no card asked; a (?<![.\w$]) lookbehind is the one-token fix if it ever bites.
  • (c) Template-literal dynamic specifier not read. Admit direction, unrealistic, same quote set as packageReference; noted.
  • (d) Roster floor — round 1's ①-5 closed.
  • (e) Gates. 44 check-runs on f402e245 all completed success or skipped at 11:14:47Z; none failed; the gate's own --self-test ran success in the Lint lanes.
  • (f) Approval state. draft: true, zero reviews; Tier H approval is not this record's to give. The seat's ACCEPT may proceed on this head.
  • (g) Probe disclosure. One local run, declared on the Local-runs: line.

Should-fix: none. Noted only: ③-(b), ③-(c).

Implemented-by: claude/project-thread-mvhzud
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:ADR-0087 登记门禁的谓词 2 修了两处。
    • (1) 退役的导出名叫 default 时,只看元数据面是否真的引用了这个包的默认导出。认的写法有:默认导入、{ default as x }、export { default } from、命名空间导入或再导出、动态 import() / require()。不再把对象里普通的 default: true 键当成引用。
    • (2) 包名#* 的退役,现在也认路径或 URL 里出现的包名,但不会把 @scope/plugin-x 误认成 plugin-x。
  • 为什么改:以前 default 导出永远无法如实登记退役,阻塞了 packages: 4 ESM-only subpaths answer require() with ESM, and driver-memory's default export is lost under CJS (attw CJSResolvesToESM / FalseExportDefault) #26206 去掉 driver-memory 的遗留默认导出。另外,路径里的包名会被漏放。
  • 风险与回滚:只改门禁脚本(含其自测),不改产品代码。其它导出名的判定与 main 逐字相同。回滚 = revert 本 PR。
  • 先看:scripts/check-adr-0087-registration.mjs 的 defaultImportReference 与 packageReference 的提及正则。
  • 两分钟验证:node scripts/check-adr-0087-registration.mjs --self-test 预期 705 条断言全过。
  • 需要你做的:无,席位落地。

席位意见:接受。契约复核两轮 PASS(6466959531、6467569746)。第一轮的两处应修(命名空间、动态导入的放行缺口,以及自测套数下限)已在第二轮修好。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-28T11:17Z

Head: f402e245ca3693c32d0ec7059a075d2d0d40c0e1 · cards #26324 and #27022 · claims 6465577081 and 6465589776

  1. 形态: a draft PR to main carrying Fixes #26324 and Fixes #27022. One path, scripts/check-adr-0087-registration.mjs (+152 / −5), which includes its self-test battery. It sits inside both claims' surface, with the skip-changeset label. check-governed-merges --pr 27207 reports NOT governed, so the ordinary queue landing applies. The assignee is os-support-ai.
  2. 前提实测: both premises hold on main. Predicate 2 word-scanned a default export name across metadata surfaces, so it hit { …, default: true } on every tree. And a <package>#* mention regex excluded any /-preceded occurrence. The review read the head:
    • Round 1 confirmed every ordinary default-import shape is read, and every other export name reads byte-identical to main.
    • Round 1 confirmed the new lookbehind pair (?<![\w@.-])(?<!@[\w.~-]+/)NAME(?![\w.-]) admits path, URL, pnpm-store and Windows mentions, while still refusing @scope/plugin-x for an unscoped plugin-x.
    • Round 2 confirmed that a namespace import, a namespace re-export and a dynamic import() / require() now refuse a default operand, and that the readers stay linear (4–24 ms on 1–2 MB pathological inputs).
  3. 门禁 check-runs (on this head): 44 runs read at 2026-10-28T11:14Z: 26 success, 18 skipped, none red or pending. The gate's --self-test lanes are green. Dev-side readings:
    • 705 assertions, exit 0, under the lock;
    • 33 of 33 derived gates, exit 0, 0 NOT-MEASURED;
    • the @objectstack/driver-memory#default probe exits 1 with main's gate and 0 with this gate, and exits 1 again when a base-side object file imports the default.
  4. 接受的偏差:
    • (a) The default reader uses two linear regexes instead of reusing importSpecifiersFor, which measured super-linear (41 s on one file). The PR body explains this.
    • (b) Clause-②: no where the order said n/a, copied from the claims; this is tooling only.
    • (c) Accepted as noted by the round 2 review, not owed: DYNAMIC_IMPORT_RE is not statement-anchored, so a string literal quoting import("M"), or a method named import, would over-refuse. That is the safe direction, with 0 such shapes on main. A template-literal specifier is not read; that is unrealistic, and it shares its quote set with packageReference.
  5. 范围外去向: none owed. packages: 4 ESM-only subpaths answer require() with ESM, and driver-memory's default export is lost under CJS (attw CJSResolvesToESM / FalseExportDefault) #26206's second half (dropping driver-memory's legacy default export) is unblocked once this lands. It belongs to that card's own lane.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (round 2 applied and read back) and the gate's inline docs. The seat readies the PR and arms auto-merge in this act, then follows it to MERGED, and lands both cards.

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

Projects

None yet

2 participants