Skip to content

Commit 6d86e4a

Browse files
test(runtime,metadata-protocol,objectql): pay the lint root's first load at module top where a clocked case paid it (#29785)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi9paTx1Lu8ENYwwssthfK3h)_ Fixes #29627 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只改测试。三个包各加一个测试专用的预载模块(`lint-root-preload.testkit.ts`),在模块顶部加载 `@objectstack/lint` 包根;实测会在用例里冷加载它的 66 个测试文件(runtime 9、metadata-protocol 39、objectql 18)在文件顶部各引入一行。objectql 另加 lint 的 devDependency 和锚定源码别名。 - 为什么改:第一次读视图或发布对象时,协议层才懒加载 lint 包根,vitest 下这次冷编译落在某个计时用例里:metadata-protocol 和 objectql 的这类用例空闲时约 0.7–0.8 秒,四路并发下最慢 2.3 秒,而 5 秒超时是同一个用例的预算,分片负载一高就可能超时变红。现在这笔开销在收集阶段付掉,这些用例降到几十毫秒。 - 风险与回滚:不动产品代码,不加超时,不重试,不跳过,断言一条没改。回滚即还原本 PR。唯一越出测试文件的是 objectql 的 `package.json`/`pnpm-lock.yaml` 多一条 devDependency。 - 先看:`packages/objectql/package.json` 与 `packages/objectql/vitest.config.ts`(新依赖与别名),`packages/metadata-protocol/src/lint-root-preload.testkit.ts`(为何不用 setupFiles 的实测),任一个测试文件顶部新增的两行。 - 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.lock-door-read-agree.test.ts --reporter=verbose`,预期全绿,`an overlay view declaring _lock=none` 那个用例约 45 毫秒(main 上约 0.8 秒)。 - 需要你做的:无,席位落地。 ## What changes - **One preload testkit per package**: `packages/runtime/src/lint-root-preload.testkit.ts`, `packages/metadata-protocol/src/lint-root-preload.testkit.ts`, `packages/objectql/src/__fixtures__/lint-root-preload.testkit.ts`. Each is a module-top `import '@objectstack/lint'` with a header naming the cost it moves and why it is not a setup file. - **66 test files** import their package's testkit at module top (two lines each: a one-line comment and the import): runtime 9, metadata-protocol 39, objectql 18. Each is a file whose traced run paid the lint root's first load inside a case. No `it`, `expect`, timeout or helper changes. - **objectql**: `@objectstack/lint` becomes a devDependency (`package.json`, 3 lines in `pnpm-lock.yaml`), and `vitest.config.ts` gains the anchored `^@objectstack/lint$` alias to `../lint/src/index.ts`. Both are forced, see Acceptance notes. - **runtime** `vitest.config.ts`: one comment on the existing `#29603` alias now names the testkit. No resolution change. ## Why A served view read wraps its body in `withFormLayoutWarnings`, which awaits `loadFormLayoutLint()` (`import('@objectstack/lint')`, `packages/metadata-protocol/src/metadata-diagnostics.ts`). An object publish's build probes load the same root the same way (`packages/metadata-protocol/src/build-probes.ts`). Under vitest the first such load in a file is a cold transform of the lint graph, inside whichever case gets there first. A module-top import is paid during collection, which no timeout clocks, and the protocol's later `import()` gets the same instance back. The lazy import itself is product design and stays as it is. ### How the population was measured Not by grep. A scratch tracer (deleted) appended one counter increment to the lint root's transformed module and recorded, per test file, whether the counter moved during collection, in a hook, or inside a case. One run of each package's whole suite (both vitest projects) on `7dae3b3c0d`: | package | files | load inside a case | at module top already | elsewhere | |---|---|---|---|---| | runtime | 532 | 9 | 1 (`meta-write-org-scope.test.ts`) | 0 | | metadata-protocol | 308 | 39 | 4 | 1, in a describe-level `beforeAll` with a 120000 ms budget (`protocol.read-lock-flags-write-door.test.ts`), dropped | | objectql | 544 | 18 | 0 | 0 | The card's coarse grep (real protocol plus a view read, minus files already importing the root) names 149 files in the three packages. The trace keeps 43 of them and drops 106, and it finds 23 positives the grep misses, mostly object publishes reaching the build probes. Of the card's three named runtime candidates, `package-revert-stored-members` and `meta-save-preconditions-parity` are kept; `packages-seed-apply-org-scope` is dropped (the lint root is never evaluated in that file). ### Measurements Each sample is one vitest process on one file (`--maxWorkers=1`, json reporter), under `os-verify-lock.sh` on a 4-vCPU shared box. "Idle" is 3 sequential runs. "4x" is the contention method from #29622: 4 identical copies of the same file run concurrently, 1 round, 4 samples. The number is the duration of the case the trace named. Before is `7dae3b3c0d`; after is `4bab1b183f` (the same base plus this change). 0 failed runs in 924 samples. | package | before idle p50 / max | before 4x p50 / max | after idle p50 / max | after 4x p50 / max | |---|---|---|---|---| | runtime (9 files) | 237 / 542 | 436 / 920 | 119 / 384 | 118 / 408 | | metadata-protocol (39) | 714 / 838 | 975 / 1241 | 25 / 65 | 29 / 88 | | objectql (18) | 796 / 1139 | 1169 / 2282 | 108 / 231 | 139 / 347 | In runtime the root already resolves to lint's source through #29622's alias, and that load is cheaper (roughly 100 to 300 ms) than the 1.5 MB `dist/` bundle metadata-protocol and objectql read (roughly 650 to 1100 ms). It still lands in the case, so the 9 files are kept. After the change, a targeted re-trace of the 66 files shows the root evaluated once per file, at collection, and 0 in-case loads. <details><summary>Per file, ms p50 / max</summary> **packages/runtime** (9 files), first lint-loading case, ms p50 / max | file | before idle | before 4x | after idle | after 4x | |---|---|---|---|---| | `domains/meta-app-package-id-key.test.ts` | 174 / 193 | 323 / 362 | 42 / 57 | 52 / 58 | | `domains/meta-package-all-fold.test.ts` | 231 / 237 | 348 / 493 | 78 / 81 | 110 / 112 | | `domains/meta-published-runtime-publish.test.ts` | 168 / 216 | 309 / 349 | 58 / 79 | 63 / 90 | | `domains/meta-save-preconditions-parity.test.ts` | 232 / 263 | 436 / 444 | 119 / 126 | 114 / 135 | | `domains/packages-export-base-unchanged-per-type.integration.test.ts` | 496 / 542 | 855 / 920 | 316 / 384 | 388 / 408 | | `domains/packages-publish-drafts-change-note.integration.test.ts` | 353 / 375 | 563 / 610 | 228 / 241 | 262 / 297 | | `package-list-commits-org-scope.integration.test.ts` | 328 / 344 | 525 / 685 | 167 / 178 | 236 / 260 | | `package-promote-runtime-owner-refusal.integration.test.ts` | 216 / 218 | 270 / 352 | 67 / 69 | 97 / 106 | | `package-revert-stored-members.integration.test.ts` | 394 / 395 | 508 / 772 | 212 / 240 | 247 / 274 | **packages/metadata-protocol** (39 files), first lint-loading case, ms p50 / max | file | before idle | before 4x | after idle | after 4x | |---|---|---|---|---| | `protocol-publish-drafts-closure.test.ts` | 728 / 759 | 922 / 1114 | 43 / 45 | 50 / 64 | | `protocol-publish-drafts-endpoint-gate.test.ts` | 763 / 826 | 1001 / 1085 | 43 / 43 | 53 / 81 | | `protocol-publish-drafts-object-field-refs.test.ts` | 751 / 816 | 851 / 880 | 31 / 31 | 43 / 52 | | `protocol-publish-drafts-org-scope.test.ts` | 755 / 758 | 1026 / 1038 | 35 / 46 | 46 / 57 | | `protocol-publish-drafts-package-scope.test.ts` | 770 / 834 | 958 / 967 | 35 / 36 | 32 / 36 | | `protocol.app-package-id-key.test.ts` | 715 / 754 | 951 / 958 | 9 / 11 | 13 / 14 | | `protocol.draft-publish-verdict.test.ts` | 738 / 777 | 1103 / 1211 | 16 / 16 | 18 / 28 | | `protocol.extension-field-suffix.test.ts` | 700 / 782 | 1046 / 1103 | 23 / 25 | 33 / 41 | | `protocol.flow-by-name-shipped-name.test.ts` | 761 / 766 | 1116 / 1159 | 14 / 14 | 16 / 25 | | `protocol.list-slot-prefer-local.test.ts` | 675 / 796 | 967 / 1052 | 15 / 17 | 12 / 14 | | `protocol.lock-door-read-agree.test.ts` | 810 / 814 | 823 / 872 | 45 / 47 | 40 / 58 | | `protocol.lock-one-resolution.shard-1.test.ts` | 747 / 830 | 1049 / 1060 | 26 / 35 | 24 / 34 | | `protocol.lock-one-resolution.shard-2.test.ts` | 725 / 759 | 1129 / 1194 | 21 / 29 | 24 / 32 | | `protocol.lock-one-resolution.shard-3.test.ts` | 718 / 736 | 1034 / 1070 | 20 / 27 | 24 / 26 | | `protocol.lock-one-resolution.shard-4.test.ts` | 681 / 718 | 1122 / 1200 | 20 / 32 | 28 / 34 | | `protocol.lock-one-resolution.test.ts` | 787 / 824 | 1049 / 1083 | 27 / 35 | 33 / 42 | | `protocol.lock-org-axis-agree.test.ts` | 676 / 775 | 1165 / 1241 | 29 / 29 | 41 / 51 | | `protocol.named-base-overlay-write.test.ts` | 685 / 820 | 922 / 952 | 23 / 25 | 38 / 44 | | `protocol.object-cascade-delete-objects.test.ts` | 668 / 694 | 978 / 999 | 3 / 3 | 3 / 4 | | `protocol.object-disabled-actions.test.ts` | 641 / 648 | 934 / 969 | 3 / 3 | 6 / 13 | | `protocol.object-registry-write-through-spelling.test.ts` | 735 / 838 | 1057 / 1143 | 32 / 33 | 39 / 52 | | `protocol.org-scoped-write-refused.test.ts` | 665 / 714 | 846 / 913 | 32 / 38 | 45 / 69 | | `protocol.package-publish-audit-rows.test.ts` | 683 / 684 | 1057 / 1134 | 11 / 11 | 16 / 23 | | `protocol.packageless-save-address.test.ts` | 681 / 777 | 946 / 1077 | 12 / 18 | 19 / 25 | | `protocol.publish-commit-capture-read-failure.test.ts` | 662 / 691 | 974 / 1054 | 46 / 48 | 69 / 82 | | `protocol.publish-side-effects-canonical-type.test.ts` | 682 / 705 | 987 / 1022 | 34 / 36 | 38 / 42 | | `protocol.publish-stored-type-canonical.test.ts` | 698 / 698 | 833 / 875 | 32 / 32 | 45 / 47 | | `protocol.reads-environment-only.test.ts` | 678 / 686 | 965 / 1051 | 25 / 26 | 27 / 36 | | `protocol.record-package-commit-durability.test.ts` | 673 / 738 | 955 / 1071 | 60 / 61 | 61 / 73 | | `protocol.save-union-issues.test.ts` | 654 / 723 | 850 / 891 | 9 / 9 | 9 / 12 | | `protocol.scoped-list-fallback.test.ts` | 736 / 741 | 853 / 977 | 10 / 11 | 17 / 20 | | `protocol.seed-external-id-refusal.test.ts` | 764 / 786 | 1053 / 1115 | 26 / 33 | 28 / 32 | | `protocol.served-content-hash.test.ts` | 763 / 770 | 1084 / 1130 | 22 / 24 | 19 / 24 | | `protocol.stored-conversions.test.ts` | 738 / 776 | 997 / 1166 | 11 / 11 | 11 / 18 | | `protocol.summary-rollup-publish-gate.test.ts` | 682 / 773 | 966 / 993 | 29 / 30 | 37 / 57 | | `protocol.unknown-datasource-refusal.test.ts` | 754 / 757 | 923 / 976 | 37 / 48 | 46 / 52 | | `protocol.unknown-grant-object-refusal.test.ts` | 672 / 725 | 818 / 888 | 11 / 15 | 11 / 12 | | `protocol.view-container-reset-expansions.test.ts` | 788 / 795 | 1135 / 1200 | 58 / 65 | 83 / 88 | | `view-container-runtime-expansion.test.ts` | 661 / 784 | 798 / 834 | 8 / 8 | 9 / 12 | **packages/objectql** (18 files), first lint-loading case, ms p50 / max | file | before idle | before 4x | after idle | after 4x | |---|---|---|---|---| | `build-probes.test.ts` | 707 / 741 | 1040 / 1416 | 11 / 12 | 15 / 16 | | `environment-overlay-five-types.test.ts` | 931 / 983 | 1135 / 1368 | 92 / 101 | 126 / 140 | | `package-disable-enforcement.test.ts` | 879 / 1101 | 1384 / 1489 | 162 / 162 | 223 / 225 | | `protocol-commit-history.test.ts` | 713 / 761 | 1192 / 1268 | 10 / 16 | 16 / 28 | | `protocol-custom-override-bare-slot-envelope.test.ts` | 676 / 724 | 1123 / 1222 | 33 / 40 | 55 / 66 | | `protocol-custom-override-effective-read.test.ts` | 777 / 927 | 1222 / 1490 | 126 / 134 | 153 / 167 | | `protocol-duplicate-package-names-in-step.test.ts` | 841 / 957 | 1713 / 2282 | 209 / 221 | 323 / 347 | | `protocol-lock-artifact-package-axis.test.ts` | 809 / 871 | 1100 / 1170 | 27 / 28 | 28 / 36 | | `protocol-meta.test.ts` | 711 / 798 | 1156 / 1288 | 25 / 26 | 38 / 43 | | `protocol-packaged-view-base.test.ts` | 702 / 756 | 1080 / 1121 | 23 / 32 | 25 / 27 | | `protocol-packageless-overlay-migration.test.ts` | 843 / 1139 | 1358 / 1420 | 130 / 169 | 167 / 224 | | `protocol-packageless-read-dependent-package.test.ts` | 839 / 875 | 1225 / 1239 | 119 / 124 | 148 / 187 | | `protocol-publish-package-drafts.test.ts` | 718 / 760 | 985 / 1058 | 16 / 22 | 35 / 47 | | `protocol-publish-rollback.test.ts` | 703 / 719 | 1107 / 1179 | 46 / 48 | 49 / 71 | | `protocol-revert-org-scope.test.ts` | 822 / 835 | 1246 / 1330 | 133 / 182 | 161 / 170 | | `publish-package-drafts-backfill.test.ts` | 853 / 889 | 1084 / 1462 | 208 / 231 | 231 / 255 | | `publish-package-drafts-response-conformance.test.ts` | 826 / 838 | 1052 / 1250 | 120 / 133 | 189 / 212 | | `publish-package-drafts-seed-receipt-counts.test.ts` | 885 / 973 | 1390 / 1500 | 176 / 187 | 201 / 303 | </details> <details><summary>The 106 grep candidates the trace dropped (0 lint-root evaluations inside a case)</summary> `packages/runtime/src/audit-meta-item-org-scope.integration.test.ts`, `packages/runtime/src/domains/meta-item-query-decision.test.ts`, `packages/runtime/src/domains/meta-save-actor.test.ts`, `packages/runtime/src/domains/meta-save-destructive-remedy.test.ts`, `packages/runtime/src/domains/packages-adopt-orphans-readonly.test.ts`, `packages/runtime/src/domains/packages-customizations.test.ts`, `packages/runtime/src/domains/packages-publish-drafts-request-conformance.test.ts`, `packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts`, `packages/runtime/src/http-dispatcher.test.ts`, `packages/runtime/src/meta-compound-arity-mint-door.test.ts`, `packages/runtime/src/meta-field-overlay-lock.test.ts`, `packages/runtime/src/meta-managed-content-seal.test.ts`, `packages/runtime/src/package-dependencies-patch.integration.test.ts`, `packages/runtime/src/package-duplicate-adopt-org-scope.integration.test.ts`, `packages/runtime/src/package-revert-code-shipped-members.integration.test.ts`, `packages/runtime/src/package-uninstall-org-scope.integration.test.ts`, `packages/runtime/src/package-uninstall-store-refusal.integration.test.ts`, `packages/runtime/src/stored-metadata-reader-seam.test.ts`, `packages/metadata-protocol/src/meta-overlay-cache.test.ts`, `packages/metadata-protocol/src/mutation-listeners.test.ts`, `packages/metadata-protocol/src/package-unmanaged-install.test.ts`, `packages/metadata-protocol/src/protocol-14078-audit-invalid-date-total-arm.test.ts`, `packages/metadata-protocol/src/protocol-publish-drafts-outcome.test.ts`, `packages/metadata-protocol/src/protocol.adr0005-org-override-rollback.test.ts`, `packages/metadata-protocol/src/protocol.audit-org-scope.test.ts`, `packages/metadata-protocol/src/protocol.audit-read-failure-propagation.test.ts`, `packages/metadata-protocol/src/protocol.batch-verb-driver-code.test.ts`, `packages/metadata-protocol/src/protocol.batch-verb-driver-text.test.ts`, `packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts`, `packages/metadata-protocol/src/protocol.code-only-types.test.ts`, `packages/metadata-protocol/src/protocol.data-door-stored-content-hash.test.ts`, `packages/metadata-protocol/src/protocol.data-door-stored-metadata-filter-reads.test.ts`, `packages/metadata-protocol/src/protocol.data-door-stored-metadata-redaction.test.ts`, `packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts`, `packages/metadata-protocol/src/protocol.delete-object-registry-unregister.test.ts`, `packages/metadata-protocol/src/protocol.delete-receipt-wording.test.ts`, `packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts`, `packages/metadata-protocol/src/protocol.diff-canonical-type-and-history-outage.test.ts`, `packages/metadata-protocol/src/protocol.diff-credential-redaction.test.ts`, `packages/metadata-protocol/src/protocol.diff-dead-history-read.test.ts`, `packages/metadata-protocol/src/protocol.driver-text-disclosure.test.ts`, `packages/metadata-protocol/src/protocol.flow-layered-shipped-name.test.ts`, `packages/metadata-protocol/src/protocol.flow-org-override-closed.test.ts`, `packages/metadata-protocol/src/protocol.flow-stored-row-shipped-name.test.ts`, `packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts`, `packages/metadata-protocol/src/protocol.hydrate-overlay-canonical-type.test.ts`, `packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts`, `packages/metadata-protocol/src/protocol.item-name-every-door.test.ts`, `packages/metadata-protocol/src/protocol.item-name-grammar.test.ts`, `packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts`, `packages/metadata-protocol/src/protocol.legacy-organization-read-seam.test.ts`, `packages/metadata-protocol/src/protocol.lifecycle-audit-rows.test.ts`, `packages/metadata-protocol/src/protocol.lock-gate-canonical-type.test.ts`, `packages/metadata-protocol/src/protocol.lock-gate-fail-closed.test.ts`, `packages/metadata-protocol/src/protocol.lock-one-resolution.kit.test.ts`, `packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts`, `packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts`, `packages/metadata-protocol/src/protocol.metadata-redaction.test.ts`, `packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts`, `packages/metadata-protocol/src/protocol.package-door-before-gates.test.ts`, `packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts`, `packages/metadata-protocol/src/protocol.platform-schedule-org-gate.test.ts`, `packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts`, `packages/metadata-protocol/src/protocol.publish-item-draft-org-scope.test.ts`, `packages/metadata-protocol/src/protocol.read-lock-flags-write-door.test.ts`, `packages/metadata-protocol/src/protocol.read-seam-empty-accumulator.test.ts`, `packages/metadata-protocol/src/protocol.read-verb-canonical-fold.test.ts`, `packages/metadata-protocol/src/protocol.recovery-doors-mutation-projector.test.ts`, `packages/metadata-protocol/src/protocol.reference-derivation.test.ts`, `packages/metadata-protocol/src/protocol.revert-stored-type-canonical.test.ts`, `packages/metadata-protocol/src/protocol.rollback-carries-item-failures.test.ts`, `packages/metadata-protocol/src/protocol.row-binding-column.test.ts`, `packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts`, `packages/metadata-protocol/src/protocol.save-meta-missing-item.test.ts`, `packages/metadata-protocol/src/protocol.save-receipt-wording.test.ts`, `packages/metadata-protocol/src/protocol.sealed-remedy-kind.test.ts`, `packages/metadata-protocol/src/protocol.stored-migration.test.ts`, `packages/metadata-protocol/src/protocol.stored-residue-resave.test.ts`, `packages/metadata-protocol/src/protocol.tenant-authored-write.test.ts`, `packages/metadata-protocol/src/protocol.ui-view-hidden-columns.test.ts`, `packages/metadata-protocol/src/protocol.ui-view-response-conformance.test.ts`, `packages/metadata-protocol/src/protocol.unrecognised-meta-type.test.ts`, `packages/metadata-protocol/src/sys-metadata-repository-14938-list-drafts-updated-at.test.ts`, `packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts`, `packages/objectql/src/delete-meta-response-conformance.test.ts`, `packages/objectql/src/environment-overlay-email-locale-restart.test.ts`, `packages/objectql/src/overlay-precedence.test.ts`, `packages/objectql/src/protocol-bundle-member-base-address.test.ts`, `packages/objectql/src/protocol-destructive.test.ts`, `packages/objectql/src/protocol-environment-kernel-registry-write-through.test.ts`, `packages/objectql/src/protocol-flow-custom-override.test.ts`, `packages/objectql/src/protocol-layered-get.test.ts`, `packages/objectql/src/protocol-lock-enforcement.test.ts`, `packages/objectql/src/protocol-meta-types-rich.test.ts`, `packages/objectql/src/protocol-object-overlay-layer.test.ts`, `packages/objectql/src/protocol-org-overlay-registry-gate.test.ts`, `packages/objectql/src/protocol-package-lifecycle.test.ts`, `packages/objectql/src/protocol-per-package-layer-provenance.test.ts`, `packages/objectql/src/protocol-publish-canonical-fold.test.ts`, `packages/objectql/src/protocol-recorded-by-null.test.ts`, `packages/objectql/src/protocol-references.test.ts`, `packages/objectql/src/protocol-save-meta-repo-path-real-engine.test.ts`, `packages/objectql/src/protocol-save-meta-repo-path.test.ts`, `packages/objectql/src/protocol-view-identity-overlay.test.ts`, `packages/objectql/src/publish-meta-response-conformance.test.ts`, `packages/objectql/src/save-meta-response-conformance.test.ts` </details> ### Why a testkit per file and not one setup file Measured first: a `setupFiles` entry in each package (commit `a89a1ccea2`, superseded) did warm every case, but it made every file in the package pay the lint load, including the many that never reach it. Whole-suite runs, setup plus import worker time and wall, against the traced baseline on the same base: | package | baseline setup+import / wall | with setupFiles setup+import / wall | |---|---|---| | metadata-protocol | 506 s / 207 s | 707 s / 279 s | | objectql | 772 s / 312 s | 1101 s / 433 s | | runtime | 1561 s / 608 s | 1758 s / 689 s | So the shared piece is the one module per package, and only the files that reach the load import it. Its header tells the author of a new file when to add the import. ## Acceptance notes - **Deviation, objectql `package.json` and `pnpm-lock.yaml`.** objectql does not depend on `@objectstack/lint`; metadata-protocol does, and loads it from its `dist/`. A static import of it from objectql code fails `tsc` with TS2882 (measured: `src/__fixtures__/lint-root-preload.ts(1,8): error TS2882: Cannot find module or type declarations for side-effect import of '@objectstack/lint'`), so the import needs the package declared. A devDependency is enough because the file sits under `__fixtures__`, which `check:undeclared-dep-imports` reads as test code (a `.testkit.ts` beside product source was red there). - **The objectql alias is forced**: removing it reds `check:test-source-alias` with `@objectstack/objectql: NEW unaliased artifact import(s) ... @objectstack/lint`. With it, the preload and metadata-protocol's own `import()` resolve to one source module. - **The object-publish path is included.** The card names the view read; the build probes load the same root on an object publish, and 23 of the 66 files reach it only that way. Same cost, same fix. - **PM assumption 1, partly held**: runtime's #29622 alias does make the cold load a source-graph transform, and it is about a third of the `dist/` cost, but it still sits in 9 runtime cases. **PM assumption 2, refuted**: metadata-protocol resolves the root to `dist/` (no alias; its `check:test-source-alias` row lists it). - **No gate arm.** `check:test-source-alias` is static, and this load is reached at run time through another package's code: whether a case's read serves a `view` record, or a publish includes an object, is data. A dependency-shaped arm (any package whose tests import the protocol must preload) would name about 40 packages beyond these three, most of them never reaching the load, and red untouched packages. The tracer above is the instrument that answers the question; it is a scratch tool and is not landed. - Not measured: packages outside runtime, metadata-protocol and objectql whose tests boot the real protocol. - No timeout, retry, skip or assertion changed. Forced test edits: none. ## Verification Head `c1fdec3921` (branch merged with `origin/main` `cba12d2218`; delta vs `main`: 73 files, +240 / -1). - `pnpm --filter @objectstack/metadata-protocol test`: `Test Files 310 passed | 4 skipped (314)`, `Tests 22292 passed | 26 skipped`, VERDICT command-exit 0. - `pnpm --filter @objectstack/objectql test`: `Test Files 544 passed (544)`, `Tests 10760 passed`; `test:repo`: `1 passed`, `5 passed`. Both exit 0. - `pnpm --filter @objectstack/runtime test`: `Test Files 533 passed (533)`, `Tests 8540 passed | 19 skipped`; `test:repo`: `3 passed`, `1211 passed`. Both exit 0. - `pnpm --filter PKG typecheck` for each of the three packages after a closure build with declarations: exit 0 each; `tsc --listFiles` includes each testkit. - eslint `--no-inline-config` over the 71 changed `.ts` files: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands` on this head: 67 commands (73 paths, 241 changed lines). All 67 ran on this head. 66 exited 0 on the first pass; `check:dual-build-cjs-loads` exited 3 (`PREREQUISITE NOT MET`, no `dist/` for 37 packages), then 0 after a full `pnpm build`. `--ran` reconciliation: `67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN`. Also run, outside the derived set: `check:authz-resolver`, `check:filter-alias-parity`, `check:route-ledger-census` (rosters under touched directories) and `check-nul-bytes`, each exit 0. - No ablation: the diff changes no tested behaviour. The before/after table is the reverse reading. - Dogfood: n/a (no `!`, no `(narrowing)` arm). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 97c2982 commit 6d86e4a

73 files changed

Lines changed: 240 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Import this at module top in a test file whose cases make a served `view`
4+
// read or an object publish, so the file pays the first load of the
5+
// `@objectstack/lint` package ROOT during collection, never inside a clocked
6+
// case. Test-only: no product module imports it.
7+
//
8+
// The protocol loads the lint root lazily, on first use, in two places: a
9+
// served `view` read decorates its `_diagnostics` with the form-layout
10+
// warnings (`loadFormLayoutLint`, metadata-diagnostics.ts), and an object
11+
// publish runs its build probes (`runBuildProbes`, build-probes.ts). The lazy
12+
// shape is product design: the kernel boot path never reaches the root.
13+
// Under vitest the first such load in a file is a cold transform of the lint
14+
// module graph, paid inside whichever case gets there first. A module-top
15+
// import is paid during collection, which neither `testTimeout` nor
16+
// `hookTimeout` clocks, and the protocol's later `import()` gets the same
17+
// module instance back (AGENTS.md § Build & Test, the clocked-window rule).
18+
//
19+
// Here the root resolves to `lint/dist/index.js`: this package's row in
20+
// `check:test-source-alias` lists `@objectstack/lint` as still read from
21+
// `dist/`.
22+
//
23+
// Who imports it: every test file in which a traced run of the whole suite
24+
// saw that first load inside a case. A new file whose cases reach a view read
25+
// or an object publish imports it too. It is not a `setupFiles` entry,
26+
// because then every test file in the package pays the load, including the
27+
// many that never reach it: measured, that added about 200 s of worker time
28+
// and about a third to the suite's wall time.
29+
import '@objectstack/lint';

‎packages/metadata-protocol/src/protocol-publish-drafts-closure.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
* `saveMetaItem` / `publishPackageDrafts` run.
5757
*/
5858

59+
// Pays the lint root's first load at collection, never inside a clocked case.
60+
import './lint-root-preload.testkit.js';
5961
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
6062
// The producer's OWN write-verb dispatch decisions, so the fake engine below
6163
// cannot accept a call ObjectQL refuses. From `@objectstack/metadata-core`,

‎packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
// Pays the lint root's first load at collection, never inside a clocked case.
4+
import './lint-root-preload.testkit.js';
35
import { describe, expect, it } from 'vitest';
46
// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete /
57
// #5480 update), so the fake engine below cannot accept a call ObjectQL

‎packages/metadata-protocol/src/protocol-publish-drafts-object-field-refs.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
* `publishPackageDrafts`; nothing on the gate path is stubbed.
4242
*/
4343

44+
// Pays the lint root's first load at collection, never inside a clocked case.
45+
import './lint-root-preload.testkit.js';
4446
import { describe, expect, it } from 'vitest';
4547
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate } from '@objectstack/metadata-core';
4648
import { ObjectStackProtocolImplementation } from './protocol.js';

‎packages/metadata-protocol/src/protocol-publish-drafts-org-scope.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
// Pays the lint root's first load at collection, never inside a clocked case.
4+
import './lint-root-preload.testkit.js';
35
import { describe, expect, it } from 'vitest';
46
// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete /
57
// #5480 update), so the fake engine below cannot accept a call ObjectQL

‎packages/metadata-protocol/src/protocol-publish-drafts-package-scope.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
// Pays the lint root's first load at collection, never inside a clocked case.
4+
import './lint-root-preload.testkit.js';
35
import { describe, expect, it } from 'vitest';
46
// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete /
57
// #5480 update), so the fake engine below cannot accept a call ObjectQL

‎packages/metadata-protocol/src/protocol.app-package-id-key.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
* `findOne` over `sys_metadata` rows, and a registry answering a
3030
* package-scoped lookup with that package's items only.
3131
*/
32+
// Pays the lint root's first load at collection, never inside a clocked case.
33+
import './lint-root-preload.testkit.js';
3234
import { afterEach, describe, expect, it, vi } from 'vitest';
3335
import { assertEngineFindOnePredicate, isCodeArtifactBody } from '@objectstack/metadata-core';
3436
import { ObjectStackProtocolImplementation } from './protocol.js';

‎packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
* shape). Nothing on the save, read or publish path is stubbed.
2929
*/
3030

31+
// Pays the lint root's first load at collection, never inside a clocked case.
32+
import './lint-root-preload.testkit.js';
3133
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3234
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate, hashSpec } from '@objectstack/metadata-core';
3335
import { ObjectStackProtocolImplementation } from './protocol.js';

‎packages/metadata-protocol/src/protocol.extension-field-suffix.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
* as a row stored before the rule (`residueField()`).
3131
*/
3232

33+
// Pays the lint root's first load at collection, never inside a clocked case.
34+
import './lint-root-preload.testkit.js';
3335
import { afterEach, describe, expect, it, vi } from 'vitest';
3436
// [#5619] The producer's OWN write-verb dispatch decisions, imported from
3537
// `@objectstack/metadata-core` and NOT from `@objectstack/objectql`: objectql

‎packages/metadata-protocol/src/protocol.flow-by-name-shipped-name.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
* caller naming an organization reads as the environment does, and a type in
2828
* the overlay regime keeps its overlay.
2929
*/
30+
// Pays the lint root's first load at collection, never inside a clocked case.
31+
import './lint-root-preload.testkit.js';
3032
import { describe, expect, it, vi } from 'vitest';
3133
import { assertEngineFindOnePredicate, isCodeArtifactBody } from '@objectstack/metadata-core';
3234
import type { FlowRegistration } from '@objectstack/spec/api';

0 commit comments

Comments
 (0)