arkgate 4.8.24 → 4.8.25

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 +85 -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 +55 -8
  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 +31 -62
  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-AHACEKSV.js +16 -0
  115. package/dist/chunk-UN5EH4QV.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 +2 -1
  139. package/docs/agent-guide.md +134 -2
  140. package/docs/ai-gates.md +15 -1
  141. package/docs/configuration.md +40 -11
  142. package/docs/diagnostics.md +118 -2
  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
@@ -11,8 +11,12 @@ import path from 'node:path';
11
11
  import {
12
12
  emptyEffectiveArkRules,
13
13
  buildEffectiveArkRules,
14
+ duplicateArkRuleIds,
14
15
  loadArkRulesContract,
16
+ sliceScopeEscapes,
15
17
  } from './arkrules-contract.mjs';
18
+ import { resolveSliceRulePlan } from '../ark-layer-match.mjs';
19
+ import { collectGovernedFiles } from './scan-files.mjs';
16
20
 
17
21
  export function normalizeProjectRelativePath(value) {
18
22
  const normalized = value.replace(/\\/g, '/');
@@ -33,7 +37,7 @@ export function normalizeProjectRelativePath(value) {
33
37
  return segments.length > 0 ? segments.join('/') : undefined;
34
38
  }
35
39
 
36
- export function isWithinRoot(root, candidate) {
40
+ function isWithinRoot(root, candidate) {
37
41
  const relative = path.relative(root, candidate);
38
42
  return (
39
43
  relative === '' ||
@@ -56,60 +60,124 @@ export function isWithinRoot(root, candidate) {
56
60
  * from the source counts as an empty layer (a base that predates the file), not an error.
57
61
  * @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, errors: Array<{path:string,message:string}>, referenced: Set<string> }}
58
62
  */
63
+ function declaredLayerNames(config) {
64
+ return new Set(Array.isArray(config?.layers) ? config.layers.map((layer) => layer?.name).filter(Boolean) : []);
65
+ }
66
+
67
+ function arkRulesRefList(value) {
68
+ if (typeof value === 'string') return value.length > 0 ? [value] : null;
69
+ if (Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === 'string' && entry.length > 0)) {
70
+ return value;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function pushReadError(errors, pathKey, rel, error) {
76
+ errors.push({
77
+ path: pathKey,
78
+ message:
79
+ error instanceof Error
80
+ ? error.message
81
+ : `referenced ArkRules file ${JSON.stringify(rel)} failed to load`,
82
+ });
83
+ }
84
+
59
85
  export function loadEffectiveArkRules(config, readRelative, opts = {}) {
60
86
  const refs = config?.arkRules;
61
87
  const referenced = new Set();
62
- if (!refs || typeof refs !== 'object' || Object.keys(refs).length === 0) {
63
- return { arkRules: emptyEffectiveArkRules(), errors: [], referenced };
64
- }
65
- const layerNames = new Set(
66
- Array.isArray(config.layers) ? config.layers.map((layer) => layer.name) : []
67
- );
88
+ const layerNames = declaredLayerNames(config);
68
89
  const errors = [];
69
90
  const parts = [];
70
- for (const layer of Object.keys(refs).sort()) {
71
- const relRaw = refs[layer];
72
- const pathKey = `$.arkRules[${JSON.stringify(layer)}]`;
73
- if (typeof relRaw !== 'string' || relRaw.length === 0) {
74
- errors.push({ path: pathKey, message: 'must be a non-empty relative path string' });
75
- continue;
76
- }
77
- const rel = normalizeProjectRelativePath(relRaw);
78
- if (!rel) {
79
- errors.push({
80
- path: pathKey,
81
- message:
82
- 'must be a project-relative path without absolute roots or parent-directory traversal',
83
- });
84
- continue;
85
- }
86
- if (!layerNames.has(layer)) {
87
- errors.push({
88
- path: pathKey,
89
- message: `layer ${JSON.stringify(layer)} is not declared in layers[]`,
91
+ const hasRefs = Boolean(refs && typeof refs === 'object' && Object.keys(refs).length > 0);
92
+ if (hasRefs) {
93
+ for (const layer of Object.keys(refs).sort()) {
94
+ const raw = refs[layer];
95
+ const paths = arkRulesRefList(raw);
96
+ const pathKey = `$.arkRules[${JSON.stringify(layer)}]`;
97
+ if (!paths) {
98
+ errors.push({ path: pathKey, message: 'must be a non-empty path string or an array of paths' });
99
+ continue;
100
+ }
101
+ if (!layerNames.has(layer)) {
102
+ errors.push({
103
+ path: pathKey,
104
+ message: `layer ${JSON.stringify(layer)} is not declared in layers[]`,
105
+ });
106
+ continue;
107
+ }
108
+ paths.forEach((relRaw, index) => {
109
+ const itemKey = paths.length === 1 && typeof raw === 'string' ? pathKey : `${pathKey}[${index}]`;
110
+ const rel = normalizeProjectRelativePath(relRaw);
111
+ if (!rel) {
112
+ errors.push({
113
+ path: itemKey,
114
+ message:
115
+ 'must be a project-relative path without absolute roots or parent-directory traversal',
116
+ });
117
+ return;
118
+ }
119
+ referenced.add(rel);
120
+ const read = readRelative(rel);
121
+ if (!read.ok) {
122
+ if (read.missing && opts.missingAsEmpty) return;
123
+ errors.push({ path: itemKey, message: read.message });
124
+ return;
125
+ }
126
+ try {
127
+ const loaded = loadArkRulesContract(JSON.parse(read.content), rel, layer);
128
+ parts.push({ layer, sourceFile: rel, file: loaded.config });
129
+ } catch (error) {
130
+ pushReadError(errors, itemKey, rel, error);
131
+ }
90
132
  });
91
- continue;
92
133
  }
93
- referenced.add(rel);
94
- const read = readRelative(rel);
134
+ }
135
+ const plan = resolveSliceRulePlan({
136
+ files: Array.isArray(opts.files) ? opts.files : [],
137
+ rules: Array.isArray(config?.rules) ? config.rules : [],
138
+ layers: [...layerNames],
139
+ });
140
+ for (const entry of plan) {
141
+ const read = readRelative(entry.path);
95
142
  if (!read.ok) {
96
- if (read.missing && opts.missingAsEmpty) continue;
97
- errors.push({ path: pathKey, message: read.message });
143
+ if (read.missing) continue;
144
+ errors.push({ path: entry.path, message: read.message });
98
145
  continue;
99
146
  }
147
+ referenced.add(entry.path);
100
148
  try {
101
- const loaded = loadArkRulesContract(JSON.parse(read.content), rel, layer);
102
- parts.push({ layer, sourceFile: rel, file: loaded.config });
149
+ const loaded = loadArkRulesContract(JSON.parse(read.content), entry.path, entry.layer);
150
+ const part = {
151
+ layer: entry.layer,
152
+ sourceFile: entry.path,
153
+ file: loaded.config,
154
+ childId: entry.childId,
155
+ defaultAppliesTo: entry.defaultAppliesTo,
156
+ };
157
+ const escapes = sliceScopeEscapes(part);
158
+ if (escapes.length > 0) {
159
+ errors.push({
160
+ path: entry.path,
161
+ message: `ARKRULE_SCOPE_ESCAPES_SLICE: ${entry.path} appliesTo ${escapes
162
+ .map((row) => row.pattern)
163
+ .join(', ')} escapes ${entry.childId}`,
164
+ });
165
+ continue;
166
+ }
167
+ parts.push(part);
103
168
  } catch (error) {
104
- errors.push({
105
- path: pathKey,
106
- message:
107
- error instanceof Error
108
- ? error.message
109
- : `referenced ArkRules file ${JSON.stringify(rel)} failed to load`,
110
- });
169
+ pushReadError(errors, entry.path, entry.path, error);
111
170
  }
112
171
  }
172
+ for (const dupe of duplicateArkRuleIds(parts)) {
173
+ errors.push({
174
+ path: `$.arkRules[${JSON.stringify(dupe.layer)}]`,
175
+ message: `ARKRULE_DUPLICATE_ID: ${dupe.id} is declared in ${dupe.sourceFiles.join(', ')}`,
176
+ });
177
+ }
178
+ if (!hasRefs && parts.length === 0 && errors.length === 0) {
179
+ return { arkRules: emptyEffectiveArkRules(), errors: [], referenced };
180
+ }
113
181
  if (errors.length > 0) return { arkRules: emptyEffectiveArkRules(), errors, referenced };
114
182
  return { arkRules: buildEffectiveArkRules(parts), errors: [], referenced };
115
183
  }
@@ -210,7 +278,7 @@ function unreferencedArkRulesWarnings(canonicalRoot, referenced) {
210
278
  return warnings;
211
279
  }
212
280
 
213
- export const ARKRULE_FILE_UNREFERENCED = 'ARKRULE_FILE_UNREFERENCED';
281
+ const ARKRULE_FILE_UNREFERENCED = 'ARKRULE_FILE_UNREFERENCED';
214
282
 
215
283
  /**
216
284
  * Loader drift warnings as advisory check warnings (never fail --strict-config).
@@ -233,10 +301,79 @@ export function arkRulesDriftWarnings(warnings) {
233
301
  * @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
234
302
  * @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, warnings: Array<{path:string,message:string,severity:string}>, errors: Array<{path:string,message:string}> }}
235
303
  */
304
+ function governedRelativeFiles(canonicalRoot, config) {
305
+ const include = Array.isArray(config?.include) && config.include.length > 0 ? config.include : ['src'];
306
+ return collectGovernedFiles(canonicalRoot, { ...config, include })
307
+ .map((file) => path.relative(canonicalRoot, file).split(path.sep).join('/'))
308
+ .filter((rel) => rel && !rel.startsWith('..'));
309
+ }
310
+
311
+ function sliceRuleBasenames(config) {
312
+ return [...declaredLayerNames(config)].map((layer) => `arkrules.${layer}.json`);
313
+ }
314
+
315
+ /** `arkrules.<Layer>.json` beside a governed file or one of its parents. Not a repo-wide search. */
316
+ function unreferencedSliceRuleWarnings(canonicalRoot, referenced, files, basenames) {
317
+ const wanted = new Set(basenames);
318
+ if (wanted.size === 0) return [];
319
+ const dirs = new Set();
320
+ for (const rel of files) {
321
+ let end = String(rel).lastIndexOf('/');
322
+ while (end > 0) {
323
+ const dir = String(rel).slice(0, end);
324
+ if (dirs.has(dir)) break;
325
+ dirs.add(dir);
326
+ end = dir.lastIndexOf('/');
327
+ }
328
+ }
329
+ const warnings = [];
330
+ for (const dir of dirs) {
331
+ let names;
332
+ try {
333
+ names = fs.readdirSync(path.join(canonicalRoot, ...dir.split('/')));
334
+ } catch {
335
+ continue;
336
+ }
337
+ for (const name of names) {
338
+ if (!wanted.has(name)) continue;
339
+ const candidate = `${dir}/${name}`;
340
+ if (referenced.has(candidate)) continue;
341
+ warnings.push({
342
+ path: candidate,
343
+ message: `ArkRules file ${JSON.stringify(candidate)} is not referenced by arkRules and will not be enforced`,
344
+ severity: 'advisory',
345
+ });
346
+ }
347
+ }
348
+ return warnings;
349
+ }
350
+
351
+ export function arkRulesLoadFailed(source, errors) {
352
+ const message = (errors ?? []).map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
353
+ const contractError = (errors ?? []).some(
354
+ (issue) =>
355
+ typeof issue.message === 'string' &&
356
+ (issue.message.includes('ARKRULE_DUPLICATE_ID') || issue.message.includes('ARKRULE_SCOPE_ESCAPES_SLICE'))
357
+ );
358
+ const label = contractError ? 'Invalid ArkGate config' : 'Invalid Effective Contract';
359
+ const error = new Error(`${label} (${source}):\n${message}`);
360
+ error.code = 'ARKRULES_LOAD_FAILED';
361
+ error.issues = errors;
362
+ if (contractError) {
363
+ error.name = 'ArkConfigValidationError';
364
+ error.source = source;
365
+ }
366
+ return error;
367
+ }
368
+
236
369
  export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
237
370
  const canonicalRoot = fs.realpathSync(root);
238
- const loaded = loadEffectiveArkRules(config, directoryArkRulesReader(canonicalRoot, opts));
371
+ const files = Array.isArray(opts.files) ? opts.files : governedRelativeFiles(canonicalRoot, config);
372
+ const loaded = loadEffectiveArkRules(config, directoryArkRulesReader(canonicalRoot, opts), { ...opts, files });
239
373
  // Drift runs with or without a map: an arkrules/ directory nobody references is visible.
240
- const warnings = unreferencedArkRulesWarnings(canonicalRoot, loaded.referenced);
374
+ const warnings = [
375
+ ...unreferencedArkRulesWarnings(canonicalRoot, loaded.referenced),
376
+ ...unreferencedSliceRuleWarnings(canonicalRoot, loaded.referenced, files, sliceRuleBasenames(config)),
377
+ ].sort((left, right) => left.path.localeCompare(right.path));
241
378
  return { arkRules: loaded.arkRules, warnings, errors: loaded.errors };
242
379
  }