arkgate 4.8.24 → 4.8.26

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 (166) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +2 -2
  3. package/bin/ark-check-runtime.mjs +17 -14
  4. package/bin/ark-layer-match.mjs +115 -13
  5. package/bin/ark-mcp-runtime.mjs +36 -31
  6. package/bin/ark-shared.mjs +4 -23
  7. package/bin/lib/adoption-stance.mjs +2 -2
  8. package/bin/lib/adr-path.mjs +3 -3
  9. package/bin/lib/agent-gates.mjs +0 -61
  10. package/bin/lib/agent-homes.mjs +12 -21
  11. package/bin/lib/agent-projection-command.mjs +4 -4
  12. package/bin/lib/ambient-state.mjs +9 -30
  13. package/bin/lib/analysis-engine.mjs +14 -14
  14. package/bin/lib/architecture-scan.mjs +11 -9
  15. package/bin/lib/ark-order-doctor.mjs +3 -28
  16. package/bin/lib/ark-order-facts.mjs +6 -99
  17. package/bin/lib/ark-order-types.mjs +1 -2
  18. package/bin/lib/ark-run-doctor.mjs +3 -28
  19. package/bin/lib/ark-run-facts.mjs +30 -40
  20. package/bin/lib/ark-run-sensors.mjs +1 -1
  21. package/bin/lib/arkrule-file-hints.mjs +2 -47
  22. package/bin/lib/arkrules-contract.mjs +124 -133
  23. package/bin/lib/arkrules-sensors.mjs +7 -5
  24. package/bin/lib/check-args.mjs +63 -1
  25. package/bin/lib/ci-and-commands.mjs +8 -72
  26. package/bin/lib/ci-merge-boundary.mjs +1 -1
  27. package/bin/lib/class-source-scan.mjs +5 -5
  28. package/bin/lib/clone-detection.mjs +474 -0
  29. package/bin/lib/codex-home.mjs +1 -1
  30. package/bin/lib/config-contract-slices.mjs +44 -6
  31. package/bin/lib/config-contract.mjs +40 -117
  32. package/bin/lib/config-extras.mjs +16 -22
  33. package/bin/lib/contract-smells.mjs +33 -15
  34. package/bin/lib/core-ratchet.mjs +0 -2
  35. package/bin/lib/dashboard-cli.mjs +2 -2
  36. package/bin/lib/deep-module-coach.mjs +2 -4
  37. package/bin/lib/deploy-path.mjs +13 -38
  38. package/bin/lib/design-smells.mjs +2 -2
  39. package/bin/lib/diagnostic-catalog.mjs +8 -1
  40. package/bin/lib/doctor-advisories.mjs +55 -9
  41. package/bin/lib/doctor-green-cite.mjs +1 -1
  42. package/bin/lib/doctor-human.mjs +1 -1
  43. package/bin/lib/doctor-next-actions.mjs +2 -2
  44. package/bin/lib/doctor-plan.mjs +4 -6
  45. package/bin/lib/duplication-io.mjs +572 -0
  46. package/bin/lib/effective-contract-load.mjs +181 -44
  47. package/bin/lib/entry-points-io.mjs +424 -0
  48. package/bin/lib/extra-merge-teeth.mjs +36 -0
  49. package/bin/lib/field-install.mjs +38 -14
  50. package/bin/lib/first-run-help.mjs +7 -3
  51. package/bin/lib/flat-parent-importers.mjs +5 -3
  52. package/bin/lib/gate-files.mjs +30 -89
  53. package/bin/lib/github-enforcement.mjs +165 -28
  54. package/bin/lib/golden-pattern.mjs +1 -1
  55. package/bin/lib/hook-templates.mjs +16 -16
  56. package/bin/lib/html-report-advisories.mjs +4 -0
  57. package/bin/lib/html-report-depth.mjs +2 -2
  58. package/bin/lib/html-report.mjs +2 -2
  59. package/bin/lib/import-graph-projection.mjs +112 -0
  60. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  61. package/bin/lib/invariant-coverage-io.mjs +4 -3
  62. package/bin/lib/invariant-coverage.mjs +26 -0
  63. package/bin/lib/invariant-probe-cli.mjs +347 -0
  64. package/bin/lib/invariant-probe-io.mjs +165 -0
  65. package/bin/lib/invariant-probe-runner.mjs +268 -0
  66. package/bin/lib/invariant-probe-sites.mjs +174 -0
  67. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  68. package/bin/lib/invariant-probe.mjs +461 -0
  69. package/bin/lib/invariant-tests-path.mjs +16 -22
  70. package/bin/lib/literal-path-drift.mjs +4 -2
  71. package/bin/lib/managed-upgrade.mjs +2 -8
  72. package/bin/lib/mcp-adoption.mjs +4 -5
  73. package/bin/lib/mcp-hook-payload.mjs +4 -4
  74. package/bin/lib/orphan-modules-io.mjs +458 -0
  75. package/bin/lib/orphan-modules.mjs +251 -0
  76. package/bin/lib/outside-importers.mjs +197 -0
  77. package/bin/lib/package-manager.mjs +3 -3
  78. package/bin/lib/parse-health.mjs +1 -1
  79. package/bin/lib/persistence-write-hint.mjs +33 -8
  80. package/bin/lib/pin-dependent-gates.mjs +2 -2
  81. package/bin/lib/policy-delta-io.mjs +60 -12
  82. package/bin/lib/post-green-path.mjs +1 -1
  83. package/bin/lib/prepare-change.mjs +3 -9
  84. package/bin/lib/prepare-write.mjs +1 -2
  85. package/bin/lib/presets.mjs +56 -68
  86. package/bin/lib/product-copy.mjs +1 -6
  87. package/bin/lib/project-root.mjs +43 -85
  88. package/bin/lib/resolved-candidate-facts.mjs +90 -62
  89. package/bin/lib/rules-under-contract.mjs +72 -8
  90. package/bin/lib/scan-files.mjs +4 -4
  91. package/bin/lib/schema-validation.mjs +130 -0
  92. package/bin/lib/sensor-promote-cli.mjs +2 -11
  93. package/bin/lib/sensor-promote-io.mjs +33 -22
  94. package/bin/lib/sensor-promotion.mjs +1 -1
  95. package/bin/lib/skill-catalog-apply.mjs +1 -1
  96. package/bin/lib/skill-install.mjs +42 -38
  97. package/bin/lib/snippet-analysis.mjs +1 -1
  98. package/bin/lib/start-install-recovery.mjs +1 -1
  99. package/bin/lib/start-preview.mjs +1 -1
  100. package/bin/lib/states-transitions-presence.mjs +3 -3
  101. package/bin/lib/status-command.mjs +2 -18
  102. package/bin/lib/status-manifest.mjs +18 -26
  103. package/bin/lib/status-transition-catalog.mjs +0 -6
  104. package/bin/lib/suggestions.mjs +3 -3
  105. package/bin/lib/team-parliament-io.mjs +3 -17
  106. package/bin/lib/team-parliament.mjs +15 -9
  107. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  108. package/bin/lib/upgrade-command.mjs +2 -24
  109. package/bin/lib/upgrade-outcome.mjs +1 -1
  110. package/bin/lib/upstream-report.mjs +21 -10
  111. package/bin/lib/violations.mjs +10 -9
  112. package/bin/lib/weakest-link.mjs +13 -0
  113. package/bin/lib/write-path-capabilities.mjs +2 -2
  114. package/dist/chunk-G3JRBJGH.js +16 -0
  115. package/dist/chunk-R3AJ5RSZ.cjs +16 -0
  116. package/dist/{configTypes-BJbaeoj4.d.cts → configTypes-GPvvdleM.d.cts} +12 -1
  117. package/dist/{configTypes-BJbaeoj4.d.ts → configTypes-GPvvdleM.d.ts} +12 -1
  118. package/dist/eslint/index.cjs +11 -11
  119. package/dist/eslint/index.d.cts +10 -1
  120. package/dist/eslint/index.d.ts +10 -1
  121. package/dist/eslint/index.js +11 -11
  122. package/dist/index.cjs +34 -34
  123. package/dist/index.d.cts +587 -515
  124. package/dist/index.d.ts +587 -515
  125. package/dist/index.js +34 -34
  126. package/dist/nestjs/index.cjs +1 -1
  127. package/dist/nestjs/index.d.cts +1 -1
  128. package/dist/nestjs/index.d.ts +1 -1
  129. package/dist/nestjs/index.js +1 -1
  130. package/dist/order/index.cjs +1 -1
  131. package/dist/order/index.js +1 -1
  132. package/dist/runtime/index.cjs +12 -12
  133. package/dist/runtime/index.d.cts +174 -136
  134. package/dist/runtime/index.d.ts +174 -136
  135. package/dist/runtime/index.js +12 -12
  136. package/dist/{types-DRinG-vT.d.cts → types-CnauMQQL.d.cts} +33 -25
  137. package/dist/{types-DRinG-vT.d.ts → types-CnauMQQL.d.ts} +33 -25
  138. package/docs/README.md +3 -1
  139. package/docs/agent-guide.md +140 -6
  140. package/docs/ai-gates.md +15 -1
  141. package/docs/configuration.md +40 -11
  142. package/docs/diagnostics.md +119 -3
  143. package/docs/package-surface.md +4 -1
  144. package/docs/product-voice.md +7 -1
  145. package/package.json +11 -2
  146. package/schemas/ark.config.schema.json +31 -3
  147. package/schemas/ark.invariant-probe.schema.json +368 -0
  148. package/server.json +2 -2
  149. package/templates/agent-skills/README.md +1 -1
  150. package/templates/agent-skills/ark-adopt/SKILL.md +1 -0
  151. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  152. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  153. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  154. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  155. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  156. package/templates/skills/ark-adopt.md +1 -0
  157. package/templates/skills/ark-contract.md +17 -0
  158. package/templates/skills/ark-coverage.md +2 -0
  159. package/templates/skills/ark-explore.md +7 -0
  160. package/templates/skills/ark-fix.md +20 -0
  161. package/templates/skills/ark-place.md +8 -0
  162. package/bin/lib/ark-order-error.mjs +0 -18
  163. package/bin/lib/ark-order-invariants.mjs +0 -388
  164. package/bin/lib/stable-hash.mjs +0 -90
  165. package/dist/chunk-MBXCFLO4.js +0 -16
  166. package/dist/chunk-RNQHRDCQ.cjs +0 -16
@@ -58,7 +58,7 @@ import {
58
58
  normalize,
59
59
  } from './scan-files.mjs';
60
60
 
61
- export const RESOLVED_FACTS_RESOLVER_IDENTITY = 'arkgate-typescript-resolver@1';
61
+ const RESOLVED_FACTS_RESOLVER_IDENTITY = 'arkgate-typescript-resolver@1';
62
62
 
63
63
  const IN_MEMORY_STORES = new Set([
64
64
  'InMemoryAuditStore',
@@ -995,7 +995,8 @@ function objectLiteralHasProperty(ts, object, name) {
995
995
  );
996
996
  }
997
997
 
998
- function tsSuppressionPositions(sourceFile, source) {
998
+ /** Start offsets of `@ts-ignore` directives and `@ts-nocheck` pragmas in a parsed file. */
999
+ export function tsSuppressionPositions(sourceFile, source) {
999
1000
  const positions = new Set();
1000
1001
  for (const directive of sourceFile.commentDirectives ?? []) {
1001
1002
  const start = directive.range?.pos;
@@ -1012,28 +1013,21 @@ function tsSuppressionPositions(sourceFile, source) {
1012
1013
  return [...positions];
1013
1014
  }
1014
1015
 
1015
- function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
1016
- const facts = tsSuppressionPositions(sourceFile, source).map((position) => ({
1017
- file: relativePath,
1018
- line: lineOf(sourceFile, position),
1019
- kind: 'ts-suppression',
1020
- }));
1021
- for (const dependency of dependencies) {
1022
- if (!dependency.unresolved) continue;
1023
- facts.push({
1024
- file: relativePath,
1025
- line: dependency.line,
1026
- kind: dependency.kind === 'require' ? 'dynamic-require' : 'dynamic-import',
1027
- });
1028
- }
1016
+ const ARKGATE_RUNTIME_SPECIFIER = /^arkgate(?:\/runtime)?$/;
1029
1017
 
1018
+ /**
1019
+ * Local bindings of the arkgate kernel factories whose omitted options fall back
1020
+ * to in-memory stores: named imports (local name → factory) plus namespace
1021
+ * imports of `arkgate` / `arkgate/runtime`.
1022
+ */
1023
+ export function arkInMemoryFactoryBindings(ts, sourceFile) {
1030
1024
  const importedFactories = new Map();
1031
1025
  const arkNamespaces = new Set();
1032
1026
  for (const statement of sourceFile.statements) {
1033
1027
  if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) {
1034
1028
  continue;
1035
1029
  }
1036
- if (!/^arkgate(?:\/runtime)?$/.test(statement.moduleSpecifier.text)) continue;
1030
+ if (!ARKGATE_RUNTIME_SPECIFIER.test(statement.moduleSpecifier.text)) continue;
1037
1031
  const bindings = statement.importClause?.namedBindings;
1038
1032
  if (bindings && ts.isNamespaceImport(bindings)) arkNamespaces.add(bindings.name.text);
1039
1033
  if (!bindings || !ts.isNamedImports(bindings)) continue;
@@ -1043,6 +1037,71 @@ function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
1043
1037
  if (requirements) importedFactories.set(element.name.text, { imported, requirements });
1044
1038
  }
1045
1039
  }
1040
+ return { importedFactories, arkNamespaces };
1041
+ }
1042
+
1043
+ /** Named `InMemory*Store` imports of an arkgate import declaration. */
1044
+ export function arkInMemoryStoreImports(ts, node) {
1045
+ const specifier = node.moduleSpecifier;
1046
+ if (!ts.isStringLiteralLike(specifier) || !ARKGATE_RUNTIME_SPECIFIER.test(specifier.text)) {
1047
+ return [];
1048
+ }
1049
+ const elements =
1050
+ node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
1051
+ ? node.importClause.namedBindings.elements
1052
+ : [];
1053
+ const out = [];
1054
+ for (const element of elements) {
1055
+ const imported = element.propertyName?.text ?? element.name.text;
1056
+ if (IN_MEMORY_STORES.has(imported)) out.push({ element, imported });
1057
+ }
1058
+ return out;
1059
+ }
1060
+
1061
+ /**
1062
+ * The factory a call expression invokes when that call definitely takes the
1063
+ * in-memory defaults (no options, `undefined`, or an object literal missing a
1064
+ * required store); otherwise undefined.
1065
+ */
1066
+ export function inMemoryDefaultsFactoryCall(ts, node, { importedFactories, arkNamespaces }) {
1067
+ let factory;
1068
+ if (ts.isIdentifier(node.expression)) {
1069
+ factory = importedFactories.get(node.expression.text);
1070
+ } else if (
1071
+ ts.isPropertyAccessExpression(node.expression) &&
1072
+ ts.isIdentifier(node.expression.expression) &&
1073
+ arkNamespaces.has(node.expression.expression.text)
1074
+ ) {
1075
+ const imported = node.expression.name.text;
1076
+ const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
1077
+ if (requirements) factory = { imported, requirements };
1078
+ }
1079
+ if (!factory) return undefined;
1080
+ const options = node.arguments[0];
1081
+ const definitelyDefaults =
1082
+ !options ||
1083
+ (ts.isIdentifier(options) && options.text === 'undefined') ||
1084
+ (ts.isObjectLiteralExpression(options) &&
1085
+ factory.requirements.some((name) => !objectLiteralHasProperty(ts, options, name)));
1086
+ return definitelyDefaults ? factory : undefined;
1087
+ }
1088
+
1089
+ function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
1090
+ const facts = tsSuppressionPositions(sourceFile, source).map((position) => ({
1091
+ file: relativePath,
1092
+ line: lineOf(sourceFile, position),
1093
+ kind: 'ts-suppression',
1094
+ }));
1095
+ for (const dependency of dependencies) {
1096
+ if (!dependency.unresolved) continue;
1097
+ facts.push({
1098
+ file: relativePath,
1099
+ line: dependency.line,
1100
+ kind: dependency.kind === 'require' ? 'dynamic-require' : 'dynamic-import',
1101
+ });
1102
+ }
1103
+
1104
+ const factoryBindings = arkInMemoryFactoryBindings(ts, sourceFile);
1046
1105
 
1047
1106
  const visit = (node) => {
1048
1107
  if (
@@ -1057,56 +1116,25 @@ function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
1057
1116
  }
1058
1117
 
1059
1118
  if (ts.isImportDeclaration(node)) {
1060
- const specifier = node.moduleSpecifier;
1061
- const fromArk =
1062
- ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
1063
- if (fromArk) {
1064
- const elements =
1065
- node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
1066
- ? node.importClause.namedBindings.elements
1067
- : [];
1068
- for (const element of elements) {
1069
- const imported = element.propertyName?.text ?? element.name.text;
1070
- if (IN_MEMORY_STORES.has(imported)) {
1071
- facts.push({
1072
- file: relativePath,
1073
- line: lineOf(sourceFile, element.getStart(sourceFile)),
1074
- kind: 'in-memory-store',
1075
- symbol: imported,
1076
- });
1077
- }
1078
- }
1119
+ for (const { element, imported } of arkInMemoryStoreImports(ts, node)) {
1120
+ facts.push({
1121
+ file: relativePath,
1122
+ line: lineOf(sourceFile, element.getStart(sourceFile)),
1123
+ kind: 'in-memory-store',
1124
+ symbol: imported,
1125
+ });
1079
1126
  }
1080
1127
  }
1081
1128
 
1082
1129
  if (ts.isCallExpression(node)) {
1083
- let factory;
1084
- if (ts.isIdentifier(node.expression)) {
1085
- factory = importedFactories.get(node.expression.text);
1086
- } else if (
1087
- ts.isPropertyAccessExpression(node.expression) &&
1088
- ts.isIdentifier(node.expression.expression) &&
1089
- arkNamespaces.has(node.expression.expression.text)
1090
- ) {
1091
- const imported = node.expression.name.text;
1092
- const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
1093
- if (requirements) factory = { imported, requirements };
1094
- }
1130
+ const factory = inMemoryDefaultsFactoryCall(ts, node, factoryBindings);
1095
1131
  if (factory) {
1096
- const options = node.arguments[0];
1097
- const definitelyDefaults =
1098
- !options ||
1099
- (ts.isIdentifier(options) && options.text === 'undefined') ||
1100
- (ts.isObjectLiteralExpression(options) &&
1101
- factory.requirements.some((name) => !objectLiteralHasProperty(ts, options, name)));
1102
- if (definitelyDefaults) {
1103
- facts.push({
1104
- file: relativePath,
1105
- line: lineOf(sourceFile, node.getStart(sourceFile)),
1106
- kind: 'in-memory-store',
1107
- symbol: `${factory.imported} defaults`,
1108
- });
1109
- }
1132
+ facts.push({
1133
+ file: relativePath,
1134
+ line: lineOf(sourceFile, node.getStart(sourceFile)),
1135
+ kind: 'in-memory-store',
1136
+ symbol: `${factory.imported} defaults`,
1137
+ });
1110
1138
  }
1111
1139
  }
1112
1140
  ts.forEachChild(node, visit);
@@ -16,8 +16,8 @@ import {
16
16
  invariantIdsFromCatalog,
17
17
  loadInvariantCoverageInputs,
18
18
  } from './invariant-coverage-io.mjs';
19
+ import { formatProbeStatusLines, probeStatusSection } from './invariant-probe-io.mjs';
19
20
  import {
20
- EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
21
21
  composeMergePlanesHonesty,
22
22
  demoteExtraPlaneTeethUnderClassificationFloor,
23
23
  } from './extra-merge-teeth.mjs';
@@ -39,8 +39,6 @@ const COVERED_SAMPLE_MAX = 24;
39
39
  const STRUCTURE_CATALOG_MAX = 40;
40
40
  const UNCOVERED_CATALOG_MAX = 30;
41
41
 
42
- export { EXTRA_MERGE_TEETH_GOVERNED_FLOOR };
43
-
44
42
  /** Doctor walk often hands absolute paths; layer globs are project-relative. */
45
43
  function projectRelativePath(root, filePath) {
46
44
  const posix = String(filePath).replace(/\\/g, '/');
@@ -100,6 +98,39 @@ function arkOrderMergeInput(config, residualCount = 0) {
100
98
  };
101
99
  }
102
100
 
101
+ function rulesBySlice(arkRules) {
102
+ const groups = new Map();
103
+ const rows = [...(arkRules?.structure ?? []), ...(arkRules?.invariants ?? [])];
104
+ for (const entry of rows) {
105
+ const childId = entry?.provenance?.childId;
106
+ const sourceFile = entry?.provenance?.sourceFile;
107
+ if (typeof childId !== 'string' || typeof sourceFile !== 'string') continue;
108
+ const key = `${childId}\0${sourceFile}`;
109
+ let group = groups.get(key);
110
+ if (!group) {
111
+ group = {
112
+ slice: childId,
113
+ layer: entry.provenance?.layer ?? null,
114
+ sourceFile,
115
+ ids: [],
116
+ appliesTo: [],
117
+ };
118
+ groups.set(key, group);
119
+ }
120
+ if (typeof entry.id === 'string' && !group.ids.includes(entry.id)) group.ids.push(entry.id);
121
+ for (const pattern of entry.appliesTo ?? []) {
122
+ if (typeof pattern === 'string' && !group.appliesTo.includes(pattern)) group.appliesTo.push(pattern);
123
+ }
124
+ }
125
+ return [...groups.values()]
126
+ .map((group) => ({
127
+ ...group,
128
+ ids: [...group.ids].sort(),
129
+ appliesTo: [...group.appliesTo].sort(),
130
+ }))
131
+ .sort((left, right) => left.slice.localeCompare(right.slice) || left.sourceFile.localeCompare(right.sourceFile));
132
+ }
133
+
103
134
  /** ADR 0012 D2 drift: arkrules/*.json no arkRules entry references (advisory). */
104
135
  function unreferencedArkRulesFiles(loaded) {
105
136
  const files = (loaded?.warnings ?? []).map((w) => w.path).filter(Boolean);
@@ -110,11 +141,32 @@ function unreferencedArkRulesFiles(loaded) {
110
141
  * @param {{ rulesMigration?: boolean }} [options] rulesMigration:false skips the
111
142
  * migration counts (the inventory payload computes them itself).
112
143
  */
144
+ function governedPathsFromFacts(root, facts) {
145
+ const rows = facts?.files;
146
+ if (!Array.isArray(rows) || rows.length === 0) return undefined;
147
+ const base = String(root).replace(/\\/g, '/').replace(/\/+$/, '');
148
+ const prefix = `${base}/`;
149
+ const out = [];
150
+ for (const entry of rows) {
151
+ const raw = typeof entry === 'string' ? entry : entry?.path;
152
+ if (typeof raw !== 'string' || raw.length === 0) continue;
153
+ const norm = raw.replace(/\\/g, '/');
154
+ const rel = norm.startsWith(prefix)
155
+ ? norm.slice(prefix.length)
156
+ : norm.replace(/^\.\//, '');
157
+ if (!rel || rel.startsWith('..') || rel.startsWith('/')) continue;
158
+ out.push(rel);
159
+ }
160
+ return out.length > 0 ? out : undefined;
161
+ }
162
+
113
163
  export function summarizeRulesUnderContract(root, config, facts, classification, options = {}) {
164
+ const governedFiles = governedPathsFromFacts(root, facts);
165
+ const loadOpts = governedFiles ? { files: governedFiles } : {};
114
166
  if (!config?.arkRules || Object.keys(config.arkRules).length === 0) {
115
167
  let drift = {};
116
168
  try {
117
- drift = unreferencedArkRulesFiles(loadEffectiveArkRulesFromDisk(root, config));
169
+ drift = unreferencedArkRulesFiles(loadEffectiveArkRulesFromDisk(root, config, loadOpts));
118
170
  } catch {
119
171
  drift = {};
120
172
  }
@@ -136,7 +188,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification,
136
188
  };
137
189
  }
138
190
  try {
139
- const loaded = loadEffectiveArkRulesFromDisk(root, config);
191
+ const loaded = loadEffectiveArkRulesFromDisk(root, config, loadOpts);
140
192
  if (loaded.errors?.length) {
141
193
  return {
142
194
  active: true,
@@ -167,6 +219,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification,
167
219
  const covById = new Map(
168
220
  (coverage.coverage ?? []).map((row) => [row.invariantId, row])
169
221
  );
222
+ const probeSection = probeStatusSection(root, loaded.arkRules.invariants);
170
223
  const byLayer = loaded.arkRules.byLayer ?? {};
171
224
  const layers = Object.keys(byLayer)
172
225
  .sort((a, b) => a.localeCompare(b))
@@ -180,6 +233,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification,
180
233
  return {
181
234
  name,
182
235
  sourceFile: part.sourceFile ?? null,
236
+ ...(Array.isArray(part.sourceFiles) ? { sourceFiles: [...part.sourceFiles] } : {}),
183
237
  structureRules: (part.structure ?? []).length,
184
238
  invariants: layerInvariants.length,
185
239
  coveredInvariants: covered,
@@ -280,6 +334,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification,
280
334
  partialCoverage: coverage.partial,
281
335
  testFilesScanned: coverageInputs.testFiles.length,
282
336
  layers,
337
+ ...(rulesBySlice(loaded.arkRules).length > 0 ? { bySlice: rulesBySlice(loaded.arkRules) } : {}),
283
338
  structure,
284
339
  structureTruncated,
285
340
  uncovered,
@@ -296,6 +351,8 @@ export function summarizeRulesUnderContract(root, config, facts, classification,
296
351
  .rulesMigration,
297
352
  }),
298
353
  ...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
354
+ // ADR 0039: only when a probe artifact exists — absence stays silent.
355
+ ...(probeSection ? { probe: probeSection } : {}),
299
356
  mergePlanes,
300
357
  notAScore: true,
301
358
  note: 'ArkRules plane (intra-layer) — counts and catalog, never a score. Green with uncovered residual must say so. Structure sensors are heuristics; invariants are catalog+coverage evidence, not a business runtime.',
@@ -335,6 +392,7 @@ export function formatArkRulesEvidenceLines(section) {
335
392
  }
336
393
  const discard = formatCoverageDiscards(section.coverageStats);
337
394
  if (discard) lines.push(`ArkRules:${discard}`);
395
+ lines.push(...formatProbeStatusLines(section.probe));
338
396
  return lines;
339
397
  }
340
398
 
@@ -621,7 +679,7 @@ export function formatRulesUnderContractHtml(section, esc) {
621
679
  </section>`;
622
680
  }
623
681
 
624
- export const RULES_INVENTORY_MAX_FILES = 400;
682
+ const RULES_INVENTORY_MAX_FILES = 400;
625
683
 
626
684
  /**
627
685
  * Brownfield rules migration (AR13/AR15): one code path for `--rules-inventory`,
@@ -633,7 +691,7 @@ export const RULES_INVENTORY_MAX_FILES = 400;
633
691
  * @param {{ root: string, config: Record<string, any>, files?: string[], arkRules?: any }} input
634
692
  * `files` defaults to the governed walk; `arkRules` to the catalog on disk.
635
693
  */
636
- export function buildRulesMigration({ root, config, files, arkRules }) {
694
+ function buildRulesMigration({ root, config, files, arkRules }) {
637
695
  const governed = Array.isArray(files) ? files : collectGovernedFiles(root, config);
638
696
  const fileContents = {};
639
697
  const fileLayers = {};
@@ -651,7 +709,12 @@ export function buildRulesMigration({ root, config, files, arkRules }) {
651
709
  let catalog = arkRules;
652
710
  if (catalog === undefined && config?.arkRules) {
653
711
  try {
654
- const loaded = loadEffectiveArkRulesFromDisk(root, config);
712
+ const loaded = loadEffectiveArkRulesFromDisk(root, config, {
713
+ files: governed.map((file) => {
714
+ const absolute = path.isAbsolute(file) ? file : path.resolve(root, file);
715
+ return path.relative(root, absolute).split(path.sep).join('/');
716
+ }),
717
+ });
655
718
  catalog = loaded.errors.length > 0 ? null : loaded.arkRules;
656
719
  } catch {
657
720
  catalog = null; // the advisory inventory is still useful
@@ -730,6 +793,7 @@ export function buildRulesInventoryPayload(root, config, files) {
730
793
  coverageEvidence = {
731
794
  symbolEvidence: section.symbolEvidence ?? [],
732
795
  discarded: section.coverageStats?.discarded ?? null,
796
+ ...(section.probe ? { probe: section.probe } : {}),
733
797
  };
734
798
  }
735
799
  } catch {
@@ -8,12 +8,12 @@ import { isScanExcludedRelative, isSkippedSourceDir } from '../ark-shared.mjs';
8
8
 
9
9
  export { isSkippedSourceDir };
10
10
 
11
- export const SOURCE_FILE_NAME = /\.[cm]?[tj]sx?$/;
11
+ const SOURCE_FILE_NAME = /\.[cm]?[tj]sx?$/;
12
12
 
13
13
  /** Unit/e2e test files are not architecture surface — agents and Nest put them next
14
14
  * to production code (*.spec.ts). Counting them as ungoverned forces false
15
15
  * CONFIG_UNCLASSIFIED_FILES under --strict-config on every starter. */
16
- export const TEST_FILE_NAME =
16
+ const TEST_FILE_NAME =
17
17
  /(^test(?:[-_.]).*|\.(spec|test)(?:-d)?\.)(tsx?|jsx?|mjsx?|cjsx?|mts|cts)$/i;
18
18
 
19
19
  export function isGovernableSourceFile(name) {
@@ -123,7 +123,7 @@ export function walk(dir, files = [], options = {}) {
123
123
  }
124
124
 
125
125
  /** Walk include roots then drop codegen / config.exclude (universal scan filter). */
126
- export function isIncludeMatch(relativePath, include) {
126
+ function isIncludeMatch(relativePath, include) {
127
127
  return (include ?? []).some((entry) => {
128
128
  const includeRoot = String(entry)
129
129
  .replace(/\\/g, '/')
@@ -192,7 +192,7 @@ export function normalize(value) {
192
192
  export const UNGOVERNED_PROBE_CAP = 200;
193
193
 
194
194
  /** Tooling configs (vite.config.ts, eslint.config.js …) are not the product source a contract governs. */
195
- export const TOOLING_CONFIG_FILE_NAME = /\.config\.[cm]?[jt]sx?$/i;
195
+ const TOOLING_CONFIG_FILE_NAME = /\.config\.[cm]?[jt]sx?$/i;
196
196
 
197
197
  /**
198
198
  * Count governable source files under `root` that the contract's own scope cannot hide.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/schemaValidation.ts
5
+ * Regenerate: node scripts/generate-cli-pure.mjs
6
+ * Drift check: node scripts/generate-cli-pure.mjs --check
7
+ *
8
+ * Pure CLI helper (bin/lib/schema-validation.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export function isObject(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+ export function propertyPath(parent, key) {
15
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
16
+ ? `${parent}.${key}`
17
+ : `${parent}[${JSON.stringify(key)}]`;
18
+ }
19
+ export function valueType(value) {
20
+ if (value === null)
21
+ return 'null';
22
+ if (Array.isArray(value))
23
+ return 'array';
24
+ return typeof value;
25
+ }
26
+ function resolveSchemaRef(ref, root) {
27
+ const prefix = '#/$defs/';
28
+ if (!ref.startsWith(prefix))
29
+ return undefined;
30
+ return root.$defs[ref.slice(prefix.length)];
31
+ }
32
+ /** Walk `value` against `schema`, appending every finding to `issues`. */
33
+ export function validateSchemaNode(value, schema, path, root, issues) {
34
+ if (schema.$ref) {
35
+ const referenced = resolveSchemaRef(schema.$ref, root);
36
+ if (!referenced) {
37
+ issues.push({ path, message: `schema reference ${schema.$ref} cannot be resolved` });
38
+ return;
39
+ }
40
+ validateSchemaNode(value, referenced, path, root, issues);
41
+ return;
42
+ }
43
+ if (schema.const !== undefined && !Object.is(value, schema.const)) {
44
+ issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
45
+ return;
46
+ }
47
+ if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
48
+ issues.push({ path, message: `must be one of ${schema.enum.map(String).join(', ')}` });
49
+ return;
50
+ }
51
+ if (schema.type === 'object') {
52
+ if (!isObject(value)) {
53
+ issues.push({ path, message: `must be an object; received ${valueType(value)}` });
54
+ return;
55
+ }
56
+ const properties = schema.properties ?? {};
57
+ for (const key of schema.required ?? []) {
58
+ if (value[key] === undefined) {
59
+ issues.push({ path: propertyPath(path, key), message: 'is required' });
60
+ }
61
+ }
62
+ if (schema.additionalProperties === false) {
63
+ for (const key of Object.keys(value)) {
64
+ if (!(key in properties)) {
65
+ issues.push({ path: propertyPath(path, key), message: 'unknown field' });
66
+ }
67
+ }
68
+ }
69
+ else if (schema.additionalProperties !== undefined &&
70
+ schema.additionalProperties !== true &&
71
+ typeof schema.additionalProperties === 'object') {
72
+ const additional = schema.additionalProperties;
73
+ for (const key of Object.keys(value)) {
74
+ if (!(key in properties)) {
75
+ validateSchemaNode(value[key], additional, propertyPath(path, key), root, issues);
76
+ }
77
+ }
78
+ }
79
+ for (const [key, childSchema] of Object.entries(properties)) {
80
+ if (value[key] !== undefined) {
81
+ validateSchemaNode(value[key], childSchema, propertyPath(path, key), root, issues);
82
+ }
83
+ }
84
+ return;
85
+ }
86
+ if (schema.type === 'array') {
87
+ if (!Array.isArray(value)) {
88
+ issues.push({ path, message: `must be an array; received ${valueType(value)}` });
89
+ return;
90
+ }
91
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
92
+ issues.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
93
+ }
94
+ if (schema.uniqueItems) {
95
+ const serialized = value.map((entry) => JSON.stringify(entry));
96
+ if (new Set(serialized).size !== serialized.length) {
97
+ issues.push({ path, message: 'must not contain duplicate items' });
98
+ }
99
+ }
100
+ if (schema.items) {
101
+ value.forEach((entry, index) => validateSchemaNode(entry, schema.items, `${path}[${index}]`, root, issues));
102
+ }
103
+ return;
104
+ }
105
+ if (schema.type === 'string') {
106
+ if (typeof value !== 'string') {
107
+ issues.push({ path, message: `must be a string; received ${valueType(value)}` });
108
+ return;
109
+ }
110
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
111
+ issues.push({ path, message: `must contain at least ${schema.minLength} character(s)` });
112
+ }
113
+ return;
114
+ }
115
+ if (schema.type === 'boolean') {
116
+ if (typeof value !== 'boolean') {
117
+ issues.push({ path, message: `must be a boolean; received ${valueType(value)}` });
118
+ }
119
+ return;
120
+ }
121
+ if (schema.type === 'integer') {
122
+ if (!Number.isInteger(value)) {
123
+ issues.push({ path, message: `must be an integer; received ${valueType(value)}` });
124
+ return;
125
+ }
126
+ if (schema.minimum !== undefined && value < schema.minimum) {
127
+ issues.push({ path, message: `must be at least ${schema.minimum}` });
128
+ }
129
+ }
130
+ }
@@ -11,18 +11,9 @@ import path from 'node:path';
11
11
 
12
12
  import { arkCommand } from '../ark-shared.mjs';
13
13
  import { collectGovernedFiles, normalize } from './scan-files.mjs';
14
+ // Same palette and TTY test as the entry's stderr output.
15
+ import { color } from './violations.mjs';
14
16
 
15
- // Same shape and the same TTY test as the entry's own `color`: this module is
16
- // the only other writer to that terminal, and a second copy is cheaper than
17
- // exporting the entry's internals into a cycle.
18
- const useColor = process.stderr.isTTY && !process.env.NO_COLOR;
19
- const color = {
20
- red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s),
21
- yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
22
- green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
23
- dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
24
- bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
25
- };
26
17
 
27
18
  /**
28
19
  * A rule id or a source path comes out of the project's own ArkRules JSON and
@@ -16,6 +16,7 @@ import {
16
16
  invariantIdsFromCatalog,
17
17
  loadInvariantCoverageInputs,
18
18
  } from './invariant-coverage-io.mjs';
19
+ import { attachProbeEvidence } from './invariant-probe-io.mjs';
19
20
 
20
21
  /**
21
22
  * The root as the filesystem sees it. Comparing a realpath against a lexical
@@ -79,28 +80,11 @@ export function loadSensorMap(root, config, facts) {
79
80
  }));
80
81
 
81
82
  const catalogued = loaded.arkRules.invariants ?? [];
82
- let coverageRows = [];
83
- let partial = false;
84
- if (catalogued.length > 0) {
85
- const inputs = loadInvariantCoverageInputs(root, facts ?? { files: [] }, {
86
- invariantIds: invariantIdsFromCatalog(loaded.arkRules),
87
- ...coverageOptionsFromConfig(config),
88
- });
89
- const coverage = evaluateInvariantCoverage({
90
- arkRules: loaded.arkRules,
91
- fileContents: inputs.fileContents,
92
- testFiles: inputs.testFiles,
93
- testGlobsMissing: inputs.testGlobsMissing,
94
- coverageBudgetExhausted: inputs.coverageBudgetExhausted === true,
95
- ...(inputs.stats ? { coverageStats: inputs.stats } : {}),
96
- // The declared roots decide `outsideDeclaredRoots`, which is one of the
97
- // four reasons canPromoteInvariant refuses. Dropping them here would make
98
- // this surface promise a promotion the gate then denies.
99
- ...(inputs.coverageRoots ? { coverageRoots: inputs.coverageRoots } : {}),
100
- });
101
- coverageRows = coverage.coverage ?? [];
102
- partial = coverage.partial === true;
103
- }
83
+ const evaluated = evaluateCatalogCoverage(root, config, facts, loaded.arkRules);
84
+ // ADR 0039: a committed probe artifact can only subtract promotability.
85
+ // Read-only here — this path never runs a test.
86
+ const coverageRows = attachProbeEvidence(root, evaluated.rows, catalogued).rows;
87
+ const partial = evaluated.partial;
104
88
  const evidenceById = new Map(coverageRows.map((row) => [row.invariantId, row]));
105
89
 
106
90
  const invariants = catalogued.map((entry) => ({
@@ -120,6 +104,33 @@ export function loadSensorMap(root, config, facts) {
120
104
  };
121
105
  }
122
106
 
107
+ /**
108
+ * Invariant coverage rows for a loaded catalog, evaluated the way the
109
+ * promotion judge needs them (declared `coverageRoots` included). `inputs` is
110
+ * the loaded file set, reused by the probe to pick covering tests.
111
+ */
112
+ export function evaluateCatalogCoverage(root, config, facts, arkRules) {
113
+ const catalogued = arkRules?.invariants ?? [];
114
+ if (catalogued.length === 0) return { rows: [], partial: false, inputs: null };
115
+ const inputs = loadInvariantCoverageInputs(root, facts ?? { files: [] }, {
116
+ invariantIds: invariantIdsFromCatalog(arkRules),
117
+ ...coverageOptionsFromConfig(config),
118
+ });
119
+ const coverage = evaluateInvariantCoverage({
120
+ arkRules,
121
+ fileContents: inputs.fileContents,
122
+ testFiles: inputs.testFiles,
123
+ testGlobsMissing: inputs.testGlobsMissing,
124
+ coverageBudgetExhausted: inputs.coverageBudgetExhausted === true,
125
+ ...(inputs.stats ? { coverageStats: inputs.stats } : {}),
126
+ // The declared roots decide `outsideDeclaredRoots`, which is one of the
127
+ // four reasons canPromoteInvariant refuses. Dropping them here would make
128
+ // this surface promise a promotion the gate then denies.
129
+ ...(inputs.coverageRoots ? { coverageRoots: inputs.coverageRoots } : {}),
130
+ });
131
+ return { rows: coverage.coverage ?? [], partial: coverage.partial === true, inputs };
132
+ }
133
+
123
134
  /**
124
135
  * Count the findings each declared rule produced in an analysis that already
125
136
  * ran. One run answers for every rule: the loop this replaces was one full
@@ -188,7 +188,7 @@ export function buildSensorMap(input) {
188
188
  sourceFile,
189
189
  description: nullable(rule.description),
190
190
  promotable: verdict.ok,
191
- blocker: verdict.ok ? null : 'no-coverage-evidence',
191
+ blocker: verdict.ok ? null : verdict.blocker ?? 'no-coverage-evidence',
192
192
  reason: `${locate(rule.id, sourceFile)}: ${verdict.reason}`,
193
193
  coverageEvaluated: rule.coverage != null,
194
194
  ...ambiguity(rule.id),
@@ -19,7 +19,7 @@ function skillName(skill) {
19
19
  return Array.isArray(skill) ? skill[0] : skill?.name || skill;
20
20
  }
21
21
 
22
- export function projectCatalogReady(root, skillNames) {
22
+ function projectCatalogReady(root, skillNames) {
23
23
  return skillNames.some((name) => fs.existsSync(path.join(root, canonicalSkillPath(name))));
24
24
  }
25
25