arkgate 4.8.23 → 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 (201) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +41 -91
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +707 -121
  6. package/bin/ark-mcp-runtime.mjs +448 -169
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +42 -22
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/adoption-stance.mjs +2 -2
  13. package/bin/lib/adr-path.mjs +3 -3
  14. package/bin/lib/agent-gates.mjs +0 -61
  15. package/bin/lib/agent-homes.mjs +12 -21
  16. package/bin/lib/agent-projection-command.mjs +9 -5
  17. package/bin/lib/ambient-state.mjs +9 -30
  18. package/bin/lib/analysis-engine.mjs +15 -11
  19. package/bin/lib/architecture-scan.mjs +61 -15
  20. package/bin/lib/ark-order-doctor.mjs +4 -29
  21. package/bin/lib/ark-order-facts.mjs +319 -17
  22. package/bin/lib/ark-order-sensors.mjs +28 -101
  23. package/bin/lib/ark-order-types.mjs +3 -2
  24. package/bin/lib/ark-run-doctor.mjs +17 -28
  25. package/bin/lib/ark-run-facts.mjs +461 -48
  26. package/bin/lib/ark-run-sensors.mjs +6 -4
  27. package/bin/lib/arkgate-pins.mjs +91 -0
  28. package/bin/lib/arkrule-file-hints.mjs +2 -47
  29. package/bin/lib/arkrules-contract.mjs +124 -133
  30. package/bin/lib/arkrules-sensors.mjs +55 -252
  31. package/bin/lib/check-args.mjs +85 -1
  32. package/bin/lib/ci-and-commands.mjs +24 -78
  33. package/bin/lib/ci-merge-boundary.mjs +1 -1
  34. package/bin/lib/class-source-scan.mjs +479 -0
  35. package/bin/lib/clone-detection.mjs +474 -0
  36. package/bin/lib/codex-home.mjs +5 -3
  37. package/bin/lib/config-contract-slices.mjs +739 -0
  38. package/bin/lib/config-contract.mjs +42 -607
  39. package/bin/lib/config-extras.mjs +16 -22
  40. package/bin/lib/config-warnings.mjs +32 -0
  41. package/bin/lib/contract-smells.mjs +33 -15
  42. package/bin/lib/core-ratchet.mjs +0 -2
  43. package/bin/lib/dashboard-cli.mjs +62 -0
  44. package/bin/lib/deep-module-coach.mjs +2 -4
  45. package/bin/lib/deploy-path.mjs +13 -38
  46. package/bin/lib/design-delta.mjs +1 -1
  47. package/bin/lib/design-smells.mjs +2 -2
  48. package/bin/lib/diagnostic-catalog.mjs +23 -9
  49. package/bin/lib/doctor-advisories.mjs +78 -8
  50. package/bin/lib/doctor-green-cite.mjs +1 -1
  51. package/bin/lib/doctor-human.mjs +1 -1
  52. package/bin/lib/doctor-next-actions.mjs +19 -4
  53. package/bin/lib/doctor-plan.mjs +9 -13
  54. package/bin/lib/duplication-io.mjs +572 -0
  55. package/bin/lib/effective-contract-load.mjs +282 -83
  56. package/bin/lib/enforcement-profiles.mjs +40 -4
  57. package/bin/lib/entry-points-io.mjs +424 -0
  58. package/bin/lib/extra-merge-teeth.mjs +36 -0
  59. package/bin/lib/field-install.mjs +38 -14
  60. package/bin/lib/first-run-help.mjs +10 -5
  61. package/bin/lib/flat-parent-importers.mjs +139 -0
  62. package/bin/lib/flat-parent-pilot.mjs +40 -100
  63. package/bin/lib/gate-files.mjs +58 -90
  64. package/bin/lib/git-change-scope.mjs +109 -0
  65. package/bin/lib/github-enforcement.mjs +166 -29
  66. package/bin/lib/golden-pattern.mjs +1 -1
  67. package/bin/lib/hook-templates.mjs +17 -15
  68. package/bin/lib/host-support-matrix.mjs +3 -1
  69. package/bin/lib/html-report-advisories.mjs +4 -0
  70. package/bin/lib/html-report-depth.mjs +2 -2
  71. package/bin/lib/html-report.mjs +2 -2
  72. package/bin/lib/import-graph-projection.mjs +112 -0
  73. package/bin/lib/import-resolve.mjs +13 -1
  74. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  75. package/bin/lib/install-migrate.mjs +50 -73
  76. package/bin/lib/invariant-coverage-io.mjs +4 -3
  77. package/bin/lib/invariant-coverage.mjs +85 -11
  78. package/bin/lib/invariant-probe-cli.mjs +347 -0
  79. package/bin/lib/invariant-probe-io.mjs +165 -0
  80. package/bin/lib/invariant-probe-runner.mjs +268 -0
  81. package/bin/lib/invariant-probe-sites.mjs +174 -0
  82. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  83. package/bin/lib/invariant-probe.mjs +461 -0
  84. package/bin/lib/invariant-tests-path.mjs +16 -22
  85. package/bin/lib/literal-path-drift.mjs +4 -2
  86. package/bin/lib/managed-upgrade.mjs +2 -8
  87. package/bin/lib/mcp-adoption.mjs +13 -6
  88. package/bin/lib/mcp-hook-payload.mjs +403 -63
  89. package/bin/lib/orphan-modules-io.mjs +458 -0
  90. package/bin/lib/orphan-modules.mjs +251 -0
  91. package/bin/lib/outside-importers.mjs +197 -0
  92. package/bin/lib/package-manager.mjs +60 -2
  93. package/bin/lib/parse-health.mjs +1 -1
  94. package/bin/lib/passthrough-commands.mjs +83 -0
  95. package/bin/lib/persistence-write-hint.mjs +106 -0
  96. package/bin/lib/pin-dependent-gates.mjs +117 -0
  97. package/bin/lib/policy-delta-io.mjs +219 -14
  98. package/bin/lib/post-green-path.mjs +1 -1
  99. package/bin/lib/prepare-change.mjs +115 -7
  100. package/bin/lib/prepare-write.mjs +2 -2
  101. package/bin/lib/presets.mjs +59 -73
  102. package/bin/lib/product-copy.mjs +1 -6
  103. package/bin/lib/project-root.mjs +31 -62
  104. package/bin/lib/remediation.mjs +23 -4
  105. package/bin/lib/resolved-candidate-facts.mjs +405 -117
  106. package/bin/lib/rules-inventory.mjs +22 -3
  107. package/bin/lib/rules-under-contract.mjs +247 -9
  108. package/bin/lib/scan-files.mjs +36 -4
  109. package/bin/lib/schema-validation.mjs +130 -0
  110. package/bin/lib/sensor-promote-cli.mjs +2 -11
  111. package/bin/lib/sensor-promote-io.mjs +33 -22
  112. package/bin/lib/sensor-promotion.mjs +1 -1
  113. package/bin/lib/skill-catalog-apply.mjs +1 -1
  114. package/bin/lib/skill-install.mjs +42 -38
  115. package/bin/lib/snippet-analysis.mjs +140 -6
  116. package/bin/lib/source-policy.mjs +34 -0
  117. package/bin/lib/start-install-recovery.mjs +72 -2
  118. package/bin/lib/start-preview.mjs +31 -1
  119. package/bin/lib/states-transitions-presence.mjs +3 -3
  120. package/bin/lib/status-command.mjs +35 -22
  121. package/bin/lib/status-manifest.mjs +50 -30
  122. package/bin/lib/status-transition-catalog.mjs +0 -6
  123. package/bin/lib/suggestions.mjs +3 -3
  124. package/bin/lib/team-parliament-io.mjs +76 -88
  125. package/bin/lib/team-parliament.mjs +21 -10
  126. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  127. package/bin/lib/upgrade-command.mjs +2 -24
  128. package/bin/lib/upgrade-outcome.mjs +1 -1
  129. package/bin/lib/upstream-report.mjs +21 -10
  130. package/bin/lib/violations.mjs +10 -9
  131. package/bin/lib/weakest-link.mjs +13 -0
  132. package/bin/lib/write-path-capabilities.mjs +6 -4
  133. package/dist/chunk-A5EIY3HI.cjs +5 -0
  134. package/dist/chunk-AHACEKSV.js +16 -0
  135. package/dist/chunk-SVSQVGFJ.js +5 -0
  136. package/dist/chunk-UN5EH4QV.cjs +16 -0
  137. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
  138. package/dist/configTypes-GPvvdleM.d.ts +257 -0
  139. package/dist/eslint/index.cjs +18 -5
  140. package/dist/eslint/index.d.cts +368 -0
  141. package/dist/eslint/index.d.ts +139 -156
  142. package/dist/eslint/index.js +11 -5
  143. package/dist/index.cjs +36 -35
  144. package/dist/index.d.cts +5547 -0
  145. package/dist/index.d.ts +3283 -198
  146. package/dist/index.js +36 -35
  147. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  148. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  149. package/dist/nestjs/index.cjs +1 -18
  150. package/dist/nestjs/index.d.cts +22 -0
  151. package/dist/nestjs/index.d.ts +1 -3
  152. package/dist/nestjs/index.js +1 -18
  153. package/dist/order/index.cjs +1 -1
  154. package/dist/order/index.d.cts +155 -0
  155. package/dist/order/index.d.ts +15 -3
  156. package/dist/order/index.js +1 -1
  157. package/dist/runtime/index.cjs +13 -30
  158. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
  159. package/dist/runtime/index.d.ts +2741 -133
  160. package/dist/runtime/index.js +13 -30
  161. package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
  162. package/dist/types-CnauMQQL.d.ts +1800 -0
  163. package/docs/README.md +3 -1
  164. package/docs/agent-guide.md +217 -16
  165. package/docs/ai-gates.md +174 -26
  166. package/docs/arkorder.md +32 -15
  167. package/docs/brownfield-adoption.md +5 -1
  168. package/docs/configuration.md +230 -31
  169. package/docs/develop.md +2 -1
  170. package/docs/diagnostics.md +212 -19
  171. package/docs/package-surface.md +35 -9
  172. package/docs/product-voice.md +7 -1
  173. package/package.json +69 -19
  174. package/schemas/ark.analysis-result.schema.json +15 -0
  175. package/schemas/ark.config.schema.json +77 -12
  176. package/schemas/ark.invariant-probe.schema.json +368 -0
  177. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  178. package/schemas/ark.status-manifest.schema.json +22 -0
  179. package/server.json +3 -9
  180. package/templates/agent-skills/README.md +1 -1
  181. package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
  182. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  183. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  184. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  185. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  186. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  187. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  188. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  189. package/templates/arkrules/PersistenceAdapters.json +1 -1
  190. package/templates/arkrules/PresentationAdapters.json +1 -1
  191. package/templates/skills/ark-adopt.md +5 -1
  192. package/templates/skills/ark-contract.md +17 -0
  193. package/templates/skills/ark-coverage.md +2 -0
  194. package/templates/skills/ark-explore.md +7 -0
  195. package/templates/skills/ark-fix.md +20 -0
  196. package/templates/skills/ark-order.md +4 -2
  197. package/templates/skills/ark-place.md +8 -0
  198. package/templates/skills/ark-runtime.md +6 -1
  199. package/bin/lib/ark-order-error.mjs +0 -18
  200. package/bin/lib/ark-order-invariants.mjs +0 -346
  201. package/dist/types-Ik_OakcI.d.ts +0 -183
@@ -3,17 +3,31 @@ import path from 'node:path';
3
3
  import {
4
4
  loadArchitectureChangeMap,
5
5
  loadContract,
6
- preflightResolvedChange,
6
+ preflightTrustedResolvedChange,
7
7
  } from './analysis-engine.mjs';
8
8
  import { createAdapterResult } from './adapter-contract.mjs';
9
9
  import { isGovernableSourceFile } from './scan-files.mjs';
10
10
  import {
11
11
  canonicalizeCandidateChanges,
12
+ createResolverIngestCache,
12
13
  resolveCandidateFacts,
13
14
  } from './resolved-candidate-facts.mjs';
14
15
  import { effectiveAnalysisConfig } from './analysis-policy.mjs';
15
16
  import { isScanExcludedRelative } from '../ark-shared.mjs';
16
17
  import { classifyChangeSet, evaluateTeamGate } from './team-parliament.mjs';
18
+ import { arkRulesLoadFailed, loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
19
+ import {
20
+ coverageOptionsFromConfig,
21
+ invariantIdsFromCatalog,
22
+ loadInvariantCoverageInputs,
23
+ } from './invariant-coverage-io.mjs';
24
+ import { catalogHasEnforcedInvariant } from './invariant-coverage.mjs';
25
+ import {
26
+ declaredCoverageRootsPresent,
27
+ declaredInvariantTestsPathPresent,
28
+ scanDemandsInvariantTestsPath,
29
+ } from './invariant-tests-path.mjs';
30
+ import { loadArkRuleFileHints } from './arkrule-file-hints.mjs';
17
31
 
18
32
  function candidatePath(value) {
19
33
  if (typeof value !== 'string' || value.trim() === '') {
@@ -69,7 +83,7 @@ function assertInsideProject(root, relativePath) {
69
83
  }
70
84
  }
71
85
 
72
- export function normalizeChangeSet(input) {
86
+ function normalizeChangeSet(input) {
73
87
  if (!Array.isArray(input)) throw new Error('changes must be an array.');
74
88
  return input.map((change, index) => {
75
89
  if (!change || typeof change !== 'object' || Array.isArray(change)) {
@@ -88,6 +102,73 @@ export function normalizeChangeSet(input) {
88
102
  });
89
103
  }
90
104
 
105
+ function overlayContents(contents, overlay) {
106
+ const out = { ...(contents ?? {}) };
107
+ for (const change of overlay) {
108
+ if (change.delete === true) delete out[change.path];
109
+ else out[change.path] = change.content;
110
+ }
111
+ return out;
112
+ }
113
+
114
+ /**
115
+ * ArkRules engine inputs for base (disk) and candidate (disk + in-memory overlay):
116
+ * invariant coverage contents and structural file hints, loaded exactly the way
117
+ * architecture-scan loads them for ark-check. Undefined when ArkRules is off.
118
+ */
119
+ function arkRulesAnalysisInputs({ root, config, arkRules, baseFacts, candidateFacts, overlay }) {
120
+ const structure = arkRules?.structure ?? [];
121
+ const invariants = arkRules?.invariants ?? [];
122
+ if (structure.length === 0 && invariants.length === 0) return {};
123
+ const adopted = scanDemandsInvariantTestsPath(root, {});
124
+ const shared = {
125
+ ...(adopted ? { adopted: true } : {}),
126
+ ...(adopted && declaredInvariantTestsPathPresent(root, config.coverage) === false
127
+ ? { invariantTestsPathPresent: false }
128
+ : {}),
129
+ ...(catalogHasEnforcedInvariant(invariants) &&
130
+ declaredCoverageRootsPresent(root, config.coverage) === false
131
+ ? { coverageRootsPresent: false }
132
+ : {}),
133
+ };
134
+ const baseCoverage =
135
+ invariants.length > 0
136
+ ? loadInvariantCoverageInputs(root, baseFacts, {
137
+ invariantIds: invariantIdsFromCatalog(arkRules),
138
+ ...coverageOptionsFromConfig(config),
139
+ })
140
+ : undefined;
141
+ const candidateCoverage = baseCoverage
142
+ ? {
143
+ ...baseCoverage,
144
+ fileContents: overlayContents(baseCoverage.fileContents, overlay),
145
+ testFiles: (baseCoverage.testFiles ?? []).filter(
146
+ (file) => !overlay.some((change) => change.delete === true && change.path === file)
147
+ ),
148
+ }
149
+ : undefined;
150
+ const baseHints = loadArkRuleFileHints(root, baseFacts, arkRules, baseCoverage?.fileContents);
151
+ // Candidate hints must read the overlay, never the stale on-disk text of a changed file.
152
+ const candidateHints = loadArkRuleFileHints(
153
+ root,
154
+ candidateFacts,
155
+ arkRules,
156
+ overlayContents(candidateCoverage?.fileContents ?? {}, overlay)
157
+ );
158
+ return {
159
+ baseAnalysisInputs: {
160
+ ...shared,
161
+ ...(baseCoverage ? { coverageInputs: baseCoverage } : {}),
162
+ ...(baseHints ? { fileHints: baseHints } : {}),
163
+ },
164
+ candidateAnalysisInputs: {
165
+ ...shared,
166
+ ...(candidateCoverage ? { coverageInputs: candidateCoverage } : {}),
167
+ ...(candidateHints ? { fileHints: candidateHints } : {}),
168
+ },
169
+ };
170
+ }
171
+
91
172
  export function prepareChangeFromRoot({
92
173
  root,
93
174
  config,
@@ -115,10 +196,16 @@ export function prepareChangeFromRoot({
115
196
  for (const change of normalizedChanges) {
116
197
  assertInsideProject(root, change.path);
117
198
  }
118
- const contract = loadContract(
119
- effectiveConfig,
120
- configSource ?? path.join(root, 'ark.config.json')
121
- );
199
+ const contractSource = configSource ?? path.join(root, 'ark.config.json');
200
+ // Same Effective Contract as ark-check: ArkRules ride policyHash and the verdict,
201
+ // and a broken reference fails closed instead of silently dropping the catalog.
202
+ const arkRulesLoad = loadEffectiveArkRulesFromDisk(root, effectiveConfig);
203
+ if (arkRulesLoad.errors.length > 0) {
204
+ throw arkRulesLoadFailed(contractSource, arkRulesLoad.errors);
205
+ }
206
+ const contract = loadContract(effectiveConfig, contractSource, {
207
+ arkRules: arkRulesLoad.arkRules,
208
+ });
122
209
  const loadedChangeMap =
123
210
  changeMap === undefined
124
211
  ? undefined
@@ -129,24 +216,45 @@ export function prepareChangeFromRoot({
129
216
  changes: normalizedChanges,
130
217
  });
131
218
  for (const change of canonicalChanges) assertGovernedSource(effectiveConfig, change.path);
219
+ // Files the overlay does not touch are parsed once for both trees.
220
+ const ingestCache = createResolverIngestCache(contract.config);
132
221
  const baseFacts = resolveCandidateFacts({
133
222
  root,
134
223
  config: contract.config,
135
224
  ts,
225
+ ingestCache,
136
226
  ...(tsconfig ? { tsconfig } : {}),
137
227
  });
138
228
  const candidateFacts = resolveCandidateFacts({
139
229
  root,
140
230
  config: contract.config,
141
231
  ts,
232
+ ingestCache,
142
233
  changes: normalizedOverlayChanges,
143
234
  ...(tsconfig ? { tsconfig } : {}),
144
235
  });
145
- const result = preflightResolvedChange({
236
+ ingestCache.records.clear();
237
+ const { baseAnalysisInputs, candidateAnalysisInputs } = arkRulesAnalysisInputs({
238
+ root,
239
+ config: effectiveConfig,
240
+ arkRules: arkRulesLoad.arkRules,
241
+ baseFacts,
242
+ candidateFacts,
243
+ overlay: canonicalizeCandidateChanges({
244
+ root,
245
+ config: contract.config,
246
+ changes: normalizedOverlayChanges,
247
+ }),
248
+ });
249
+ // Both fact sets were created (validated, frozen) by this bundle instance just
250
+ // above: skip re-validating two whole-project copies of each.
251
+ const result = preflightTrustedResolvedChange({
146
252
  contract,
147
253
  baseFacts,
148
254
  candidateFacts,
149
255
  changes: canonicalChanges,
256
+ ...(baseAnalysisInputs ? { baseAnalysisInputs } : {}),
257
+ ...(candidateAnalysisInputs ? { candidateAnalysisInputs } : {}),
150
258
  ...(loadedChangeMap ? { changeMap: loadedChangeMap } : {}),
151
259
  });
152
260
  const completeness =
@@ -8,11 +8,10 @@ import crypto from 'node:crypto';
8
8
  import { validateWithAutoPatch } from './auto-patch.mjs';
9
9
  import { classifyRemediation, enrichViolationWithFixClass } from './remediation.mjs';
10
10
  import {
11
- layerDescriptionCaption,
12
11
  placementDescriptionFields,
13
12
  } from './layer-description.mjs';
14
13
 
15
- export { layerDescriptionCaption, placementDescriptionFields };
14
+ export { placementDescriptionFields };
16
15
 
17
16
  /**
18
17
  * Stable content identity for host commit / cache keys.
@@ -41,6 +40,7 @@ export function buildJudgmentBrief(violations) {
41
40
  targetTypeOnlyExports: v.targetTypeOnlyExports,
42
41
  namedBindingsTypeOnly: v.namedBindingsTypeOnly,
43
42
  peerIsolation: v.peerIsolation ?? v.details?.peerIsolation,
43
+ reasonId: v.reasonId ?? v.details?.reasonId,
44
44
  edgeKind: v.edgeKind ?? v.details?.importKind,
45
45
  fromLayer: v.fromLayer,
46
46
  toLayer: v.toLayer,
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import {
8
8
  applyFrameworkLayoutOverlays,
9
+ cloneArchetypeForLayer,
9
10
  createElevenLayerConfig,
10
11
  DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
11
12
  discoverRepoUnits,
@@ -18,7 +19,7 @@ import { withArkConfigMetadata } from './config-contract.mjs';
18
19
  const PRESETS_DIR = path.dirname(fileURLToPath(import.meta.url));
19
20
  const ARKRULES_TEMPLATES_DIR = path.join(PRESETS_DIR, '../../templates/arkrules');
20
21
 
21
- export function denyUpward(names) {
22
+ function denyUpward(names) {
22
23
  const rules = [];
23
24
  for (let i = 0; i < names.length; i += 1) {
24
25
  for (let j = i + 1; j < names.length; j += 1) {
@@ -32,7 +33,7 @@ export function denyUpward(names) {
32
33
  * peerIsolation matrix: deny only when importer/importee sit under different slices.
33
34
  * Covers same-layer and cross-layer pairs (honest DDD / vertical-slice isolation).
34
35
  */
35
- export function peerIsolationEdges(layerNames, sliceFolders, message) {
36
+ function peerIsolationEdges(layerNames, sliceFolders, message) {
36
37
  const rules = [];
37
38
  for (const from of layerNames) {
38
39
  for (const to of layerNames) {
@@ -56,7 +57,7 @@ export function peerIsolationEdges(layerNames, sliceFolders, message) {
56
57
  // Framework internals under `src/kernel/**` are NOT application architecture — a broad
57
58
  // `src/**/domain/**` would otherwise swallow `src/kernel/domain`. Do NOT use `**/kernel/**`
58
59
  // (that carves out legitimate `src/shared/kernel/**` SharedKernel paths).
59
- export const FRAMEWORK_INTERNAL_EXCLUDE = ['src/kernel/**', '**/src/kernel/**'];
60
+ const FRAMEWORK_INTERNAL_EXCLUDE = ['src/kernel/**', '**/src/kernel/**'];
60
61
 
61
62
  /**
62
63
  * Shared high-spec domain globs. Prefer these over Application/Presentation scatter
@@ -216,18 +217,6 @@ export const APPLICATION_LIB_ORCHESTRATION_PATTERNS = Object.freeze([
216
217
  '**/lib/api-handlers/**',
217
218
  ]);
218
219
 
219
- /**
220
- * AR08 — attach lean arkRules map. Keys are always exact project layer names.
221
- * Sensor roles (domain-structure / orchestration / adapter-thin / generic) are
222
- * independent of display names so renamed layers still get the right starter.
223
- */
224
- export const DEFAULT_ARKRULES_REFS = {
225
- DomainModel: 'arkrules/DomainModel.json',
226
- ApplicationOrchestration: 'arkrules/ApplicationOrchestration.json',
227
- PresentationAdapters: 'arkrules/PresentationAdapters.json',
228
- PersistenceAdapters: 'arkrules/PersistenceAdapters.json',
229
- };
230
-
231
220
  /** Sensor roles used when selecting or synthesizing per-layer templates. */
232
221
  export const ARKRULES_SENSOR_ROLES = Object.freeze({
233
222
  DOMAIN_STRUCTURE: 'domain-structure',
@@ -240,7 +229,7 @@ export const ARKRULES_SENSOR_ROLES = Object.freeze({
240
229
  * Exact-name aliases → sensor role. Prefer this table over heuristics when the
241
230
  * project uses a known vocabulary (hexagonal, monorepo field renames, etc.).
242
231
  */
243
- export const LAYER_SENSOR_ROLE_ALIASES = Object.freeze({
232
+ const LAYER_SENSOR_ROLE_ALIASES = Object.freeze({
244
233
  // Domain / pure model
245
234
  DomainModel: ARKRULES_SENSOR_ROLES.DOMAIN_STRUCTURE,
246
235
  Domain: ARKRULES_SENSOR_ROLES.DOMAIN_STRUCTURE,
@@ -371,11 +360,8 @@ export function buildArkRulesTemplateForLayer(layerName, role = resolveLayerSens
371
360
  const sourcePath = path.join(ARKRULES_TEMPLATES_DIR, archetype);
372
361
  if (fs.existsSync(sourcePath)) {
373
362
  try {
374
- const parsed = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
375
- return {
376
- ...parsed,
377
- layer: layerName,
378
- };
363
+ // Unique ids across arkrules/*.json so `--promote <id> --apply` works on the starter.
364
+ return cloneArchetypeForLayer(JSON.parse(fs.readFileSync(sourcePath, 'utf8')), layerName);
379
365
  } catch {
380
366
  // Fall through to generic mold.
381
367
  }
@@ -450,7 +436,46 @@ export function writeArkRulesTemplates(root, config, { force = false } = {}) {
450
436
  return written;
451
437
  }
452
438
 
453
- export function presetWithOverlays(baseConfig, root) {
439
+ /**
440
+ * Deny edges shared by the ports-and-adapters presets after their DomainModel
441
+ * rules: orchestration never reaches adapters, adapters never reach each other,
442
+ * and presentation never reaches the domain directly. Fresh objects per call.
443
+ */
444
+ function portsAndAdaptersRules() {
445
+ return [
446
+ { from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
447
+ { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
448
+ { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
449
+ { from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
450
+ { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
451
+ { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
452
+ ];
453
+ }
454
+
455
+ /**
456
+ * UI-surface preset rules: the domain and orchestration stay out of the UI, while
457
+ * presentation may reach data clients on day one (soft, `message` explains why).
458
+ * Persistence → Presentation stays denied. Fresh objects per call.
459
+ * @param {string} presentationToPersistenceMessage
460
+ */
461
+ function uiSurfaceRules(presentationToPersistenceMessage) {
462
+ return [
463
+ { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
464
+ { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
465
+ { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
466
+ { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
467
+ {
468
+ from: 'PresentationAdapters',
469
+ to: 'PersistenceAdapters',
470
+ allowed: true,
471
+ message: presentationToPersistenceMessage,
472
+ },
473
+ { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
474
+ { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
475
+ ];
476
+ }
477
+
478
+ function presetWithOverlays(baseConfig, root) {
454
479
  const config = root ? applyFrameworkLayoutOverlays(baseConfig, root) : baseConfig;
455
480
  return withArkConfigMetadata(withDefaultArkRules(config));
456
481
  }
@@ -513,12 +538,7 @@ export const ARCHITECTURE_PRESETS = {
513
538
  { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
514
539
  { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
515
540
  { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
516
- { from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
517
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
518
- { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
519
- { from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
520
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
521
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
541
+ ...portsAndAdaptersRules(),
522
542
  ],
523
543
  },
524
544
  root
@@ -707,12 +727,7 @@ export const ARCHITECTURE_PRESETS = {
707
727
  { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
708
728
  { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
709
729
  { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
710
- { from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
711
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
712
- { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
713
- { from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
714
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
715
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
730
+ ...portsAndAdaptersRules(),
716
731
  ],
717
732
  },
718
733
  root
@@ -812,24 +827,12 @@ export const ARCHITECTURE_PRESETS = {
812
827
  optional: true,
813
828
  },
814
829
  ],
815
- rules: [
816
- { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
817
- { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
818
- { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
819
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
820
- // Next RSC often imports data clients from routes; deny is ideal but day-one
821
- // ui-surface keeps this as a soft guidance edge (allowed) until ports exist —
822
- // Persistence → Presentation stays denied when that edge appears.
823
- {
824
- from: 'PresentationAdapters',
825
- to: 'PersistenceAdapters',
826
- allowed: true,
827
- message:
828
- 'UI/routes may reach data clients on day one (RSC); prefer application ports as the product grows.',
829
- },
830
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
831
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
832
- ],
830
+ // Next RSC often imports data clients from routes; deny is ideal but day-one
831
+ // ui-surface keeps this as a soft guidance edge (allowed) until ports exist —
832
+ // Persistence → Presentation stays denied when that edge appears.
833
+ rules: uiSurfaceRules(
834
+ 'UI/routes may reach data clients on day one (RSC); prefer application ports as the product grows.'
835
+ ),
833
836
  },
834
837
  root
835
838
  ),
@@ -980,12 +983,7 @@ export const ARCHITECTURE_PRESETS = {
980
983
  { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
981
984
  { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
982
985
  { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
983
- { from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
984
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
985
- { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
986
- { from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
987
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
988
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
986
+ ...portsAndAdaptersRules(),
989
987
  { from: 'SharedKernel', to: 'DomainModel', allowed: false },
990
988
  { from: 'SharedKernel', to: 'ApplicationOrchestration', allowed: false },
991
989
  { from: 'SharedKernel', to: 'PresentationAdapters', allowed: false },
@@ -1080,21 +1078,9 @@ export const ARCHITECTURE_PRESETS = {
1080
1078
  optional: true,
1081
1079
  },
1082
1080
  ],
1083
- rules: [
1084
- { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
1085
- { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
1086
- { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
1087
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
1088
- {
1089
- from: 'PresentationAdapters',
1090
- to: 'PersistenceAdapters',
1091
- allowed: true,
1092
- message:
1093
- 'SPA day-one UI may reach data clients; prefer ports as the product grows.',
1094
- },
1095
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
1096
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
1097
- ],
1081
+ rules: uiSurfaceRules(
1082
+ 'SPA day-one UI may reach data clients; prefer ports as the product grows.'
1083
+ ),
1098
1084
  },
1099
1085
  root
1100
1086
  ),
@@ -25,7 +25,7 @@ export const ARKRULES_FIRST_CONTACT_NEXT =
25
25
  export const ARKRULES_EMPTY_CATALOG_NEXT =
26
26
  'Next: add 1–2 short phrases to invariants[] in the Domain rules file, then ark-check --doctor.';
27
27
 
28
- /** Status-light leftover-design qualifier (was “design-weak”). */
28
+ /** Status-light leftover-design qualifier (was “design-weak”); also the HTML/doctor badge text. */
29
29
  export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
30
30
 
31
31
  /**
@@ -41,14 +41,9 @@ export function operatingModeTitle(mode, leftoverDesign, stewardsUnset) {
41
41
  return light;
42
42
  }
43
43
 
44
- /** Short HTML/doctor badge text. */
45
- export const LEFTOVER_DESIGN_BADGE = LEFTOVER_DESIGN_LABEL;
46
-
47
44
  export const POST_GREEN_HUMAN =
48
45
  'Imports check out, but the design is still messy. Next: /ark-explore, then one small refactor with /ark-autopilot and your OK.';
49
46
 
50
47
  export const POST_GREEN_LEDE =
51
48
  'Import rules are clean, but leftover design work remains. That does not fail the check — it only means “done” is still wrong until you tidy shape.';
52
49
 
53
- export const HARD_WRITE_HUMAN = 'pre-write block';
54
- export const ADVISORY_WRITE_HUMAN = 'warning only (not blocked)';
@@ -16,14 +16,14 @@ import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
 
18
18
  /** Safety cap so a pathological tree cannot walk forever. */
19
- export const MAX_CONFIG_WALK_DEPTH = 32;
19
+ const MAX_CONFIG_WALK_DEPTH = 32;
20
20
 
21
21
  /**
22
22
  * True when dir looks like a package-manager workspaces / monorepo root.
23
23
  * Used as an upper bound for walk-up (config at this root is accepted; parents are not).
24
24
  * @param {string} dir
25
25
  */
26
- export function isWorkspacesPackageRoot(dir) {
26
+ function isWorkspacesPackageRoot(dir) {
27
27
  const pkgPath = path.join(dir, 'package.json');
28
28
  try {
29
29
  if (!fs.statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) {
@@ -55,7 +55,7 @@ export function isWorkspacesPackageRoot(dir) {
55
55
  * True when dir is a git worktree root (.git file or directory).
56
56
  * @param {string} dir
57
57
  */
58
- export function isGitRoot(dir) {
58
+ function isGitRoot(dir) {
59
59
  try {
60
60
  const git = path.join(dir, '.git');
61
61
  const st = fs.statSync(git, { throwIfNoEntry: false });
@@ -97,7 +97,7 @@ function isFile(absPath) {
97
97
  }
98
98
 
99
99
  /** True when --config is a path (nested or absolute), not a basename to walk. */
100
- export function configNameIsPath(configName) {
100
+ function configNameIsPath(configName) {
101
101
  return (
102
102
  typeof configName === 'string' &&
103
103
  (path.isAbsolute(configName) || /[\\/]/.test(configName))
@@ -118,7 +118,7 @@ function isInsideRoot(root, target) {
118
118
  * @param {string} configName
119
119
  * @returns {string | null} absolute config file path
120
120
  */
121
- export function resolveConfigPathCandidate(startDir, configName) {
121
+ function resolveConfigPathCandidate(startDir, configName) {
122
122
  if (typeof configName !== 'string' || configName.trim() === '') return null;
123
123
  const start = path.resolve(startDir || process.cwd());
124
124
  if (path.isAbsolute(configName)) return isFile(configName) ? path.resolve(configName) : null;
@@ -243,76 +243,45 @@ export function resolveEffectiveProjectRoot(startRoot, opts = {}) {
243
243
  // Writes adopt walked config root only with explicit opt-in.
244
244
  const adoptWalkedRoot = !writeMode || followConfigRoot;
245
245
 
246
+ /** Config found by walk-up (or at start); root follows it unless a plain write. */
247
+ const walkedResult = (found) => ({
248
+ root: adoptWalkedRoot ? found.root : start,
249
+ writeRoot: start,
250
+ config: configName,
251
+ configPath: found.configPath,
252
+ configRoot: found.root,
253
+ walkedUp: found.walkedUp,
254
+ configFound: true,
255
+ writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
256
+ });
257
+ /** Everything stays at the start directory. */
258
+ const startResult = (configPath, configFound) => ({
259
+ root: start,
260
+ writeRoot: start,
261
+ config: configName,
262
+ configPath,
263
+ configRoot: start,
264
+ walkedUp: false,
265
+ configFound,
266
+ writeRootFollowedConfig: false,
267
+ });
268
+
246
269
  if (typeof configName === 'string' && path.isAbsolute(configName)) {
247
270
  const found = findNearestArkConfig(start, configName, opts);
248
- if (found) {
249
- const root = adoptWalkedRoot ? found.root : start;
250
- return {
251
- root,
252
- writeRoot: start,
253
- config: configName,
254
- configPath: found.configPath,
255
- configRoot: found.root,
256
- walkedUp: found.walkedUp,
257
- configFound: true,
258
- writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
259
- };
260
- }
261
- return {
262
- root: start,
263
- writeRoot: start,
264
- config: configName,
265
- configPath: configName,
266
- configRoot: start,
267
- walkedUp: false,
268
- configFound: false,
269
- writeRootFollowedConfig: false,
270
- };
271
+ return found ? walkedResult(found) : startResult(configName, false);
271
272
  }
272
273
 
273
274
  const localPath = path.join(start, configName);
274
275
  try {
275
276
  if (fs.statSync(localPath, { throwIfNoEntry: false })?.isFile()) {
276
- return {
277
- root: start,
278
- writeRoot: start,
279
- config: configName,
280
- configPath: localPath,
281
- configRoot: start,
282
- walkedUp: false,
283
- configFound: true,
284
- writeRootFollowedConfig: false,
285
- };
277
+ return startResult(localPath, true);
286
278
  }
287
279
  } catch {
288
280
  // fall through to walk-up
289
281
  }
290
282
 
291
283
  const found = findNearestArkConfig(start, configName, opts);
292
- if (found) {
293
- const root = adoptWalkedRoot ? found.root : start;
294
- return {
295
- root,
296
- writeRoot: start,
297
- config: configName,
298
- configPath: found.configPath,
299
- configRoot: found.root,
300
- walkedUp: found.walkedUp,
301
- configFound: true,
302
- writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
303
- };
304
- }
305
-
306
- return {
307
- root: start,
308
- writeRoot: start,
309
- config: configName,
310
- configPath: localPath,
311
- configRoot: start,
312
- walkedUp: false,
313
- configFound: false,
314
- writeRootFollowedConfig: false,
315
- };
284
+ return found ? walkedResult(found) : startResult(localPath, false);
316
285
  }
317
286
 
318
287
  /**
@@ -8,6 +8,7 @@
8
8
  * Pure CLI helper (bin/lib/remediation.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
+ import { sliceReasonHint } from './diagnostic-catalog.mjs';
11
12
  export const REMEDIATION_CLASSES = [
12
13
  'mechanical-safe',
13
14
  'judgment',
@@ -67,7 +68,15 @@ export function classifyLayerImportKind(target, extra) {
67
68
  return 'unknown';
68
69
  return 'unknown';
69
70
  }
71
+ /** The reason's fix, plus its advisory note only when this finding is a warning. */
72
+ function sliceHintAction(hint, violation) {
73
+ return violation.failsStrict === false && hint.advisoryNote ? `${hint.fix} ${hint.advisoryNote}` : hint.fix;
74
+ }
70
75
  export function layerImportNextAction(violation) {
76
+ // A nested-wall reason has its own fix. It wins over the generic peer text.
77
+ const hint = sliceReasonHint(typeof violation.reasonId === 'string' ? violation.reasonId : undefined);
78
+ if (hint)
79
+ return `${sliceHintAction(hint, violation)} Then preflight again.`;
71
80
  if (violation.typeOnly || violation.targetTypeOnlyExports || violation.namedBindingsTypeOnly) {
72
81
  return 'Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.';
73
82
  }
@@ -198,6 +207,11 @@ export function deterministicNextAction(violation) {
198
207
  switch (violation.ruleId) {
199
208
  case 'LAYER_IMPORT_VIOLATION':
200
209
  return layerImportNextAction(violation);
210
+ case 'CONFIG_CHILD_SLICES_VERSION':
211
+ // The finding already names the pinned file and the version to bump to.
212
+ return typeof violation.nextAction === 'string' && violation.nextAction.length > 0
213
+ ? violation.nextAction
214
+ : 'Bump the pinned arkgate named in the finding to the version it gives, then run Ark again.';
201
215
  case 'FORBIDDEN_GLOBAL':
202
216
  return `Inject ${violation.target ?? 'the capability'} through a port, test at the public interface, then preflight again.`;
203
217
  case 'CAPABILITY_VIOLATION':
@@ -423,11 +437,13 @@ export function enrichViolationWithFixClass(violation) {
423
437
  ? 'The imported module only exports types — use `import type` and place the type in a layer both sides may share.'
424
438
  : 'This is a type-only import — move the type to a layer both sides may share, or relocate the file to match its role.';
425
439
  }
426
- else if (violation.peerIsolation) {
440
+ else if (violation.peerIsolation || sliceReasonHint(violation.reasonId)) {
441
+ const hint = sliceReasonHint(violation.reasonId);
427
442
  enriched.fixClass = 'cross-slice-boundary';
428
443
  enriched.effort = 'medium';
429
- enriched.enthusiastHint =
430
- 'Cross-slice import blocked (peerIsolation). Do not import another feature/context directly — extract shared code to a shared layer, or coordinate via events/ports. Moving code across slices is a judgment call, not a mechanical auto-fix.';
444
+ enriched.enthusiastHint = hint
445
+ ? `${hint.why} ${sliceHintAction(hint, violation)}`
446
+ : 'Cross-slice import blocked (peerIsolation). Do not import another feature/context directly — extract shared code to a shared layer, or coordinate via events/ports. Moving code across slices is a judgment call, not a mechanical auto-fix.';
431
447
  }
432
448
  else {
433
449
  const kind = classifyLayerImportKind(typeof violation.target === 'string' ? violation.target : '', {
@@ -539,6 +555,7 @@ export function enrichViolationWithFixClass(violation) {
539
555
  case 'ARKORDER_XI_TTL':
540
556
  case 'ARKORDER_STALE_SIGMA':
541
557
  case 'ARKORDER_UNVALVED_RELEASE':
558
+ case 'ARKORDER_STALE_PROPOSAL':
542
559
  enriched.fixClass = 'arkorder-usage';
543
560
  enriched.effort = 'medium';
544
561
  enriched.enthusiastHint =
@@ -560,7 +577,9 @@ export function enrichViolationWithFixClass(violation) {
560
577
  ? 'Refresh σ. ξ does not expire.'
561
578
  : violation.ruleId === 'ARKORDER_UNVALVED_RELEASE'
562
579
  ? 'The choice is already frozen. proposeRelease then apply — do not call release() again with a different value.'
563
- : 'Call createOrderPlane from arkgate/order in a listed plane root so the app actually freezes a pattern.';
580
+ : violation.ruleId === 'ARKORDER_STALE_PROPOSAL'
581
+ ? 'That proposal was computed against an older pattern. Propose again, review the new blast radius, then apply.'
582
+ : 'Call createOrderPlane from arkgate/order in a listed plane root so the app actually freezes a pattern.';
564
583
  break;
565
584
  default:
566
585
  enriched.fixClass = 'review-contract';