arkgate 4.8.13 → 4.8.15

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 (126) hide show
  1. package/CHANGELOG.md +219 -4
  2. package/README.md +33 -22
  3. package/SECURITY.md +5 -3
  4. package/bin/ark-check-runtime.mjs +25 -57
  5. package/bin/ark-mcp-runtime.mjs +111 -2
  6. package/bin/ark-shared.mjs +140 -11
  7. package/bin/ark.mjs +7 -25
  8. package/bin/lib/adr-presence.mjs +97 -0
  9. package/bin/lib/agent-projection-formatters.mjs +2 -0
  10. package/bin/lib/agent-skills-package.mjs +241 -8
  11. package/bin/lib/analysis-completeness.mjs +38 -2
  12. package/bin/lib/analysis-engine.mjs +6 -6
  13. package/bin/lib/architecture-scan.mjs +33 -5
  14. package/bin/lib/ark-order-doctor.mjs +7 -1
  15. package/bin/lib/ark-order-report.mjs +2 -1
  16. package/bin/lib/ark-run-doctor.mjs +6 -0
  17. package/bin/lib/ark-run-report.mjs +6 -2
  18. package/bin/lib/arkrules-sensors.mjs +81 -0
  19. package/bin/lib/baseline-key.mjs +4 -1
  20. package/bin/lib/check-args.mjs +52 -2
  21. package/bin/lib/ci-and-commands.mjs +7 -2
  22. package/bin/lib/config-contract.mjs +64 -1
  23. package/bin/lib/design-smells.mjs +21 -1
  24. package/bin/lib/diagnostic-catalog.mjs +5 -3
  25. package/bin/lib/doctor-advisories.mjs +109 -17
  26. package/bin/lib/doctor-human.mjs +20 -6
  27. package/bin/lib/doctor-next-actions.mjs +14 -0
  28. package/bin/lib/doctor-plan.mjs +32 -2
  29. package/bin/lib/enforcement-honesty.mjs +47 -2
  30. package/bin/lib/first-run-help.mjs +19 -3
  31. package/bin/lib/gate-files.mjs +40 -3
  32. package/bin/lib/host-support-matrix.mjs +9 -1
  33. package/bin/lib/html-report-depth.mjs +2 -0
  34. package/bin/lib/html-report.mjs +13 -2
  35. package/bin/lib/import-resolve.mjs +74 -13
  36. package/bin/lib/improvement-compass-doctor.mjs +6 -1
  37. package/bin/lib/improvement-compass-map.mjs +3 -1
  38. package/bin/lib/install-migrate.mjs +23 -0
  39. package/bin/lib/layer-description.mjs +90 -8
  40. package/bin/lib/mcp-hook-payload.mjs +57 -1
  41. package/bin/lib/no-domain-frontend.mjs +91 -0
  42. package/bin/lib/presets.mjs +3 -4
  43. package/bin/lib/product-copy.mjs +19 -0
  44. package/bin/lib/projected-governed-coverage.mjs +114 -0
  45. package/bin/lib/recommend-cli.mjs +54 -0
  46. package/bin/lib/remediation.mjs +6 -2
  47. package/bin/lib/resolved-candidate-facts.mjs +225 -100
  48. package/bin/lib/rules-under-contract.mjs +117 -2
  49. package/bin/lib/scan-files.mjs +39 -0
  50. package/bin/lib/snippet-analysis.mjs +14 -8
  51. package/bin/lib/start-preview.mjs +8 -2
  52. package/bin/lib/states-transitions-presence.mjs +212 -0
  53. package/bin/lib/status-command.mjs +2 -0
  54. package/bin/lib/status-transition-catalog.mjs +410 -0
  55. package/bin/lib/team-parliament-io.mjs +10 -0
  56. package/bin/lib/upgrade-whats-new.mjs +3 -3
  57. package/bin/lib/violations.mjs +43 -1
  58. package/bin/lib/write-path-capabilities.mjs +20 -5
  59. package/bin/lib/write-path-detect.mjs +27 -2
  60. package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
  61. package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
  62. package/dist/eslint/index.cjs +4 -4
  63. package/dist/eslint/index.d.ts +1 -1
  64. package/dist/eslint/index.js +4 -4
  65. package/dist/index.cjs +31 -31
  66. package/dist/index.d.ts +126 -19
  67. package/dist/index.js +31 -31
  68. package/dist/nestjs/index.cjs +5 -5
  69. package/dist/nestjs/index.d.ts +3 -3
  70. package/dist/nestjs/index.js +5 -5
  71. package/dist/runtime/index.cjs +15 -15
  72. package/dist/runtime/index.d.ts +6 -6
  73. package/dist/runtime/index.js +15 -15
  74. package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
  75. package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
  76. package/docs/README.md +8 -6
  77. package/docs/agent-guide.md +35 -17
  78. package/docs/ai-gates.md +13 -4
  79. package/docs/arkorder.md +11 -4
  80. package/docs/brownfield-adoption.md +4 -1
  81. package/docs/configuration.md +63 -14
  82. package/docs/develop.md +4 -1
  83. package/docs/diagnostics.md +30 -10
  84. package/docs/enthusiast/README.md +6 -1
  85. package/docs/enthusiast/how-to-agent-gates.md +5 -0
  86. package/docs/enthusiast/how-to-gallery-starter.md +2 -1
  87. package/docs/enthusiast/how-to-pick-shape.md +1 -1
  88. package/docs/package-surface.md +10 -6
  89. package/docs/product-voice.md +32 -6
  90. package/docs/threat-model.md +2 -2
  91. package/docs/typescript-support.md +3 -3
  92. package/docs/use.md +23 -11
  93. package/package.json +1 -1
  94. package/schemas/ark.config.schema.json +23 -2
  95. package/server.json +2 -2
  96. package/templates/agent-skills/README.md +7 -4
  97. package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
  98. package/templates/agent-skills/ark-architect/SKILL.md +5 -18
  99. package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
  100. package/templates/agent-skills/ark-contract/SKILL.md +9 -20
  101. package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
  102. package/templates/agent-skills/ark-explain/SKILL.md +11 -5
  103. package/templates/agent-skills/ark-explore/SKILL.md +39 -5
  104. package/templates/agent-skills/ark-fix/SKILL.md +15 -20
  105. package/templates/agent-skills/ark-loop/SKILL.md +14 -20
  106. package/templates/agent-skills/ark-order/SKILL.md +200 -0
  107. package/templates/agent-skills/ark-place/SKILL.md +49 -11
  108. package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
  109. package/templates/agent-skills/ark-think/SKILL.md +24 -126
  110. package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
  111. package/templates/arkrules/DomainModel.json +14 -1
  112. package/templates/skills/ark-adopt.md +91 -6
  113. package/templates/skills/ark-architect.md +5 -18
  114. package/templates/skills/ark-autopilot.md +14 -6
  115. package/templates/skills/ark-contract.md +9 -20
  116. package/templates/skills/ark-coverage.md +13 -9
  117. package/templates/skills/ark-explain.md +11 -5
  118. package/templates/skills/ark-explore.md +39 -5
  119. package/templates/skills/ark-fix.md +15 -20
  120. package/templates/skills/ark-loop.md +14 -20
  121. package/templates/skills/ark-order.md +200 -0
  122. package/templates/skills/ark-place.md +49 -11
  123. package/templates/skills/ark-runtime.md +21 -6
  124. package/templates/skills/ark-think.md +24 -126
  125. package/templates/skills/ark-upgrade.md +14 -3
  126. package/templates/tests/ark-adoption-gaps.test.ts +5 -4
@@ -0,0 +1,114 @@
1
+ /**
2
+ * One definition of projected governed coverage: include-scoped files that
3
+ * match a layer glob, via computeCoverage. Shared by start, --recommend, and
4
+ * doctor so the same tree cannot advertise two "projected" percents.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { collectGovernedFiles } from './scan-files.mjs';
9
+ import { computeCoverage } from './doctor-plan.mjs';
10
+ import { ARCHITECTURE_PRESETS } from './presets.mjs';
11
+ import {
12
+ detectWorkspaces,
13
+ resolveIncludeRoots,
14
+ resolveStartInitPreset,
15
+ } from '../ark-shared.mjs';
16
+
17
+ export function measureGovernedCoverage(root, config) {
18
+ const files = collectGovernedFiles(root, config);
19
+ return computeCoverage(root, config, files, config.rules ?? []);
20
+ }
21
+
22
+ export function summarizeGovernedCoverage(coverage) {
23
+ const governed = coverage?.governed ?? {};
24
+ return {
25
+ percent: typeof governed.percent === 'number' ? governed.percent : 0,
26
+ classifiedFiles: governed.classifiedFiles ?? 0,
27
+ totalFiles: governed.totalFiles ?? coverage?.totalFiles ?? 0,
28
+ emptyScope: coverage?.emptyScope === true,
29
+ };
30
+ }
31
+
32
+ export function starterConfigForPreset(root, preset) {
33
+ const factory = ARCHITECTURE_PRESETS[preset];
34
+ if (typeof factory !== 'function') return null;
35
+ const workspacesOrInclude =
36
+ preset === 'monorepo' || preset === 'ui-surface'
37
+ ? resolveIncludeRoots(root)
38
+ : detectWorkspaces(root);
39
+ return factory(workspacesOrInclude, root);
40
+ }
41
+
42
+ /**
43
+ * Contract start/recommend/doctor should score: existing ark.config.json, else
44
+ * the starter --init would write for this tree (same preset resolution as start).
45
+ */
46
+ export function resolveProjectedCoverageConfig(root, recommendation = {}) {
47
+ const configPath = path.join(root, 'ark.config.json');
48
+ if (fs.existsSync(configPath)) {
49
+ try {
50
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
51
+ if (parsed && typeof parsed === 'object') {
52
+ return { config: parsed, source: 'existing-config' };
53
+ }
54
+ } catch {
55
+ /* fall through to starter */
56
+ }
57
+ }
58
+ const preset = resolveStartInitPreset(root, recommendation);
59
+ if (!preset) return { config: null, source: 'none', preset: null };
60
+ return { config: starterConfigForPreset(root, preset), source: 'start-starter', preset };
61
+ }
62
+
63
+ export function measureProjectedGovernedCoverage(root, recommendation = {}) {
64
+ const resolved = resolveProjectedCoverageConfig(root, recommendation);
65
+ if (!resolved.config) {
66
+ return {
67
+ percent: 0,
68
+ classifiedFiles: 0,
69
+ totalFiles: 0,
70
+ emptyScope: true,
71
+ source: resolved.source,
72
+ preset: resolved.preset ?? null,
73
+ };
74
+ }
75
+ return {
76
+ ...summarizeGovernedCoverage(measureGovernedCoverage(root, resolved.config)),
77
+ source: resolved.source,
78
+ preset: resolved.preset ?? recommendation.preset ?? null,
79
+ };
80
+ }
81
+
82
+ const PROJECTED_COVERAGE_REASON = /projected governed coverage is \d+% \(below 90%\)/;
83
+
84
+ export function withProjectedGovernedCoverage(recommendation, root) {
85
+ if (!recommendation || typeof recommendation !== 'object') return recommendation;
86
+ const measured = measureProjectedGovernedCoverage(root, recommendation);
87
+ const percent = measured.percent;
88
+ const confirmationReasons = (recommendation.confirmationReasons ?? []).filter(
89
+ (reason) => !PROJECTED_COVERAGE_REASON.test(String(reason))
90
+ );
91
+ if (percent < 90) {
92
+ confirmationReasons.push(`projected governed coverage is ${percent}% (below 90%)`);
93
+ }
94
+ return {
95
+ ...recommendation,
96
+ requiresConfirmation:
97
+ Boolean(recommendation.requiresConfirmation) ||
98
+ percent < 90 ||
99
+ Boolean(recommendation.thinTsSurface),
100
+ confirmationReasons,
101
+ signals: {
102
+ ...recommendation.signals,
103
+ projectedGovernedCoverage: percent,
104
+ projectedGovernedClassifiedFiles: measured.classifiedFiles,
105
+ projectedGovernedTotalFiles: measured.totalFiles,
106
+ projectedGovernedSource: measured.source,
107
+ },
108
+ projectedCoverage: {
109
+ percent,
110
+ classifiedFiles: measured.classifiedFiles,
111
+ totalFiles: measured.totalFiles,
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `--recommend` presentation. Lives here so ark-check-runtime stays under the
3
+ * orchestration LOC budget and doctor does not parse this path.
4
+ */
5
+ import path from 'node:path';
6
+ import {
7
+ ADOPTION_PLAN_FILENAME,
8
+ buildArchitectureRecommendation,
9
+ formatArchitectureRecommendationHuman,
10
+ writeAdoptionPlan,
11
+ } from '../ark-shared.mjs';
12
+ import { withProjectedGovernedCoverage } from './projected-governed-coverage.mjs';
13
+
14
+ export function runRecommend(args) {
15
+ try {
16
+ const recommendation = withProjectedGovernedCoverage(
17
+ buildArchitectureRecommendation(args.root),
18
+ args.root
19
+ );
20
+ let planWritten;
21
+ if (args.writePlan) {
22
+ const result = writeAdoptionPlan(args.root, recommendation);
23
+ planWritten = result.path;
24
+ }
25
+ if (args.json) {
26
+ console.log(
27
+ JSON.stringify(
28
+ {
29
+ ...recommendation,
30
+ ...(planWritten
31
+ ? { adoptionPlanPath: path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME }
32
+ : {}),
33
+ },
34
+ null,
35
+ 2
36
+ )
37
+ );
38
+ } else {
39
+ console.log(formatArchitectureRecommendationHuman(recommendation));
40
+ if (planWritten) {
41
+ console.log('');
42
+ console.log(`Wrote ${path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME}`);
43
+ }
44
+ }
45
+ } catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ if (args.json) {
48
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
49
+ } else {
50
+ console.error(`ark-check --recommend failed: ${message}`);
51
+ }
52
+ process.exitCode = 2;
53
+ }
54
+ }
@@ -216,6 +216,10 @@ export function deterministicNextAction(violation) {
216
216
  return 'Add metadata.source to the publish call, then run Ark again.';
217
217
  case 'INVARIANT_COVERAGE_OUTSIDE_ROOTS':
218
218
  return `Move the covering test under a declared coverage root, or add its root to coverage.coverageRoots in ark.config.json, then run Ark again.`;
219
+ case 'INVARIANT_CATALOG_EMPTY':
220
+ return typeof violation.file === 'string' && violation.file.length > 0
221
+ ? `Add 1–2 short phrases to invariants[] in ${violation.file}, then run ark-check --doctor. Starters show the shape.`
222
+ : 'Add 1–2 short phrases to invariants[] in arkrules/<Domain>.json, then run ark-check --doctor. Starters show the shape.';
219
223
  case 'ARKRULE_STRUCTURE':
220
224
  case 'ARKRULE_INVARIANT':
221
225
  case 'INVARIANT_UNCOVERED':
@@ -249,7 +253,7 @@ export function deterministicNextAction(violation) {
249
253
  ? `Don't write ${violation.target} from a use-case. Take the event in, or change that choice through the valve (proposeRelease then apply), not a generic update.`
250
254
  : "Don't write a named product choice from a use-case. Take the event in, or change that choice through the valve (proposeRelease then apply), not a generic update.";
251
255
  case 'ARKORDER_UNVALVED_RELEASE':
252
- return 'Change ξ with proposeRelease then apply(ProposeResult). release() is only the first freeze. Never mechanical-safe.';
256
+ return 'Change the choice with proposeRelease then apply. release() is only the first freeze. Never mechanical-safe.';
253
257
  default:
254
258
  if (typeof violation.ruleId === 'string' && violation.ruleId.startsWith('ARKRULE_')) {
255
259
  return `Fix the ArkRule ${typeof violation.arkruleId === 'string' ? violation.arkruleId : violation.ruleId}, then preflight again.`;
@@ -544,7 +548,7 @@ export function enrichViolationWithFixClass(violation) {
544
548
  : violation.ruleId === 'ARKORDER_STALE_SIGMA'
545
549
  ? 'Refresh σ. ξ does not expire.'
546
550
  : violation.ruleId === 'ARKORDER_UNVALVED_RELEASE'
547
- ? 'The pattern is frozen. proposeRelease then apply — do not call release() again with a different ξ.'
551
+ ? 'The choice is already frozen. proposeRelease then apply — do not call release() again with a different value.'
548
552
  : 'Call createOrderPlane from arkgate/order in a listed plane root so the app actually freezes a pattern.';
549
553
  break;
550
554
  default:
@@ -211,32 +211,67 @@ function canonicalOverlayPath(
211
211
  return [...candidates].sort()[0] ?? requested;
212
212
  }
213
213
 
214
- function collectCandidateFiles(root, config, changes, observeInput) {
214
+ function discoverScopedFiles(root, config, scopeRelatives, observeInput) {
215
+ const discovered = [];
216
+ const seen = new Set();
217
+ for (const raw of scopeRelatives ?? []) {
218
+ let relative;
219
+ try {
220
+ relative = canonicalProjectPath(
221
+ String(raw || '')
222
+ .replace(/\\/g, '/')
223
+ .replace(/^\.\//, '')
224
+ );
225
+ } catch {
226
+ continue;
227
+ }
228
+ if (seen.has(relative) || !isGovernableSourceFile(path.basename(relative))) continue;
229
+ if (!isIncluded(relative, config.include) || isScanExcludedRelative(relative, config)) {
230
+ continue;
231
+ }
232
+ const absolute = path.join(root, ...relative.split('/'));
233
+ if (!fs.existsSync(absolute)) continue;
234
+ seen.add(relative);
235
+ observeResolvedInput(observeInput, absolute, 'realpath');
236
+ discovered.push({
237
+ absolute,
238
+ real: fs.realpathSync(absolute),
239
+ relative,
240
+ });
241
+ }
242
+ return discovered.sort((left, right) =>
243
+ left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
244
+ );
245
+ }
246
+
247
+ function collectCandidateFiles(root, config, changes, observeInput, scopeRelatives) {
215
248
  const files = new Map();
216
249
  const directoryAliases = new Map();
217
250
  const expandedAliasDirectories = new Set();
218
- const discovered = (config.include ?? [])
219
- .flatMap((entry) =>
220
- collectGovernedFiles(root, { ...config, include: [entry] }, {
221
- observeInput,
222
- onDirectory(absolute, real) {
223
- let relative = normalize(path.relative(root, absolute));
224
- if (relative === '.') relative = '';
225
- rememberDirectoryAlias(directoryAliases, real, relative, absolute);
226
- },
227
- })
228
- )
229
- .map((absolute) => {
230
- observeResolvedInput(observeInput, absolute, 'realpath');
231
- return {
232
- absolute,
233
- real: fs.realpathSync(absolute),
234
- relative: canonicalProjectPath(normalize(path.relative(root, absolute))),
235
- };
236
- })
237
- .sort((left, right) =>
238
- left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
239
- );
251
+ const discovered = Array.isArray(scopeRelatives)
252
+ ? discoverScopedFiles(root, config, scopeRelatives, observeInput)
253
+ : (config.include ?? [])
254
+ .flatMap((entry) =>
255
+ collectGovernedFiles(root, { ...config, include: [entry] }, {
256
+ observeInput,
257
+ onDirectory(absolute, real) {
258
+ let relative = normalize(path.relative(root, absolute));
259
+ if (relative === '.') relative = '';
260
+ rememberDirectoryAlias(directoryAliases, real, relative, absolute);
261
+ },
262
+ })
263
+ )
264
+ .map((absolute) => {
265
+ observeResolvedInput(observeInput, absolute, 'realpath');
266
+ return {
267
+ absolute,
268
+ real: fs.realpathSync(absolute),
269
+ relative: canonicalProjectPath(normalize(path.relative(root, absolute))),
270
+ };
271
+ })
272
+ .sort((left, right) =>
273
+ left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
274
+ );
240
275
  const canonicalByRealpath = new Map();
241
276
  for (const candidate of discovered) {
242
277
  if (!canonicalByRealpath.has(candidate.real)) canonicalByRealpath.set(candidate.real, candidate);
@@ -582,7 +617,8 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
582
617
  const optionsByPath = new Map();
583
618
  const configInputsByPath = new Map();
584
619
  const reasons = [];
585
- for (const configPath of configPaths) {
620
+ const loadConfigOptions = (configPath) => {
621
+ if (optionsByPath.has(configPath)) return optionsByPath.get(configPath);
586
622
  const configContents = new Map();
587
623
  const readConfig = (fileName) => {
588
624
  observeResolvedInput(observeInput, fileName, 'tsconfig');
@@ -605,15 +641,16 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
605
641
  });
606
642
  optionsByPath.set(configPath, {});
607
643
  configInputsByPath.set(configPath, configContents);
608
- continue;
644
+ return {};
609
645
  }
610
646
  const parsed = ts.parseJsonConfigFileContent(
611
647
  read.config,
612
648
  {
613
649
  useCaseSensitiveFileNames: ts.sys?.useCaseSensitiveFileNames ?? true,
614
- readDirectory(...args) {
615
- observeResolvedInput(observeInput, args[0], 'tsconfig-directory');
616
- return ts.sys?.readDirectory ? ts.sys.readDirectory(...args) : [];
650
+ readDirectory(dir) {
651
+ observeResolvedInput(observeInput, dir, 'tsconfig-directory');
652
+ // Options-only parse: `fileNames` are unused. Skip the include walk (#205).
653
+ return [];
617
654
  },
618
655
  fileExists(fileName) {
619
656
  observeResolvedInput(observeInput, fileName, 'tsconfig-exists');
@@ -637,11 +674,19 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
637
674
  }
638
675
  optionsByPath.set(configPath, parsed.options ?? {});
639
676
  configInputsByPath.set(configPath, configContents);
640
- }
677
+ return parsed.options ?? {};
678
+ };
679
+ for (const configPath of configPaths) loadConfigOptions(configPath);
641
680
 
642
681
  const optionsFor = (fileName) => {
643
- const configPath = configByFile.get(path.resolve(fileName));
644
- return configPath ? optionsByPath.get(configPath) ?? {} : {};
682
+ const resolved = path.resolve(fileName);
683
+ let configPath = configByFile.get(resolved);
684
+ if (!configPath) {
685
+ configPath = explicitPath
686
+ ?? nearestTsconfig(root, resolved, nearestConfigByDirectory, observeInput);
687
+ if (configPath) configByFile.set(resolved, configPath);
688
+ }
689
+ return configPath ? loadConfigOptions(configPath) ?? {} : {};
645
690
  };
646
691
  const configs = configPaths.map((configPath) => ({
647
692
  path: configLabel(root, configPath, externalAnchor),
@@ -1036,6 +1081,19 @@ function unavailableFacts(config, ts, reason) {
1036
1081
  });
1037
1082
  }
1038
1083
 
1084
+ function loadOneCandidateFile(root, config, relative, observeInput) {
1085
+ const discovered = discoverScopedFiles(root, config, [relative], observeInput);
1086
+ const found = discovered[0];
1087
+ if (!found) return null;
1088
+ observeResolvedInput(observeInput, found.absolute, 'source');
1089
+ return {
1090
+ path: found.relative,
1091
+ absolute: path.resolve(found.absolute),
1092
+ real: path.resolve(found.real),
1093
+ content: fs.readFileSync(found.absolute, 'utf8'),
1094
+ };
1095
+ }
1096
+
1039
1097
  /** Resolve one complete candidate tree (base or virtual overlay) into versioned neutral facts. */
1040
1098
  export function resolveCandidateFacts({
1041
1099
  root,
@@ -1044,6 +1102,7 @@ export function resolveCandidateFacts({
1044
1102
  tsconfig,
1045
1103
  changes = [],
1046
1104
  observeInput,
1105
+ scopeFiles,
1047
1106
  }) {
1048
1107
  if (!ts?.createSourceFile || !ts?.resolveModuleName) {
1049
1108
  return unavailableFacts(config, ts, 'No API-compatible TypeScript resolver is available.');
@@ -1053,10 +1112,17 @@ export function resolveCandidateFacts({
1053
1112
  let candidateFiles;
1054
1113
  let canonicalChanges;
1055
1114
  let compiler;
1115
+ const scoped = Array.isArray(scopeFiles);
1056
1116
  try {
1057
1117
  observeResolvedInput(observeInput, root, 'realpath');
1058
1118
  canonicalRoot = fs.realpathSync(root);
1059
- const candidate = collectCandidateFiles(canonicalRoot, config, changes, observeInput);
1119
+ const candidate = collectCandidateFiles(
1120
+ canonicalRoot,
1121
+ config,
1122
+ changes,
1123
+ observeInput,
1124
+ scoped ? scopeFiles : undefined
1125
+ );
1060
1126
  candidateFiles = candidate.files;
1061
1127
  canonicalChanges = candidate.changes;
1062
1128
  compiler = compilerContext(ts, canonicalRoot, tsconfig, candidateFiles, observeInput);
@@ -1102,11 +1168,20 @@ export function resolveCandidateFacts({
1102
1168
  const arkOrderXiFieldWrites = [];
1103
1169
  const arkOrderIngestWritesXi = [];
1104
1170
  const arkOrderReleaseKeyCounts = [];
1105
- const xiKeys = [...(config.arkOrder?.xiKeys ?? [])];
1106
- const compositionRootPatterns = [...(config.arkRun?.compositionRoots ?? [])];
1107
- const planeRootPatterns = [...(config.arkOrder?.planeRoots ?? [])];
1171
+ const arkRunActive = config.arkRun != null && typeof config.arkRun === 'object';
1172
+ const arkOrderActive = config.arkOrder != null && typeof config.arkOrder === 'object';
1173
+ const arkRulesActive =
1174
+ config.arkRules != null &&
1175
+ typeof config.arkRules === 'object' &&
1176
+ Object.keys(config.arkRules).length > 0;
1177
+ const xiKeys = arkOrderActive ? [...(config.arkOrder?.xiKeys ?? [])] : [];
1178
+ const compositionRootPatterns = arkRunActive
1179
+ ? [...(config.arkRun?.compositionRoots ?? [])]
1180
+ : [];
1181
+ const planeRootPatterns = arkOrderActive ? [...(config.arkOrder?.planeRoots ?? [])] : [];
1108
1182
 
1109
- for (const candidate of candidateFiles) {
1183
+ const seedPathSet = new Set(candidateFiles.map((file) => file.path));
1184
+ const ingest = (candidate, fullExtract) => {
1110
1185
  const sourceFile = ts.createSourceFile(
1111
1186
  candidate.absolute,
1112
1187
  candidate.content,
@@ -1158,6 +1233,7 @@ export function resolveCandidateFacts({
1158
1233
  message: `${candidate.path} has ${parseDiagnosticCount} TypeScript parse diagnostic(s).`,
1159
1234
  });
1160
1235
  }
1236
+ if (!fullExtract) return;
1161
1237
  const forbiddenUses = mayContainForbiddenCapability(ts, sourceFile, forbiddenGlobals)
1162
1238
  ? collectForbiddenCapabilityUses(ts, sourceFile, forbiddenGlobals)
1163
1239
  : [];
@@ -1188,83 +1264,132 @@ export function resolveCandidateFacts({
1188
1264
  );
1189
1265
  // Class-shape extraction is text-conservative (false negatives over false positives).
1190
1266
  // Only TS/TSX candidates; sensors consume the same shape via facts.classShapes.
1267
+ // Extra-plane and ArkRules extractors stay silent when that plane is off (#212).
1191
1268
  if (/\.(tsx?|mts|cts)$/i.test(candidate.path)) {
1192
- try {
1193
- classShapes.push(...extractClassShapesFromSource(candidate.path, candidate.content));
1194
- } catch {
1195
- // Never fail the resolver for shape extraction; sensors stay silent on this file.
1196
- }
1197
- try {
1198
- arkRunKernelCalls.push(
1199
- ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
1200
- );
1201
- } catch {
1202
- // Never fail the resolver for ArkRun call extraction.
1203
- }
1204
- try {
1205
- arkRunDeclarations.push(
1206
- ...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
1207
- );
1208
- } catch {
1209
- // Never fail the resolver for ArkRun declaration extraction.
1210
- }
1211
- try {
1212
- arkOrderPlaneCalls.push(
1213
- ...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
1214
- );
1215
- } catch {
1216
- // Never fail the resolver for ArkOrder factory extraction.
1217
- }
1218
- try {
1219
- arkOrderGenericUpdates.push(
1220
- ...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
1221
- );
1222
- } catch {
1223
- // Never fail the resolver for ArkOrder generic-update extraction.
1269
+ if (arkRulesActive || arkRunActive) {
1270
+ try {
1271
+ classShapes.push(...extractClassShapesFromSource(candidate.path, candidate.content));
1272
+ } catch {
1273
+ // Never fail the resolver for shape extraction; sensors stay silent on this file.
1274
+ }
1224
1275
  }
1225
- try {
1226
- arkOrderXiFieldWrites.push(
1227
- ...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
1228
- );
1229
- } catch {
1230
- // Never fail the resolver for ArkOrder xi-field-write extraction.
1276
+ 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
+ }
1284
+ try {
1285
+ arkRunDeclarations.push(
1286
+ ...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
1287
+ );
1288
+ } catch {
1289
+ // Never fail the resolver for ArkRun declaration extraction.
1290
+ }
1231
1291
  }
1232
- try {
1233
- arkOrderIngestWritesXi.push(
1234
- ...extractArkOrderIngestWritesXiFromSource(candidate.path, candidate.content)
1235
- );
1236
- } catch {
1237
- // Never fail the resolver for ArkOrder ingest-writes-ξ extraction.
1292
+ if (arkOrderActive) {
1293
+ try {
1294
+ arkOrderPlaneCalls.push(
1295
+ ...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
1296
+ );
1297
+ } catch {
1298
+ // Never fail the resolver for ArkOrder factory extraction.
1299
+ }
1300
+ try {
1301
+ arkOrderGenericUpdates.push(
1302
+ ...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
1303
+ );
1304
+ } catch {
1305
+ // Never fail the resolver for ArkOrder generic-update extraction.
1306
+ }
1307
+ try {
1308
+ arkOrderXiFieldWrites.push(
1309
+ ...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
1310
+ );
1311
+ } catch {
1312
+ // Never fail the resolver for ArkOrder xi-field-write extraction.
1313
+ }
1314
+ try {
1315
+ arkOrderIngestWritesXi.push(
1316
+ ...extractArkOrderIngestWritesXiFromSource(candidate.path, candidate.content)
1317
+ );
1318
+ } catch {
1319
+ // Never fail the resolver for ArkOrder ingest-writes-ξ extraction.
1320
+ }
1321
+ try {
1322
+ arkOrderReleaseKeyCounts.push(
1323
+ ...extractArkOrderReleaseKeyCountsFromSource(candidate.path, candidate.content)
1324
+ );
1325
+ } catch {
1326
+ // Never fail the resolver for ArkOrder release key-count extraction.
1327
+ }
1238
1328
  }
1239
- try {
1240
- arkOrderReleaseKeyCounts.push(
1241
- ...extractArkOrderReleaseKeyCountsFromSource(candidate.path, candidate.content)
1242
- );
1243
- } catch {
1244
- // Never fail the resolver for ArkOrder release key-count extraction.
1329
+ }
1330
+ };
1331
+
1332
+ for (const candidate of candidateFiles) ingest(candidate, true);
1333
+
1334
+ if (scoped) {
1335
+ const loaded = new Set(candidateFiles.map((file) => file.path));
1336
+ let pending = [...candidateFiles];
1337
+ while (pending.length > 0) {
1338
+ const discovered = [];
1339
+ for (const source of pending) {
1340
+ const parsedFile = parsed.get(source.path);
1341
+ if (!parsedFile) continue;
1342
+ 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
+ );
1350
+ if (resolved.resolution !== 'resolved-project' || !resolved.target) continue;
1351
+ if (loaded.has(resolved.target)) continue;
1352
+ const extra = loadOneCandidateFile(
1353
+ canonicalRoot,
1354
+ config,
1355
+ resolved.target,
1356
+ observeInput
1357
+ );
1358
+ if (!extra) continue;
1359
+ loaded.add(extra.path);
1360
+ candidateFiles.push(extra);
1361
+ ingest(extra, false);
1362
+ discovered.push(extra);
1363
+ }
1245
1364
  }
1365
+ pending = discovered;
1246
1366
  }
1247
1367
  }
1248
1368
 
1369
+ const extractCandidates = scoped
1370
+ ? candidateFiles.filter((file) => seedPathSet.has(file.path))
1371
+ : candidateFiles;
1249
1372
  const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
1250
- for (const candidate of candidateFiles) {
1251
- if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
1252
- try {
1253
- arkRunManagedNews.push(
1254
- ...extractArkRunManagedNewsFromSource(
1255
- candidate.path,
1256
- candidate.content,
1257
- admittedTypeNames
1258
- )
1259
- );
1260
- } catch {
1261
- // Never fail the resolver for managed-new extraction.
1373
+ if (arkRunActive) {
1374
+ for (const candidate of extractCandidates) {
1375
+ if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
1376
+ try {
1377
+ arkRunManagedNews.push(
1378
+ ...extractArkRunManagedNewsFromSource(
1379
+ candidate.path,
1380
+ candidate.content,
1381
+ admittedTypeNames
1382
+ )
1383
+ );
1384
+ } catch {
1385
+ // Never fail the resolver for managed-new extraction.
1386
+ }
1262
1387
  }
1263
1388
  }
1264
1389
 
1265
1390
  if (planeRootPatterns.length > 0) {
1266
1391
  const factoryFiles = new Set(arkOrderPlaneCalls.map((call) => call.file));
1267
- for (const candidate of candidateFiles) {
1392
+ for (const candidate of extractCandidates) {
1268
1393
  for (const pattern of planeRootPatterns) {
1269
1394
  try {
1270
1395
  if (!globToRegExp(pattern).test(candidate.path)) continue;
@@ -1284,7 +1409,7 @@ export function resolveCandidateFacts({
1284
1409
  const factoryFiles = new Set(
1285
1410
  arkRunKernelCalls.filter((call) => call.kind === 'factory').map((call) => call.file)
1286
1411
  );
1287
- for (const candidate of candidateFiles) {
1412
+ for (const candidate of extractCandidates) {
1288
1413
  for (const pattern of compositionRootPatterns) {
1289
1414
  try {
1290
1415
  if (!globToRegExp(pattern).test(candidate.path)) continue;