arkgate 4.8.14 → 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 (97) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +13 -5
  3. package/bin/ark-check-runtime.mjs +17 -49
  4. package/bin/ark-mcp-runtime.mjs +111 -2
  5. package/bin/ark-shared.mjs +140 -11
  6. package/bin/ark.mjs +7 -25
  7. package/bin/lib/adr-presence.mjs +97 -0
  8. package/bin/lib/agent-skills-package.mjs +179 -1
  9. package/bin/lib/analysis-completeness.mjs +38 -2
  10. package/bin/lib/analysis-engine.mjs +6 -6
  11. package/bin/lib/architecture-scan.mjs +26 -4
  12. package/bin/lib/ark-run-doctor.mjs +6 -0
  13. package/bin/lib/ark-run-report.mjs +6 -2
  14. package/bin/lib/arkrules-sensors.mjs +81 -0
  15. package/bin/lib/baseline-key.mjs +4 -1
  16. package/bin/lib/check-args.mjs +52 -2
  17. package/bin/lib/config-contract.mjs +64 -1
  18. package/bin/lib/diagnostic-catalog.mjs +3 -1
  19. package/bin/lib/doctor-advisories.mjs +94 -10
  20. package/bin/lib/doctor-human.mjs +18 -5
  21. package/bin/lib/doctor-next-actions.mjs +14 -0
  22. package/bin/lib/doctor-plan.mjs +32 -2
  23. package/bin/lib/enforcement-honesty.mjs +47 -2
  24. package/bin/lib/first-run-help.mjs +8 -1
  25. package/bin/lib/host-support-matrix.mjs +9 -1
  26. package/bin/lib/html-report-depth.mjs +2 -0
  27. package/bin/lib/html-report.mjs +13 -2
  28. package/bin/lib/import-resolve.mjs +74 -13
  29. package/bin/lib/improvement-compass-doctor.mjs +6 -1
  30. package/bin/lib/improvement-compass-map.mjs +3 -1
  31. package/bin/lib/layer-description.mjs +90 -8
  32. package/bin/lib/mcp-hook-payload.mjs +56 -0
  33. package/bin/lib/no-domain-frontend.mjs +91 -0
  34. package/bin/lib/presets.mjs +3 -4
  35. package/bin/lib/product-copy.mjs +15 -0
  36. package/bin/lib/projected-governed-coverage.mjs +114 -0
  37. package/bin/lib/recommend-cli.mjs +54 -0
  38. package/bin/lib/remediation.mjs +4 -0
  39. package/bin/lib/resolved-candidate-facts.mjs +83 -66
  40. package/bin/lib/rules-under-contract.mjs +117 -2
  41. package/bin/lib/snippet-analysis.mjs +14 -8
  42. package/bin/lib/start-preview.mjs +5 -2
  43. package/bin/lib/states-transitions-presence.mjs +212 -0
  44. package/bin/lib/status-command.mjs +2 -0
  45. package/bin/lib/status-transition-catalog.mjs +410 -0
  46. package/bin/lib/team-parliament-io.mjs +10 -0
  47. package/bin/lib/violations.mjs +13 -1
  48. package/bin/lib/write-path-capabilities.mjs +20 -5
  49. package/bin/lib/write-path-detect.mjs +27 -2
  50. package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
  51. package/dist/{diagnosticCatalog-DVx_2RmF.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
  52. package/dist/eslint/index.cjs +4 -4
  53. package/dist/eslint/index.d.ts +1 -1
  54. package/dist/eslint/index.js +4 -4
  55. package/dist/index.cjs +31 -31
  56. package/dist/index.d.ts +70 -9
  57. package/dist/index.js +31 -31
  58. package/dist/nestjs/index.cjs +5 -5
  59. package/dist/nestjs/index.d.ts +3 -3
  60. package/dist/nestjs/index.js +5 -5
  61. package/dist/runtime/index.cjs +15 -15
  62. package/dist/runtime/index.d.ts +6 -6
  63. package/dist/runtime/index.js +15 -15
  64. package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
  65. package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
  66. package/docs/README.md +1 -1
  67. package/docs/agent-guide.md +6 -4
  68. package/docs/ai-gates.md +10 -3
  69. package/docs/brownfield-adoption.md +4 -1
  70. package/docs/configuration.md +55 -7
  71. package/docs/develop.md +1 -0
  72. package/docs/diagnostics.md +23 -3
  73. package/docs/enthusiast/how-to-agent-gates.md +5 -0
  74. package/docs/enthusiast/how-to-pick-shape.md +1 -1
  75. package/docs/package-surface.md +7 -5
  76. package/docs/use.md +5 -0
  77. package/package.json +1 -1
  78. package/schemas/ark.config.schema.json +23 -2
  79. package/server.json +2 -2
  80. package/templates/agent-skills/README.md +1 -1
  81. package/templates/agent-skills/ark-adopt/SKILL.md +82 -1
  82. package/templates/agent-skills/ark-autopilot/SKILL.md +6 -2
  83. package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
  84. package/templates/agent-skills/ark-explain/SKILL.md +4 -2
  85. package/templates/agent-skills/ark-explore/SKILL.md +14 -1
  86. package/templates/agent-skills/ark-place/SKILL.md +38 -3
  87. package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
  88. package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
  89. package/templates/arkrules/DomainModel.json +14 -1
  90. package/templates/skills/ark-adopt.md +82 -1
  91. package/templates/skills/ark-autopilot.md +6 -2
  92. package/templates/skills/ark-coverage.md +1 -1
  93. package/templates/skills/ark-explain.md +4 -2
  94. package/templates/skills/ark-explore.md +14 -1
  95. package/templates/skills/ark-place.md +38 -3
  96. package/templates/skills/ark-runtime.md +4 -2
  97. package/templates/skills/ark-upgrade.md +1 -1
@@ -308,6 +308,62 @@ export function emitHostAllow(output, { antigravityStyle, cursorStyle }) {
308
308
  emitCursorAllow(output, cursorStyle);
309
309
  }
310
310
 
311
+ /** Host-native deny envelopes. Exit 2 is still set by the caller. */
312
+ export function emitHostDeny(output, { antigravityStyle, cursorStyle, grokStyle, message, file }) {
313
+ const text = String(message || '').endsWith('\n') ? String(message) : `${message}\n`;
314
+ output.stderr(text);
315
+ if (antigravityStyle || grokStyle) {
316
+ output.stdout(`${JSON.stringify({ decision: 'deny', reason: String(message || '').trim() })}\n`);
317
+ }
318
+ if (cursorStyle) {
319
+ output.stdout(
320
+ JSON.stringify({
321
+ permission: 'deny',
322
+ agent_message: String(message || '').trim(),
323
+ user_message: `ArkGate blocked write to ${file || 'this file'}`,
324
+ }) + '\n'
325
+ );
326
+ }
327
+ }
328
+
329
+ /** File-local twin of CONFIG_UNCLASSIFIED_FILES — included, no layer, write must not land. */
330
+ export function unclassifiedIncludedWriteDeny(relativePath) {
331
+ const file = String(relativePath || 'this file');
332
+ return {
333
+ ruleId: 'CONFIG_UNCLASSIFIED_FILES',
334
+ message: `${file} is included but matches no layer, so import rules will not run on it.`,
335
+ nextAction:
336
+ 'Put it in a layer folder with /ark-place, or extend layer patterns / narrow include.',
337
+ };
338
+ }
339
+
340
+ /** File-local twin of CONFIG_LAYER_MISSING_OWNER — required owners, this house has none. */
341
+ export function unownedLayerWriteDeny(relativePath, layerName) {
342
+ const file = String(relativePath || 'this file');
343
+ const house = String(layerName || 'this layer');
344
+ return {
345
+ ruleId: 'CONFIG_LAYER_MISSING_OWNER',
346
+ message: `${file} is in ${house}, and that folder has no owner.`,
347
+ nextAction: `Add a GitHub handle or email to ${house}'s owners in ark.config.json (/ark-adopt).`,
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Fail-closed write when requireLayerOwners is on and this house has no owners.
353
+ * Silent when the flag is off, the layer is reserved, or owners are present.
354
+ */
355
+ export function requiredOwnerWriteDeny(config, layerName, relativePath) {
356
+ if (config?.requireLayerOwners !== true || !layerName) return null;
357
+ const layer = (config.layers ?? []).find((entry) => entry?.name === layerName);
358
+ if (!layer) return null;
359
+ if (layer.optional === true || layer.reserved === true || layer.allowEmpty === true) return null;
360
+ const owners = Array.isArray(layer.owners)
361
+ ? layer.owners.filter((entry) => typeof entry === 'string' && entry.length > 0)
362
+ : [];
363
+ if (owners.length > 0) return null;
364
+ return unownedLayerWriteDeny(relativePath, layerName);
365
+ }
366
+
311
367
  /**
312
368
  * Socket-style write-gate deny: two lines first. Pass/fail, no score.
313
369
  * Rule id stays on a following line, not the first sentence.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Soft no-Domain / all-logic-in-frontend residual (P2 §3 / Guiar).
3
+ * Tooling I/O. Never a gate fail.
4
+ *
5
+ * Projects facts doctor already has: empty Domain-role layer + presentation
6
+ * file share, or the existing `domain-logic-in-ui` smell. No second tree walk.
7
+ * Silent when there is no frontend, Domain already has files, or the bag is
8
+ * too thin to call “all logic in the UI.”
9
+ */
10
+
11
+ import { isDomainRoleLayerName } from './arkrules-sensors.mjs';
12
+
13
+ export const NO_DOMAIN_FRONTEND_ASK =
14
+ 'Business rules live in the UI, and Domain is still empty.';
15
+
16
+ export const NO_DOMAIN_FRONTEND_NEXT =
17
+ 'Put one pure rule in a Domain file (/ark-place), then one small refactor with /ark-autopilot. Do not pile more rules in pages.';
18
+
19
+ /** Presentation-role house by name. Align with design-smells presentation heuristic. */
20
+ export function isPresentationRoleLayerName(name) {
21
+ return typeof name === 'string' && /presentation|ui|view|frontend/i.test(name);
22
+ }
23
+
24
+ function layerRows(input) {
25
+ return Array.isArray(input.coverage?.layers) ? input.coverage.layers : [];
26
+ }
27
+
28
+ function configLayers(input) {
29
+ return Array.isArray(input.config?.layers) ? input.config.layers : [];
30
+ }
31
+
32
+ function emptyLayerSet(input) {
33
+ return new Set(Array.isArray(input.coverage?.emptyLayers) ? input.coverage.emptyLayers : []);
34
+ }
35
+
36
+ function totalGovernedFiles(input) {
37
+ const governed = input.coverage?.governed?.totalFiles;
38
+ if (Number.isFinite(governed) && governed >= 0) return governed;
39
+ const total = input.coverage?.totalFiles;
40
+ return Number.isFinite(total) && total >= 0 ? total : 0;
41
+ }
42
+
43
+ function hasDomainLogicInUiSmell(designSmells) {
44
+ return (Array.isArray(designSmells) ? designSmells : []).some(
45
+ (smell) => smell && smell.id === 'domain-logic-in-ui'
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Soft residual when Domain is declared but empty and the UI holds the rules.
51
+ * Never flips valid / goal.met.
52
+ *
53
+ * @param {{ config?: object, coverage?: object, designSmells?: object[] }} [input]
54
+ * @returns {{ kind: 'ui-logic' | 'presentation-bag', ask: string, nextAction: string, domainLayers: string[], presentationFiles: number } | null}
55
+ */
56
+ export function collectNoDomainFrontendResidual(input = {}) {
57
+ const totalFiles = totalGovernedFiles(input);
58
+ if (totalFiles <= 0) return null;
59
+
60
+ const declared = configLayers(input);
61
+ const domainLayers = declared.filter((layer) =>
62
+ isDomainRoleLayerName(layer.name, layer.intentPrefixes ?? [])
63
+ );
64
+ if (domainLayers.length === 0) return null;
65
+
66
+ const rows = layerRows(input);
67
+ const empty = emptyLayerSet(input);
68
+ const domainEmpty = domainLayers.every((layer) => {
69
+ const row = rows.find((entry) => entry.name === layer.name);
70
+ const files = Number(row?.files) || 0;
71
+ return files === 0 || empty.has(layer.name);
72
+ });
73
+ if (!domainEmpty) return null;
74
+
75
+ const presentationFiles = rows
76
+ .filter((row) => isPresentationRoleLayerName(row.name))
77
+ .reduce((sum, row) => sum + (Number(row.files) || 0), 0);
78
+ if (presentationFiles <= 0) return null;
79
+
80
+ const uiLogic = hasDomainLogicInUiSmell(input.designSmells);
81
+ const presentationShare = presentationFiles / totalFiles;
82
+ if (!uiLogic && (presentationShare < 0.5 || presentationFiles < 3)) return null;
83
+
84
+ return {
85
+ kind: uiLogic ? 'ui-logic' : 'presentation-bag',
86
+ ask: NO_DOMAIN_FRONTEND_ASK,
87
+ nextAction: NO_DOMAIN_FRONTEND_NEXT,
88
+ domainLayers: domainLayers.map((layer) => layer.name),
89
+ presentationFiles,
90
+ };
91
+ }
@@ -670,8 +670,7 @@ export const ARCHITECTURE_PRESETS = {
670
670
  },
671
671
  {
672
672
  name: 'ApplicationOrchestration',
673
- description:
674
- 'Use cases and services that coordinate the domain through ports. Next App Router API (`app/api/**`) and Pages API (`pages/api/**`) are orchestration shells, not UI.',
673
+ description: 'Use cases and services that coordinate the domain through ports.',
675
674
  patterns: [
676
675
  '**/application/**',
677
676
  '**/use-cases/**',
@@ -687,7 +686,7 @@ export const ARCHITECTURE_PRESETS = {
687
686
  {
688
687
  name: 'PresentationAdapters',
689
688
  description:
690
- 'Entrypoints — UI, framework app/pages dirs, controllers. Next `app/api` is Application, not this layer. Never bare lib/** (data clients are Persistence).',
689
+ 'Entrypoints — UI, pages, controllers when present. Never bare lib/** (data clients are Persistence).',
691
690
  patterns: [
692
691
  '**/app/**',
693
692
  '**/pages/**',
@@ -760,7 +759,7 @@ export const ARCHITECTURE_PRESETS = {
760
759
  {
761
760
  name: 'ApplicationOrchestration',
762
761
  description:
763
- 'Server actions, features, Next API routes (`app/api/**` / `pages/api/**`), and non-UI lib orchestration (when present).',
762
+ 'Server actions, features, and non-UI lib orchestration (when present).',
764
763
  patterns: [
765
764
  'src/features/**',
766
765
  'src/server/**',
@@ -10,6 +10,21 @@
10
10
  export const NORTH_STAR_ONE_LINE =
11
11
  'Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess.';
12
12
 
13
+ /**
14
+ * Compact doctor / details — only when `arkRules` is on.
15
+ * Absence stays silent (unlike ArkOrder, which speaks when off).
16
+ */
17
+ export const ARKRULES_ONE_BREATH =
18
+ 'Layers stop a bad import. ArkRules is optional policies inside one folder.';
19
+
20
+ /** Next step when the map is on and something still needs a human. */
21
+ export const ARKRULES_FIRST_CONTACT_NEXT =
22
+ 'Next: ark-check --rules-inventory — then /ark-adopt to write one rule, or /ark-autopilot to promote.';
23
+
24
+ /** Next step when Domain has code but invariants[] is empty. */
25
+ export const ARKRULES_EMPTY_CATALOG_NEXT =
26
+ 'Next: add 1–2 short phrases to invariants[] in the Domain rules file, then ark-check --doctor.';
27
+
13
28
  /** Status-light leftover-design qualifier (was “design-weak”). */
14
29
  export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
15
30
 
@@ -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':
@@ -1168,9 +1168,17 @@ export function resolveCandidateFacts({
1168
1168
  const arkOrderXiFieldWrites = [];
1169
1169
  const arkOrderIngestWritesXi = [];
1170
1170
  const arkOrderReleaseKeyCounts = [];
1171
- const xiKeys = [...(config.arkOrder?.xiKeys ?? [])];
1172
- const compositionRootPatterns = [...(config.arkRun?.compositionRoots ?? [])];
1173
- 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 ?? [])] : [];
1174
1182
 
1175
1183
  const seedPathSet = new Set(candidateFiles.map((file) => file.path));
1176
1184
  const ingest = (candidate, fullExtract) => {
@@ -1256,60 +1264,67 @@ export function resolveCandidateFacts({
1256
1264
  );
1257
1265
  // Class-shape extraction is text-conservative (false negatives over false positives).
1258
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).
1259
1268
  if (/\.(tsx?|mts|cts)$/i.test(candidate.path)) {
1260
- try {
1261
- classShapes.push(...extractClassShapesFromSource(candidate.path, candidate.content));
1262
- } catch {
1263
- // Never fail the resolver for shape extraction; sensors stay silent on this file.
1264
- }
1265
- try {
1266
- arkRunKernelCalls.push(
1267
- ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
1268
- );
1269
- } catch {
1270
- // Never fail the resolver for ArkRun call extraction.
1271
- }
1272
- try {
1273
- arkRunDeclarations.push(
1274
- ...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
1275
- );
1276
- } catch {
1277
- // Never fail the resolver for ArkRun declaration extraction.
1278
- }
1279
- try {
1280
- arkOrderPlaneCalls.push(
1281
- ...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
1282
- );
1283
- } catch {
1284
- // Never fail the resolver for ArkOrder factory extraction.
1285
- }
1286
- try {
1287
- arkOrderGenericUpdates.push(
1288
- ...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
1289
- );
1290
- } catch {
1291
- // Never fail the resolver for ArkOrder generic-update extraction.
1292
- }
1293
- try {
1294
- arkOrderXiFieldWrites.push(
1295
- ...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
1296
- );
1297
- } catch {
1298
- // Never fail the resolver for ArkOrder xi-field-write 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
+ }
1299
1275
  }
1300
- try {
1301
- arkOrderIngestWritesXi.push(
1302
- ...extractArkOrderIngestWritesXiFromSource(candidate.path, candidate.content)
1303
- );
1304
- } catch {
1305
- // Never fail the resolver for ArkOrder ingest-writes-ξ 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
+ }
1306
1291
  }
1307
- try {
1308
- arkOrderReleaseKeyCounts.push(
1309
- ...extractArkOrderReleaseKeyCountsFromSource(candidate.path, candidate.content)
1310
- );
1311
- } catch {
1312
- // Never fail the resolver for ArkOrder release key-count 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
+ }
1313
1328
  }
1314
1329
  }
1315
1330
  };
@@ -1355,18 +1370,20 @@ export function resolveCandidateFacts({
1355
1370
  ? candidateFiles.filter((file) => seedPathSet.has(file.path))
1356
1371
  : candidateFiles;
1357
1372
  const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
1358
- for (const candidate of extractCandidates) {
1359
- if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
1360
- try {
1361
- arkRunManagedNews.push(
1362
- ...extractArkRunManagedNewsFromSource(
1363
- candidate.path,
1364
- candidate.content,
1365
- admittedTypeNames
1366
- )
1367
- );
1368
- } catch {
1369
- // 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
+ }
1370
1387
  }
1371
1388
  }
1372
1389