arkgate 4.8.23 → 4.8.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +24 -77
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +596 -112
  6. package/bin/ark-mcp-runtime.mjs +412 -138
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +39 -0
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/agent-projection-command.mjs +5 -1
  13. package/bin/lib/analysis-engine.mjs +15 -11
  14. package/bin/lib/architecture-scan.mjs +50 -6
  15. package/bin/lib/ark-order-doctor.mjs +1 -1
  16. package/bin/lib/ark-order-facts.mjs +410 -15
  17. package/bin/lib/ark-order-invariants.mjs +51 -9
  18. package/bin/lib/ark-order-sensors.mjs +28 -101
  19. package/bin/lib/ark-order-types.mjs +2 -0
  20. package/bin/lib/ark-run-doctor.mjs +14 -0
  21. package/bin/lib/ark-run-facts.mjs +434 -11
  22. package/bin/lib/ark-run-sensors.mjs +5 -3
  23. package/bin/lib/arkgate-pins.mjs +91 -0
  24. package/bin/lib/arkrules-sensors.mjs +50 -249
  25. package/bin/lib/check-args.mjs +23 -1
  26. package/bin/lib/ci-and-commands.mjs +16 -6
  27. package/bin/lib/class-source-scan.mjs +479 -0
  28. package/bin/lib/codex-home.mjs +4 -2
  29. package/bin/lib/config-contract-slices.mjs +701 -0
  30. package/bin/lib/config-contract.mjs +5 -493
  31. package/bin/lib/config-warnings.mjs +32 -0
  32. package/bin/lib/dashboard-cli.mjs +62 -0
  33. package/bin/lib/design-delta.mjs +1 -1
  34. package/bin/lib/diagnostic-catalog.mjs +16 -9
  35. package/bin/lib/doctor-advisories.mjs +25 -1
  36. package/bin/lib/doctor-next-actions.mjs +17 -2
  37. package/bin/lib/doctor-plan.mjs +6 -8
  38. package/bin/lib/effective-contract-load.mjs +121 -59
  39. package/bin/lib/enforcement-profiles.mjs +40 -4
  40. package/bin/lib/first-run-help.mjs +3 -2
  41. package/bin/lib/flat-parent-importers.mjs +137 -0
  42. package/bin/lib/flat-parent-pilot.mjs +40 -100
  43. package/bin/lib/gate-files.mjs +29 -2
  44. package/bin/lib/git-change-scope.mjs +109 -0
  45. package/bin/lib/github-enforcement.mjs +1 -1
  46. package/bin/lib/hook-templates.mjs +18 -16
  47. package/bin/lib/host-support-matrix.mjs +3 -1
  48. package/bin/lib/import-resolve.mjs +13 -1
  49. package/bin/lib/install-migrate.mjs +50 -73
  50. package/bin/lib/invariant-coverage.mjs +59 -11
  51. package/bin/lib/mcp-adoption.mjs +9 -1
  52. package/bin/lib/mcp-hook-payload.mjs +401 -61
  53. package/bin/lib/package-manager.mjs +59 -1
  54. package/bin/lib/passthrough-commands.mjs +83 -0
  55. package/bin/lib/persistence-write-hint.mjs +81 -0
  56. package/bin/lib/pin-dependent-gates.mjs +117 -0
  57. package/bin/lib/policy-delta-io.mjs +168 -10
  58. package/bin/lib/prepare-change.mjs +120 -6
  59. package/bin/lib/prepare-write.mjs +1 -0
  60. package/bin/lib/presets.mjs +3 -5
  61. package/bin/lib/remediation.mjs +23 -4
  62. package/bin/lib/resolved-candidate-facts.mjs +315 -55
  63. package/bin/lib/rules-inventory.mjs +22 -3
  64. package/bin/lib/rules-under-contract.mjs +179 -5
  65. package/bin/lib/scan-files.mjs +32 -0
  66. package/bin/lib/snippet-analysis.mjs +140 -6
  67. package/bin/lib/source-policy.mjs +34 -0
  68. package/bin/lib/stable-hash.mjs +90 -0
  69. package/bin/lib/start-install-recovery.mjs +71 -1
  70. package/bin/lib/start-preview.mjs +30 -0
  71. package/bin/lib/status-command.mjs +33 -4
  72. package/bin/lib/status-manifest.mjs +32 -4
  73. package/bin/lib/team-parliament-io.mjs +77 -75
  74. package/bin/lib/team-parliament.mjs +6 -1
  75. package/bin/lib/write-path-capabilities.mjs +4 -2
  76. package/dist/chunk-A5EIY3HI.cjs +5 -0
  77. package/dist/chunk-MBXCFLO4.js +16 -0
  78. package/dist/chunk-RNQHRDCQ.cjs +16 -0
  79. package/dist/chunk-SVSQVGFJ.js +5 -0
  80. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-BJbaeoj4.d.cts} +18 -3
  81. package/dist/configTypes-BJbaeoj4.d.ts +246 -0
  82. package/dist/eslint/index.cjs +17 -4
  83. package/dist/eslint/index.d.cts +359 -0
  84. package/dist/eslint/index.d.ts +130 -156
  85. package/dist/eslint/index.js +10 -4
  86. package/dist/index.cjs +35 -34
  87. package/dist/index.d.cts +5475 -0
  88. package/dist/index.d.ts +3312 -299
  89. package/dist/index.js +35 -34
  90. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  91. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  92. package/dist/nestjs/index.cjs +1 -18
  93. package/dist/nestjs/index.d.cts +22 -0
  94. package/dist/nestjs/index.d.ts +1 -3
  95. package/dist/nestjs/index.js +1 -18
  96. package/dist/order/index.cjs +1 -1
  97. package/dist/order/index.d.cts +155 -0
  98. package/dist/order/index.d.ts +15 -3
  99. package/dist/order/index.js +1 -1
  100. package/dist/runtime/index.cjs +13 -30
  101. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +804 -290
  102. package/dist/runtime/index.d.ts +2678 -108
  103. package/dist/runtime/index.js +13 -30
  104. package/dist/{types-Bdj32QwA.d.ts → types-DRinG-vT.d.cts} +576 -140
  105. package/dist/types-DRinG-vT.d.ts +1792 -0
  106. package/docs/README.md +2 -1
  107. package/docs/agent-guide.md +84 -15
  108. package/docs/ai-gates.md +160 -26
  109. package/docs/arkorder.md +32 -15
  110. package/docs/brownfield-adoption.md +5 -1
  111. package/docs/configuration.md +196 -26
  112. package/docs/develop.md +2 -1
  113. package/docs/diagnostics.md +96 -19
  114. package/docs/package-surface.md +31 -8
  115. package/docs/product-voice.md +1 -1
  116. package/package.json +59 -18
  117. package/schemas/ark.analysis-result.schema.json +15 -0
  118. package/schemas/ark.config.schema.json +47 -10
  119. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  120. package/schemas/ark.status-manifest.schema.json +22 -0
  121. package/server.json +3 -9
  122. package/templates/agent-skills/README.md +1 -1
  123. package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
  124. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  125. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  126. package/templates/arkrules/PersistenceAdapters.json +1 -1
  127. package/templates/arkrules/PresentationAdapters.json +1 -1
  128. package/templates/skills/ark-adopt.md +4 -1
  129. package/templates/skills/ark-order.md +4 -2
  130. package/templates/skills/ark-runtime.md +6 -1
  131. package/dist/types-Ik_OakcI.d.ts +0 -183
@@ -36,16 +36,21 @@ import {
36
36
  import { provePortProofInject } from './port-proof.mjs';
37
37
  import { extractClassShapesFromSource } from './arkrules-sensors.mjs';
38
38
  import {
39
+ arkRunEffectiveIntentPrefixes,
40
+ createArkRunKernelRootSpecifierMatcher,
39
41
  extractArkRunDeclarationsFromSource,
42
+ reexportsArkRunKernelRoot,
40
43
  extractArkRunKernelCallsFromSource,
41
44
  extractArkRunManagedNewsFromSource,
42
45
  } from './ark-run-facts.mjs';
43
46
  import {
47
+ extractArkOrderBudgetLeaksFromSource,
44
48
  extractArkOrderGenericUpdatesFromSource,
45
49
  extractArkOrderIngestWritesXiFromSource,
46
50
  extractArkOrderPlaneCallsFromSource,
47
51
  extractArkOrderReleaseKeyCountsFromSource,
48
52
  extractArkOrderXiFieldWritesFromSource,
53
+ extractArkOrderXiTtlKeysFromSource,
49
54
  } from './ark-order-facts.mjs';
50
55
  import {
51
56
  collectGovernedFiles,
@@ -89,6 +94,17 @@ function canonicalProjectPath(value) {
89
94
  return normalized;
90
95
  }
91
96
 
97
+ /**
98
+ * `path.resolve` that reuses an equal string instead of allocating a copy, so
99
+ * one file's absolute/real path is one string across the resolver's maps.
100
+ * `sameAs` lets a real path that equals the lexical path share that instance.
101
+ */
102
+ function resolvedPath(value, sameAs) {
103
+ const resolved = path.resolve(value);
104
+ if (sameAs !== undefined && resolved === sameAs) return sameAs;
105
+ return resolved === value ? value : resolved;
106
+ }
107
+
92
108
  function isInsideRoot(root, target) {
93
109
  const relative = path.relative(root, target);
94
110
  return (
@@ -129,13 +145,13 @@ function readPackageName(root, observeInput) {
129
145
  function rememberCanonicalAlias(aliases, real, relative, absolute) {
130
146
  const current = aliases.get(real);
131
147
  if (!current || relative < current.relative) {
132
- aliases.set(real, { relative, absolute: path.resolve(absolute) });
148
+ aliases.set(real, { relative, absolute: resolvedPath(absolute) });
133
149
  }
134
150
  }
135
151
 
136
152
  function rememberDirectoryAlias(aliases, real, relative, absolute) {
137
153
  const current = aliases.get(real) ?? new Map();
138
- if (!current.has(relative)) current.set(relative, { relative, absolute: path.resolve(absolute) });
154
+ if (!current.has(relative)) current.set(relative, { relative, absolute: resolvedPath(absolute) });
139
155
  aliases.set(real, current);
140
156
  }
141
157
 
@@ -244,10 +260,20 @@ function discoverScopedFiles(root, config, scopeRelatives, observeInput) {
244
260
  );
245
261
  }
246
262
 
247
- function collectCandidateFiles(root, config, changes, observeInput, scopeRelatives) {
263
+ function collectCandidateFiles(
264
+ root,
265
+ config,
266
+ changes,
267
+ observeInput,
268
+ scopeRelatives,
269
+ { readContents = true } = {}
270
+ ) {
248
271
  const files = new Map();
249
272
  const directoryAliases = new Map();
250
273
  const expandedAliasDirectories = new Set();
274
+ // The walk already resolved each file's real path; reuse it instead of a
275
+ // second realpath per file.
276
+ const walkedRealpaths = new Map();
251
277
  const discovered = Array.isArray(scopeRelatives)
252
278
  ? discoverScopedFiles(root, config, scopeRelatives, observeInput)
253
279
  : (config.include ?? [])
@@ -259,13 +285,16 @@ function collectCandidateFiles(root, config, changes, observeInput, scopeRelativ
259
285
  if (relative === '.') relative = '';
260
286
  rememberDirectoryAlias(directoryAliases, real, relative, absolute);
261
287
  },
288
+ onFile(absolute, real) {
289
+ walkedRealpaths.set(absolute, real);
290
+ },
262
291
  })
263
292
  )
264
293
  .map((absolute) => {
265
294
  observeResolvedInput(observeInput, absolute, 'realpath');
266
295
  return {
267
296
  absolute,
268
- real: fs.realpathSync(absolute),
297
+ real: walkedRealpaths.get(absolute) ?? fs.realpathSync(absolute),
269
298
  relative: canonicalProjectPath(normalize(path.relative(root, absolute))),
270
299
  };
271
300
  })
@@ -306,12 +335,14 @@ function collectCandidateFiles(root, config, changes, observeInput, scopeRelativ
306
335
  }
307
336
  }
308
337
  for (const { absolute, real, relative } of canonicalByRealpath.values()) {
309
- observeResolvedInput(observeInput, absolute, 'source');
338
+ // Path-only callers (overlay canonicalization, tsconfig closure) never read
339
+ // source text: do not load every file's contents for them.
340
+ if (readContents) observeResolvedInput(observeInput, absolute, 'source');
310
341
  files.set(relative, {
311
342
  path: relative,
312
- absolute: path.resolve(absolute),
313
- real: path.resolve(real),
314
- content: fs.readFileSync(absolute, 'utf8'),
343
+ absolute: resolvedPath(absolute),
344
+ real: resolvedPath(real, absolute),
345
+ ...(readContents ? { content: fs.readFileSync(absolute, 'utf8') } : {}),
315
346
  });
316
347
  }
317
348
 
@@ -364,7 +395,9 @@ function collectCandidateFiles(root, config, changes, observeInput, scopeRelativ
364
395
  /** Canonicalize a Tooling overlay without exposing filesystem identity to Kernel. */
365
396
  export function canonicalizeCandidateChanges({ root, config, changes = [] }) {
366
397
  const canonicalRoot = fs.realpathSync(root);
367
- return collectCandidateFiles(canonicalRoot, config, changes).changes.map((change) =>
398
+ return collectCandidateFiles(canonicalRoot, config, changes, undefined, undefined, {
399
+ readContents: false,
400
+ }).changes.map((change) =>
368
401
  change.delete === true
369
402
  ? { path: change.path, delete: true }
370
403
  : { path: change.path, content: change.content }
@@ -390,13 +423,17 @@ function createOverlayModuleHost(ts, root, files, changes, observeInput) {
390
423
  const rememberVirtualDirectories = (absolute) => {
391
424
  let directory = path.dirname(absolute);
392
425
  while (isInsideRoot(root, directory)) {
393
- virtualDirectories.add(path.resolve(directory));
394
- if (path.resolve(directory) === root) break;
426
+ const resolved = path.resolve(directory);
427
+ // Every walk records the whole chain up to root, so a known directory
428
+ // already has all of its ancestors: stop instead of re-walking to root.
429
+ if (virtualDirectories.has(resolved) && resolved !== root) break;
430
+ virtualDirectories.add(resolved);
431
+ if (resolved === root) break;
395
432
  directory = path.dirname(directory);
396
433
  }
397
434
  };
398
435
  const remember = (absolute, file) => {
399
- const key = path.resolve(absolute);
436
+ const key = resolvedPath(absolute);
400
437
  byAbsolute.set(key, file);
401
438
  pathByAbsolute.set(key, file.path);
402
439
  rememberVirtualDirectories(key);
@@ -442,7 +479,9 @@ function createOverlayModuleHost(ts, root, files, changes, observeInput) {
442
479
  observeResolvedInput(observeInput, absolute, 'module-read');
443
480
  if (deleted.has(absolute)) return undefined;
444
481
  const candidate = byAbsolute.get(absolute);
445
- if (candidate) return candidate.content;
482
+ // Overlay candidates carry their proposed text. On-disk candidates are read
483
+ // lazily (their text is not held for the whole resolution).
484
+ if (typeof candidate?.content === 'string') return candidate.content;
446
485
  if (sys?.readFile) return sys.readFile(fileName);
447
486
  try {
448
487
  return fs.readFileSync(fileName, 'utf8');
@@ -495,6 +534,13 @@ function createOverlayModuleHost(ts, root, files, changes, observeInput) {
495
534
  };
496
535
 
497
536
  return {
537
+ /** Drop the per-file lookup tables once module resolution is finished. */
538
+ dispose() {
539
+ byAbsolute.clear();
540
+ pathByAbsolute.clear();
541
+ deleted.clear();
542
+ virtualDirectories.clear();
543
+ },
498
544
  fileExists,
499
545
  readFile,
500
546
  directoryExists,
@@ -597,7 +643,7 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
597
643
  const configByFile = new Map();
598
644
  const nearestConfigByDirectory = new Map();
599
645
  if (explicitPath) {
600
- for (const file of candidateFiles) configByFile.set(path.resolve(file.absolute), explicitPath);
646
+ for (const file of candidateFiles) configByFile.set(resolvedPath(file.absolute), explicitPath);
601
647
  } else if (!explicitPath) {
602
648
  for (const file of candidateFiles) {
603
649
  const nearest = nearestTsconfig(
@@ -606,7 +652,7 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
606
652
  nearestConfigByDirectory,
607
653
  observeInput
608
654
  );
609
- if (nearest) configByFile.set(path.resolve(file.absolute), nearest);
655
+ if (nearest) configByFile.set(resolvedPath(file.absolute), nearest);
610
656
  }
611
657
  }
612
658
  const configPaths = [...new Set(configByFile.values())].sort((left, right) => {
@@ -678,7 +724,7 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
678
724
  };
679
725
  for (const configPath of configPaths) loadConfigOptions(configPath);
680
726
 
681
- const optionsFor = (fileName) => {
727
+ const configPathFor = (fileName) => {
682
728
  const resolved = path.resolve(fileName);
683
729
  let configPath = configByFile.get(resolved);
684
730
  if (!configPath) {
@@ -686,6 +732,10 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
686
732
  ?? nearestTsconfig(root, resolved, nearestConfigByDirectory, observeInput);
687
733
  if (configPath) configByFile.set(resolved, configPath);
688
734
  }
735
+ return configPath;
736
+ };
737
+ const optionsFor = (fileName) => {
738
+ const configPath = configPathFor(fileName);
689
739
  return configPath ? loadConfigOptions(configPath) ?? {} : {};
690
740
  };
691
741
  const configs = configPaths.map((configPath) => ({
@@ -713,6 +763,8 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
713
763
  .sort();
714
764
  return {
715
765
  optionsFor,
766
+ /** Options are a function of the governing tsconfig path ('' = none). */
767
+ optionsKeyFor: (fileName) => configPathFor(fileName) ?? '',
716
768
  reasons,
717
769
  configInputPaths,
718
770
  tsconfigHash: deterministicHash(stableSerialize(configClosure)),
@@ -734,7 +786,9 @@ export function resolvedCompilerInputPaths({
734
786
  if (!ts?.readConfigFile || !ts?.parseJsonConfigFileContent) return [];
735
787
  observeResolvedInput(observeInput, root, 'realpath');
736
788
  const canonicalRoot = fs.realpathSync(root);
737
- const candidate = collectCandidateFiles(canonicalRoot, config, changes, observeInput);
789
+ const candidate = collectCandidateFiles(canonicalRoot, config, changes, observeInput, undefined, {
790
+ readContents: false,
791
+ });
738
792
  return compilerContext(ts, canonicalRoot, tsconfig, candidate.files, observeInput)
739
793
  .configInputPaths;
740
794
  }
@@ -1081,6 +1135,30 @@ function unavailableFacts(config, ts, reason) {
1081
1135
  });
1082
1136
  }
1083
1137
 
1138
+ const SOURCE_READ_FAILURE = Symbol('ark.sourceReadFailure');
1139
+
1140
+ /**
1141
+ * Share per-file parse results between resolutions of one config (a base tree
1142
+ * and its overlay candidate). Pass the same object to both calls; it is only
1143
+ * used while `config` is the identical object.
1144
+ */
1145
+ export function createResolverIngestCache(config) {
1146
+ return { config, records: new Map() };
1147
+ }
1148
+
1149
+ /** Overlay text when present; otherwise read the on-disk source now (not held afterwards). */
1150
+ function readCandidateSource(candidate, observeInput) {
1151
+ if (typeof candidate.content === 'string') return candidate.content;
1152
+ observeResolvedInput(observeInput, candidate.absolute, 'source');
1153
+ try {
1154
+ return fs.readFileSync(candidate.absolute, 'utf8');
1155
+ } catch (error) {
1156
+ const failure = error instanceof Error ? error : new Error(String(error));
1157
+ failure[SOURCE_READ_FAILURE] = true;
1158
+ throw failure;
1159
+ }
1160
+ }
1161
+
1084
1162
  function loadOneCandidateFile(root, config, relative, observeInput) {
1085
1163
  const discovered = discoverScopedFiles(root, config, [relative], observeInput);
1086
1164
  const found = discovered[0];
@@ -1088,8 +1166,8 @@ function loadOneCandidateFile(root, config, relative, observeInput) {
1088
1166
  observeResolvedInput(observeInput, found.absolute, 'source');
1089
1167
  return {
1090
1168
  path: found.relative,
1091
- absolute: path.resolve(found.absolute),
1092
- real: path.resolve(found.real),
1169
+ absolute: resolvedPath(found.absolute),
1170
+ real: resolvedPath(found.real, found.absolute),
1093
1171
  content: fs.readFileSync(found.absolute, 'utf8'),
1094
1172
  };
1095
1173
  }
@@ -1103,6 +1181,7 @@ export function resolveCandidateFacts({
1103
1181
  changes = [],
1104
1182
  observeInput,
1105
1183
  scopeFiles,
1184
+ ingestCache,
1106
1185
  }) {
1107
1186
  if (!ts?.createSourceFile || !ts?.resolveModuleName) {
1108
1187
  return unavailableFacts(config, ts, 'No API-compatible TypeScript resolver is available.');
@@ -1116,12 +1195,15 @@ export function resolveCandidateFacts({
1116
1195
  try {
1117
1196
  observeResolvedInput(observeInput, root, 'realpath');
1118
1197
  canonicalRoot = fs.realpathSync(root);
1198
+ // Source text is read one file at a time in `ingest` and released after it,
1199
+ // instead of holding every file's text for the whole resolution.
1119
1200
  const candidate = collectCandidateFiles(
1120
1201
  canonicalRoot,
1121
1202
  config,
1122
1203
  changes,
1123
1204
  observeInput,
1124
- scoped ? scopeFiles : undefined
1205
+ scoped ? scopeFiles : undefined,
1206
+ { readContents: false }
1125
1207
  );
1126
1208
  candidateFiles = candidate.files;
1127
1209
  canonicalChanges = candidate.changes;
@@ -1141,6 +1223,28 @@ export function resolveCandidateFacts({
1141
1223
  canonicalChanges,
1142
1224
  observeInput
1143
1225
  );
1226
+ // TypeScript resolves a specifier from its containing directory (its own
1227
+ // module cache is keyed the same way), and this run's host never changes.
1228
+ // Resolve each (tsconfig, directory, specifier) once instead of once per
1229
+ // importing file: a shared `react` or `@/lib/x` no longer re-walks every
1230
+ // ancestor node_modules for each file. Only the small verdict is kept.
1231
+ const resolutionMemo = new Map();
1232
+ const resolveOnce = (dependency, containingFile) => {
1233
+ if (!dependency.specifier) return resolveDependency(ts, dependency, containingFile, {}, host);
1234
+ const key = `${compiler.optionsKeyFor(containingFile)}\0${path.dirname(containingFile)}\0${dependency.specifier}`;
1235
+ let resolved = resolutionMemo.get(key);
1236
+ if (!resolved) {
1237
+ resolved = resolveDependency(
1238
+ ts,
1239
+ dependency,
1240
+ containingFile,
1241
+ compiler.optionsFor(containingFile),
1242
+ host
1243
+ );
1244
+ resolutionMemo.set(key, resolved);
1245
+ }
1246
+ return resolved;
1247
+ };
1144
1248
  const forbiddenGlobals = [
1145
1249
  ...new Set([
1146
1250
  ...AMBIENT_CAPABILITY_ENTRIES,
@@ -1168,6 +1272,8 @@ export function resolveCandidateFacts({
1168
1272
  const arkOrderXiFieldWrites = [];
1169
1273
  const arkOrderIngestWritesXi = [];
1170
1274
  const arkOrderReleaseKeyCounts = [];
1275
+ const arkOrderXiTtlKeys = [];
1276
+ const arkOrderBudgetLeaks = [];
1171
1277
  const arkRunActive = config.arkRun != null && typeof config.arkRun === 'object';
1172
1278
  const arkOrderActive = config.arkOrder != null && typeof config.arkOrder === 'object';
1173
1279
  const arkRulesActive =
@@ -1178,13 +1284,21 @@ export function resolveCandidateFacts({
1178
1284
  const compositionRootPatterns = arkRunActive
1179
1285
  ? [...(config.arkRun?.compositionRoots ?? [])]
1180
1286
  : [];
1287
+ // Identifiers imported from a kernel root module are traced kernel receivers.
1288
+ const kernelRootPatterns = arkRunActive
1289
+ ? [...(config.arkRun?.kernelRoots ?? config.arkRun?.compositionRoots ?? [])]
1290
+ : [];
1181
1291
  const planeRootPatterns = arkOrderActive ? [...(config.arkOrder?.planeRoots ?? [])] : [];
1182
1292
 
1183
1293
  const seedPathSet = new Set(candidateFiles.map((file) => file.path));
1184
- const ingest = (candidate, fullExtract) => {
1294
+ const ingestFresh = (candidate, fullExtract, knownContent) => {
1295
+ const content = knownContent ?? readCandidateSource(candidate, observeInput);
1296
+ // Keep the text only when a later pass re-reads it (ArkRun kernel calls, kernel-root
1297
+ // barrels and managed-new facts).
1298
+ if (arkRunActive && candidate.content === undefined) candidate.content = content;
1185
1299
  const sourceFile = ts.createSourceFile(
1186
1300
  candidate.absolute,
1187
- candidate.content,
1301
+ content,
1188
1302
  ts.ScriptTarget.Latest,
1189
1303
  true,
1190
1304
  ts.getScriptKindFromFileName?.(candidate.absolute)
@@ -1199,10 +1313,10 @@ export function resolveCandidateFacts({
1199
1313
  namedBindings: namedModuleBindings(ts, node),
1200
1314
  }));
1201
1315
  let portProofEligible = false;
1202
- if (/\bimport\s*\{[^}]+\}\s*from\s*['"]\.\.?\//.test(candidate.content)) {
1316
+ if (/\bimport\s*\{[^}]+\}\s*from\s*['"]\.\.?\//.test(content)) {
1203
1317
  try {
1204
1318
  portProofEligible = Boolean(
1205
- provePortProofInject(ts, candidate.content, { filePath: candidate.absolute, sourceFile })
1319
+ provePortProofInject(ts, content, { filePath: candidate.absolute, sourceFile })
1206
1320
  .eligible
1207
1321
  );
1208
1322
  } catch {
@@ -1219,7 +1333,7 @@ export function resolveCandidateFacts({
1219
1333
  });
1220
1334
  files.push({
1221
1335
  path: candidate.path,
1222
- contentHash: deterministicHash(candidate.content),
1336
+ contentHash: deterministicHash(content),
1223
1337
  parseStatus: parseDiagnosticCount === 0 ? 'parsed' : 'invalid',
1224
1338
  parseDiagnosticCount,
1225
1339
  exportsOnlyTypes,
@@ -1260,7 +1374,7 @@ export function resolveCandidateFacts({
1260
1374
  publishCalls.push(...policy.publishCalls);
1261
1375
  intentReferences.push(...policy.intentReferences);
1262
1376
  safetyUses.push(
1263
- ...collectSafetyUses(ts, sourceFile, candidate.path, candidate.content, semanticDependencies)
1377
+ ...collectSafetyUses(ts, sourceFile, candidate.path, content, semanticDependencies)
1264
1378
  );
1265
1379
  // Class-shape extraction is text-conservative (false negatives over false positives).
1266
1380
  // Only TS/TSX candidates; sensors consume the same shape via facts.classShapes.
@@ -1268,22 +1382,17 @@ export function resolveCandidateFacts({
1268
1382
  if (/\.(tsx?|mts|cts)$/i.test(candidate.path)) {
1269
1383
  if (arkRulesActive || arkRunActive) {
1270
1384
  try {
1271
- classShapes.push(...extractClassShapesFromSource(candidate.path, candidate.content));
1385
+ classShapes.push(...extractClassShapesFromSource(candidate.path, content));
1272
1386
  } catch {
1273
1387
  // Never fail the resolver for shape extraction; sensors stay silent on this file.
1274
1388
  }
1275
1389
  }
1276
1390
  if (arkRunActive) {
1277
- try {
1278
- arkRunKernelCalls.push(
1279
- ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
1280
- );
1281
- } catch {
1282
- // Never fail the resolver for ArkRun call extraction.
1283
- }
1391
+ // Kernel calls are extracted after every file is parsed, so imports of a
1392
+ // kernel root resolve through tsconfig paths and barrel re-exports.
1284
1393
  try {
1285
1394
  arkRunDeclarations.push(
1286
- ...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
1395
+ ...extractArkRunDeclarationsFromSource(candidate.path, content)
1287
1396
  );
1288
1397
  } catch {
1289
1398
  // Never fail the resolver for ArkRun declaration extraction.
@@ -1292,44 +1401,125 @@ export function resolveCandidateFacts({
1292
1401
  if (arkOrderActive) {
1293
1402
  try {
1294
1403
  arkOrderPlaneCalls.push(
1295
- ...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
1404
+ ...extractArkOrderPlaneCallsFromSource(candidate.path, content)
1296
1405
  );
1297
1406
  } catch {
1298
1407
  // Never fail the resolver for ArkOrder factory extraction.
1299
1408
  }
1300
1409
  try {
1301
1410
  arkOrderGenericUpdates.push(
1302
- ...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
1411
+ ...extractArkOrderGenericUpdatesFromSource(candidate.path, content, {
1412
+ planeRoots: planeRootPatterns,
1413
+ })
1303
1414
  );
1304
1415
  } catch {
1305
1416
  // Never fail the resolver for ArkOrder generic-update extraction.
1306
1417
  }
1307
1418
  try {
1308
1419
  arkOrderXiFieldWrites.push(
1309
- ...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
1420
+ ...extractArkOrderXiFieldWritesFromSource(candidate.path, content, xiKeys)
1310
1421
  );
1311
1422
  } catch {
1312
1423
  // Never fail the resolver for ArkOrder xi-field-write extraction.
1313
1424
  }
1314
1425
  try {
1315
1426
  arkOrderIngestWritesXi.push(
1316
- ...extractArkOrderIngestWritesXiFromSource(candidate.path, candidate.content)
1427
+ ...extractArkOrderIngestWritesXiFromSource(candidate.path, content)
1317
1428
  );
1318
1429
  } catch {
1319
1430
  // Never fail the resolver for ArkOrder ingest-writes-ξ extraction.
1320
1431
  }
1321
1432
  try {
1322
1433
  arkOrderReleaseKeyCounts.push(
1323
- ...extractArkOrderReleaseKeyCountsFromSource(candidate.path, candidate.content)
1434
+ ...extractArkOrderReleaseKeyCountsFromSource(candidate.path, content)
1324
1435
  );
1325
1436
  } catch {
1326
1437
  // Never fail the resolver for ArkOrder release key-count extraction.
1327
1438
  }
1439
+ try {
1440
+ arkOrderXiTtlKeys.push(
1441
+ ...extractArkOrderXiTtlKeysFromSource(candidate.path, content, {
1442
+ planeRoots: planeRootPatterns,
1443
+ })
1444
+ );
1445
+ } catch {
1446
+ // Never fail the resolver for ArkOrder ξ-TTL extraction.
1447
+ }
1448
+ try {
1449
+ arkOrderBudgetLeaks.push(
1450
+ ...extractArkOrderBudgetLeaksFromSource(candidate.path, content)
1451
+ );
1452
+ } catch {
1453
+ // Never fail the resolver for ArkOrder information-budget extraction.
1454
+ }
1455
+ }
1456
+ }
1457
+ };
1458
+
1459
+ // Per-file facts depend only on (config, path, text). A base/candidate pair
1460
+ // (atomic preflight) shares one cache: every file the overlay does not touch
1461
+ // is parsed once, not twice. Overlay files are never cached or reused.
1462
+ const accumulators = {
1463
+ completenessReasons,
1464
+ files,
1465
+ capabilityUses,
1466
+ ambientUses,
1467
+ publishCalls,
1468
+ intentReferences,
1469
+ safetyUses,
1470
+ classShapes,
1471
+ arkRunDeclarations,
1472
+ arkOrderPlaneCalls,
1473
+ arkOrderGenericUpdates,
1474
+ arkOrderXiFieldWrites,
1475
+ arkOrderIngestWritesXi,
1476
+ arkOrderReleaseKeyCounts,
1477
+ arkOrderXiTtlKeys,
1478
+ arkOrderBudgetLeaks,
1479
+ };
1480
+ const accumulatorNames = Object.keys(accumulators);
1481
+ const reusable = ingestCache?.config === config ? ingestCache.records : undefined;
1482
+ const ingest = (candidate, fullExtract) => {
1483
+ if (!reusable || !fullExtract || typeof candidate.content === 'string') {
1484
+ ingestFresh(candidate, fullExtract);
1485
+ return;
1486
+ }
1487
+ const content = readCandidateSource(candidate, observeInput);
1488
+ const contentHash = deterministicHash(content);
1489
+ const cached = reusable.get(candidate.path);
1490
+ if (cached && cached.absolute === candidate.absolute && cached.contentHash === contentHash) {
1491
+ if (arkRunActive) candidate.content = content;
1492
+ parsed.set(candidate.path, { ...cached.parsed, candidate });
1493
+ for (const name of accumulatorNames) {
1494
+ const slice = cached.slices[name];
1495
+ if (slice) accumulators[name].push(...slice);
1328
1496
  }
1497
+ return;
1329
1498
  }
1499
+ const before = accumulatorNames.map((name) => accumulators[name].length);
1500
+ ingestFresh(candidate, fullExtract, content);
1501
+ const slices = {};
1502
+ accumulatorNames.forEach((name, index) => {
1503
+ if (accumulators[name].length > before[index]) {
1504
+ slices[name] = accumulators[name].slice(before[index]);
1505
+ }
1506
+ });
1507
+ const { candidate: _candidate, ...parsedFacts } = parsed.get(candidate.path);
1508
+ reusable.set(candidate.path, {
1509
+ absolute: candidate.absolute,
1510
+ contentHash,
1511
+ parsed: parsedFacts,
1512
+ slices,
1513
+ });
1330
1514
  };
1331
1515
 
1332
- for (const candidate of candidateFiles) ingest(candidate, true);
1516
+ try {
1517
+ for (const candidate of candidateFiles) ingest(candidate, true);
1518
+ } catch (error) {
1519
+ // Same outcome as the former eager read inside the discovery try-block.
1520
+ if (error?.[SOURCE_READ_FAILURE] !== true) throw error;
1521
+ return unavailableFacts(config, ts, error.message);
1522
+ }
1333
1523
 
1334
1524
  if (scoped) {
1335
1525
  const loaded = new Set(candidateFiles.map((file) => file.path));
@@ -1340,13 +1530,7 @@ export function resolveCandidateFacts({
1340
1530
  const parsedFile = parsed.get(source.path);
1341
1531
  if (!parsedFile) continue;
1342
1532
  for (const dependency of parsedFile.dependencies) {
1343
- const resolved = resolveDependency(
1344
- ts,
1345
- dependency,
1346
- parsedFile.candidate.absolute,
1347
- compiler.optionsFor(parsedFile.candidate.absolute),
1348
- host
1349
- );
1533
+ const resolved = resolveOnce(dependency, parsedFile.candidate.absolute);
1350
1534
  if (resolved.resolution !== 'resolved-project' || !resolved.target) continue;
1351
1535
  if (loaded.has(resolved.target)) continue;
1352
1536
  const extra = loadOneCandidateFile(
@@ -1369,6 +1553,27 @@ export function resolveCandidateFacts({
1369
1553
  const extractCandidates = scoped
1370
1554
  ? candidateFiles.filter((file) => seedPathSet.has(file.path))
1371
1555
  : candidateFiles;
1556
+ if (arkRunActive) {
1557
+ const rootMatcherFor = arkRunKernelRootMatchers({
1558
+ parsed,
1559
+ patterns: kernelRootPatterns,
1560
+ resolveTarget: (file, dependency) => resolveOnce(dependency, file.candidate.absolute),
1561
+ });
1562
+ const intentPrefixes = arkRunEffectiveIntentPrefixes(config);
1563
+ for (const candidate of extractCandidates) {
1564
+ if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
1565
+ try {
1566
+ arkRunKernelCalls.push(
1567
+ ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content, {
1568
+ isKernelRootSpecifier: rootMatcherFor(candidate.path),
1569
+ intentPrefixes,
1570
+ })
1571
+ );
1572
+ } catch {
1573
+ // Never fail the resolver for ArkRun call extraction.
1574
+ }
1575
+ }
1576
+ }
1372
1577
  const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
1373
1578
  if (arkRunActive) {
1374
1579
  for (const candidate of extractCandidates) {
@@ -1428,13 +1633,7 @@ export function resolveCandidateFacts({
1428
1633
  const dependencies = [];
1429
1634
  for (const source of parsed.values()) {
1430
1635
  for (const dependency of source.dependencies) {
1431
- const resolved = resolveDependency(
1432
- ts,
1433
- dependency,
1434
- source.candidate.absolute,
1435
- compiler.optionsFor(source.candidate.absolute),
1436
- host
1437
- );
1636
+ const resolved = resolveOnce(dependency, source.candidate.absolute);
1438
1637
  if (resolved.resolverFailed) {
1439
1638
  completenessReasons.push({
1440
1639
  code: 'MODULE_RESOLUTION_FAILURE',
@@ -1486,6 +1685,11 @@ export function resolveCandidateFacts({
1486
1685
  }
1487
1686
 
1488
1687
  const projectPackageName = readPackageName(canonicalRoot, observeInput);
1688
+ // Canonicalization copies every fact. Release the per-file parse records and
1689
+ // the module host (closures keep them reachable) before that copy is built.
1690
+ parsed.clear();
1691
+ resolutionMemo.clear();
1692
+ host.dispose();
1489
1693
  return createTrustedResolvedCandidateFacts({
1490
1694
  schemaVersion: '1.2',
1491
1695
  completeness: completenessReasons.length === 0 ? 'complete' : 'partial',
@@ -1514,5 +1718,61 @@ export function resolveCandidateFacts({
1514
1718
  arkOrderXiFieldWrites,
1515
1719
  arkOrderIngestWritesXi,
1516
1720
  arkOrderReleaseKeyCounts,
1721
+ arkOrderXiTtlKeys,
1722
+ arkOrderBudgetLeaks,
1517
1723
  });
1518
1724
  }
1725
+
1726
+ /**
1727
+ * Per-file predicate: does an import specifier reach an ArkRun kernel root?
1728
+ * Uses real module resolution (tsconfig `paths`, package links) plus the
1729
+ * relative-glob fallback, and treats barrels that re-export a root as roots
1730
+ * (fixpoint), so `import { ark } from '@/main'` or a re-exporting index is traced.
1731
+ */
1732
+ function arkRunKernelRootMatchers({ parsed, patterns, resolveTarget }) {
1733
+ const regexes = [];
1734
+ for (const pattern of patterns) {
1735
+ try {
1736
+ regexes.push(globToRegExp(pattern));
1737
+ } catch {
1738
+ // Invalid glob: the relative fallback still applies.
1739
+ }
1740
+ }
1741
+ const roots = new Set([...parsed.keys()].filter((file) => regexes.some((re) => re.test(file))));
1742
+ const targets = new Map();
1743
+ if (patterns.length > 0) {
1744
+ for (const [file, info] of parsed) {
1745
+ const bySpecifier = new Map();
1746
+ for (const dependency of info.dependencies) {
1747
+ if (!dependency.specifier || (dependency.kind !== 'import' && dependency.kind !== 'export')) {
1748
+ continue;
1749
+ }
1750
+ if (bySpecifier.has(dependency.specifier)) continue;
1751
+ const resolved = resolveTarget(info, dependency);
1752
+ if (resolved.resolution === 'resolved-project' && resolved.target) {
1753
+ bySpecifier.set(dependency.specifier, resolved.target);
1754
+ }
1755
+ }
1756
+ targets.set(file, bySpecifier);
1757
+ }
1758
+ }
1759
+ const matcherFor = (file) => {
1760
+ const relative = createArkRunKernelRootSpecifierMatcher(file, patterns);
1761
+ const bySpecifier = targets.get(file);
1762
+ return (specifier) => {
1763
+ const target = bySpecifier?.get(specifier);
1764
+ return target !== undefined ? roots.has(target) : relative(specifier);
1765
+ };
1766
+ };
1767
+ for (let changed = patterns.length > 0; changed; ) {
1768
+ changed = false;
1769
+ for (const [file, info] of parsed) {
1770
+ if (roots.has(file)) continue;
1771
+ if (reexportsArkRunKernelRoot(info.candidate.content, matcherFor(file))) {
1772
+ roots.add(file);
1773
+ changed = true;
1774
+ }
1775
+ }
1776
+ }
1777
+ return matcherFor;
1778
+ }