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
@@ -7,6 +7,7 @@ import {
7
7
  directoryArkRulesReader,
8
8
  loadEffectiveArkRules,
9
9
  loadEffectiveArkRulesFromDisk,
10
+ normalizeProjectRelativePath,
10
11
  objectArkRulesReader,
11
12
  } from './effective-contract-load.mjs';
12
13
  import {
@@ -15,7 +16,9 @@ import {
15
16
  loadInvariantCoverageInputs,
16
17
  } from './invariant-coverage-io.mjs';
17
18
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
19
+ import { attachProbeEvidence } from './invariant-probe-io.mjs';
18
20
  import { attachPolicyAdrNote } from './adr-path.mjs';
21
+ import { collectGovernedFiles } from './scan-files.mjs';
19
22
 
20
23
  function readJsonFile(filePath, label) {
21
24
  if (!fs.existsSync(filePath)) throw new Error(`${label} not found: ${filePath}`);
@@ -95,6 +98,20 @@ function configPathInRepository(root, configPath, top) {
95
98
  * ArkRules files at the base ref, relative to the project root inside the repository.
96
99
  * A path absent at the ref (added by the candidate) reads as `missing`.
97
100
  */
101
+ function baseRefSourceFiles(top, ref, prefix) {
102
+ const shown = runGit(top, ['ls-tree', '-r', '--name-only', ref]);
103
+ if (shown.status !== 0) return [];
104
+ const rootPrefix = prefix ? `${prefix}/` : '';
105
+ const files = [];
106
+ for (const line of shown.stdout.split('\n')) {
107
+ if (!line) continue;
108
+ if (rootPrefix && !line.startsWith(rootPrefix)) continue;
109
+ const rel = rootPrefix ? line.slice(rootPrefix.length) : line;
110
+ if (/\.[cm]?[tj]sx?$/.test(rel) && !rel.endsWith('.d.ts')) files.push(rel);
111
+ }
112
+ return files;
113
+ }
114
+
98
115
  function baseRefArkRulesReader(root, top, ref) {
99
116
  const prefix = path
100
117
  .relative(fs.realpathSync(top), fs.realpathSync(root))
@@ -102,6 +119,7 @@ function baseRefArkRulesReader(root, top, ref) {
102
119
  .join('/');
103
120
  const spec = (rel) => `${ref}:${prefix ? `${prefix}/` : ''}${rel}`;
104
121
  return {
122
+ files: baseRefSourceFiles(top, ref, prefix),
105
123
  readArkRulesFile: (rel) => {
106
124
  const shown = runGit(top, ['show', spec(rel)]);
107
125
  if (shown.status === 0) return { ok: true, content: shown.stdout };
@@ -131,11 +149,16 @@ export function resolvePolicyBaseConfig({
131
149
  // A base config FILE carries its ArkRules next to it (same relative paths as the
132
150
  // project), never from the candidate working tree: that would compare the
133
151
  // candidate catalog with itself and hide every demotion/deletion.
152
+ const baseRoot = fs.realpathSync(path.dirname(absolute));
153
+ const include = Array.isArray(config.include) && config.include.length > 0 ? config.include : ['src'];
134
154
  return {
135
155
  config,
136
156
  source: absolute,
137
157
  ref: null,
138
- readArkRulesFile: directoryArkRulesReader(fs.realpathSync(path.dirname(absolute))),
158
+ files: collectGovernedFiles(baseRoot, { ...config, include })
159
+ .map((file) => path.relative(baseRoot, file).split(path.sep).join('/'))
160
+ .filter((rel) => rel && !rel.startsWith('..')),
161
+ readArkRulesFile: directoryArkRulesReader(baseRoot),
139
162
  };
140
163
  }
141
164
 
@@ -198,9 +221,10 @@ export function readPolicyAcknowledgement(root, acknowledgementPath) {
198
221
  * failure is fail-closed: silently dropping the base catalog would make every
199
222
  * ArkRule demotion or deletion classify as neutral.
200
223
  */
201
- export function loadBaseArkRules(base) {
224
+ function loadBaseArkRules(base) {
202
225
  const loaded = loadEffectiveArkRules(base.config, base.readArkRulesFile, {
203
226
  missingAsEmpty: base.ref != null,
227
+ ...(Array.isArray(base.files) ? { files: base.files } : {}),
204
228
  });
205
229
  if (loaded.errors.length > 0) {
206
230
  const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
@@ -220,6 +244,17 @@ function hasArkRulesMap(config) {
220
244
  return Boolean(refs && typeof refs === 'object' && Object.keys(refs).length > 0);
221
245
  }
222
246
 
247
+ /** Paths in a supplied catalog. A slice rule file is enough for the child wall to name it. */
248
+ function catalogPaths(files) {
249
+ if (!files || typeof files !== 'object') return [];
250
+ const out = [];
251
+ for (const key of Object.keys(files)) {
252
+ const rel = normalizeProjectRelativePath(String(key));
253
+ if (rel) out.push(rel);
254
+ }
255
+ return out;
256
+ }
257
+
223
258
  function loadedOrThrow(loaded, label) {
224
259
  if (loaded.errors.length === 0) return loaded.arkRules;
225
260
  const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
@@ -275,7 +310,9 @@ export function resolvePolicyDeltaArkRules({
275
310
  );
276
311
  }
277
312
  baseArkRules = loadedOrThrow(
278
- loadEffectiveArkRules(baseConfig, objectArkRulesReader(baseArkRuleFiles)),
313
+ loadEffectiveArkRules(baseConfig, objectArkRulesReader(baseArkRuleFiles), {
314
+ files: catalogPaths(baseArkRuleFiles),
315
+ }),
279
316
  'Base'
280
317
  );
281
318
  } else {
@@ -285,7 +322,9 @@ export function resolvePolicyDeltaArkRules({
285
322
  let candidateArkRules;
286
323
  if (candidateArkRuleFiles && typeof candidateArkRuleFiles === 'object') {
287
324
  candidateArkRules = loadedOrThrow(
288
- loadEffectiveArkRules(candidateConfig, objectArkRulesReader(candidateArkRuleFiles)),
325
+ loadEffectiveArkRules(candidateConfig, objectArkRulesReader(candidateArkRuleFiles), {
326
+ files: catalogPaths(candidateArkRuleFiles),
327
+ }),
289
328
  'Candidate'
290
329
  );
291
330
  } else if (candidateIsProjectConfig) {
@@ -347,12 +386,20 @@ export function analyzePolicyTransition({
347
386
  fileContents: coverageInputs.fileContents,
348
387
  testFiles: coverageInputs.testFiles,
349
388
  testGlobsMissing: coverageInputs.testGlobsMissing,
350
- // No coverageStats / coverageRoots: this caller reads coverage ROWS and
351
- // drops the violations, and both only shape violation messages. Passing
352
- // them would look like wiring while changing nothing observable here.
353
389
  coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
390
+ ...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
391
+ // The declared roots set `coverageRootsDeclared` / `outsideDeclaredRoots`
392
+ // on each row, and canPromoteInvariant refuses without them. Same inputs
393
+ // as `--promote` (sensor-promote-io), so both surfaces judge alike.
394
+ ...(coverageInputs.coverageRoots ? { coverageRoots: coverageInputs.coverageRoots } : {}),
354
395
  });
355
- candidateInvariantCoverage = evaluated.coverage;
396
+ // ADR 0039: the same committed probe evidence --promote reads, so both
397
+ // surfaces refuse alike. Read-only; nothing here runs a test.
398
+ candidateInvariantCoverage = attachProbeEvidence(
399
+ root,
400
+ evaluated.coverage,
401
+ candidateArkRules.invariants
402
+ ).rows;
356
403
  }
357
404
 
358
405
  const acknowledgement = readPolicyAcknowledgement(root, acknowledgementPath);
@@ -21,7 +21,7 @@ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
21
21
  export const POST_GREEN_PRIMARY_ACTION = POST_GREEN_HUMAN;
22
22
 
23
23
  /** Short label for tables / metrics. */
24
- export const POST_GREEN_PRIMARY_SHORT =
24
+ const POST_GREEN_PRIMARY_SHORT =
25
25
  '/ark-explore shape-focus → /ark-autopilot (apply one B pilot) # Shape residual';
26
26
 
27
27
  /** Placement coaching while design residual remains (new code only). */
@@ -15,7 +15,7 @@ import {
15
15
  import { effectiveAnalysisConfig } from './analysis-policy.mjs';
16
16
  import { isScanExcludedRelative } from '../ark-shared.mjs';
17
17
  import { classifyChangeSet, evaluateTeamGate } from './team-parliament.mjs';
18
- import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
18
+ import { arkRulesLoadFailed, loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
19
19
  import {
20
20
  coverageOptionsFromConfig,
21
21
  invariantIdsFromCatalog,
@@ -83,7 +83,7 @@ function assertInsideProject(root, relativePath) {
83
83
  }
84
84
  }
85
85
 
86
- export function normalizeChangeSet(input) {
86
+ function normalizeChangeSet(input) {
87
87
  if (!Array.isArray(input)) throw new Error('changes must be an array.');
88
88
  return input.map((change, index) => {
89
89
  if (!change || typeof change !== 'object' || Array.isArray(change)) {
@@ -201,13 +201,7 @@ export function prepareChangeFromRoot({
201
201
  // and a broken reference fails closed instead of silently dropping the catalog.
202
202
  const arkRulesLoad = loadEffectiveArkRulesFromDisk(root, effectiveConfig);
203
203
  if (arkRulesLoad.errors.length > 0) {
204
- const message = arkRulesLoad.errors
205
- .map((issue) => (typeof issue === 'string' ? issue : `- ${issue.path}: ${issue.message}`))
206
- .join('\n');
207
- const error = new Error(`Invalid Effective Contract (${contractSource}):\n${message}`);
208
- error.code = 'ARKRULES_LOAD_FAILED';
209
- error.issues = arkRulesLoad.errors;
210
- throw error;
204
+ throw arkRulesLoadFailed(contractSource, arkRulesLoad.errors);
211
205
  }
212
206
  const contract = loadContract(effectiveConfig, contractSource, {
213
207
  arkRules: arkRulesLoad.arkRules,
@@ -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.
@@ -19,7 +19,7 @@ import { withArkConfigMetadata } from './config-contract.mjs';
19
19
  const PRESETS_DIR = path.dirname(fileURLToPath(import.meta.url));
20
20
  const ARKRULES_TEMPLATES_DIR = path.join(PRESETS_DIR, '../../templates/arkrules');
21
21
 
22
- export function denyUpward(names) {
22
+ function denyUpward(names) {
23
23
  const rules = [];
24
24
  for (let i = 0; i < names.length; i += 1) {
25
25
  for (let j = i + 1; j < names.length; j += 1) {
@@ -33,7 +33,7 @@ export function denyUpward(names) {
33
33
  * peerIsolation matrix: deny only when importer/importee sit under different slices.
34
34
  * Covers same-layer and cross-layer pairs (honest DDD / vertical-slice isolation).
35
35
  */
36
- export function peerIsolationEdges(layerNames, sliceFolders, message) {
36
+ function peerIsolationEdges(layerNames, sliceFolders, message) {
37
37
  const rules = [];
38
38
  for (const from of layerNames) {
39
39
  for (const to of layerNames) {
@@ -57,7 +57,7 @@ export function peerIsolationEdges(layerNames, sliceFolders, message) {
57
57
  // Framework internals under `src/kernel/**` are NOT application architecture — a broad
58
58
  // `src/**/domain/**` would otherwise swallow `src/kernel/domain`. Do NOT use `**/kernel/**`
59
59
  // (that carves out legitimate `src/shared/kernel/**` SharedKernel paths).
60
- export const FRAMEWORK_INTERNAL_EXCLUDE = ['src/kernel/**', '**/src/kernel/**'];
60
+ const FRAMEWORK_INTERNAL_EXCLUDE = ['src/kernel/**', '**/src/kernel/**'];
61
61
 
62
62
  /**
63
63
  * Shared high-spec domain globs. Prefer these over Application/Presentation scatter
@@ -217,18 +217,6 @@ export const APPLICATION_LIB_ORCHESTRATION_PATTERNS = Object.freeze([
217
217
  '**/lib/api-handlers/**',
218
218
  ]);
219
219
 
220
- /**
221
- * AR08 — attach lean arkRules map. Keys are always exact project layer names.
222
- * Sensor roles (domain-structure / orchestration / adapter-thin / generic) are
223
- * independent of display names so renamed layers still get the right starter.
224
- */
225
- export const DEFAULT_ARKRULES_REFS = {
226
- DomainModel: 'arkrules/DomainModel.json',
227
- ApplicationOrchestration: 'arkrules/ApplicationOrchestration.json',
228
- PresentationAdapters: 'arkrules/PresentationAdapters.json',
229
- PersistenceAdapters: 'arkrules/PersistenceAdapters.json',
230
- };
231
-
232
220
  /** Sensor roles used when selecting or synthesizing per-layer templates. */
233
221
  export const ARKRULES_SENSOR_ROLES = Object.freeze({
234
222
  DOMAIN_STRUCTURE: 'domain-structure',
@@ -241,7 +229,7 @@ export const ARKRULES_SENSOR_ROLES = Object.freeze({
241
229
  * Exact-name aliases → sensor role. Prefer this table over heuristics when the
242
230
  * project uses a known vocabulary (hexagonal, monorepo field renames, etc.).
243
231
  */
244
- export const LAYER_SENSOR_ROLE_ALIASES = Object.freeze({
232
+ const LAYER_SENSOR_ROLE_ALIASES = Object.freeze({
245
233
  // Domain / pure model
246
234
  DomainModel: ARKRULES_SENSOR_ROLES.DOMAIN_STRUCTURE,
247
235
  Domain: ARKRULES_SENSOR_ROLES.DOMAIN_STRUCTURE,
@@ -448,7 +436,46 @@ export function writeArkRulesTemplates(root, config, { force = false } = {}) {
448
436
  return written;
449
437
  }
450
438
 
451
- 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) {
452
479
  const config = root ? applyFrameworkLayoutOverlays(baseConfig, root) : baseConfig;
453
480
  return withArkConfigMetadata(withDefaultArkRules(config));
454
481
  }
@@ -511,12 +538,7 @@ export const ARCHITECTURE_PRESETS = {
511
538
  { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
512
539
  { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
513
540
  { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
514
- { from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
515
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
516
- { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
517
- { from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
518
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
519
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
541
+ ...portsAndAdaptersRules(),
520
542
  ],
521
543
  },
522
544
  root
@@ -705,12 +727,7 @@ export const ARCHITECTURE_PRESETS = {
705
727
  { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
706
728
  { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
707
729
  { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
708
- { from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
709
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
710
- { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
711
- { from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
712
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
713
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
730
+ ...portsAndAdaptersRules(),
714
731
  ],
715
732
  },
716
733
  root
@@ -810,24 +827,12 @@ export const ARCHITECTURE_PRESETS = {
810
827
  optional: true,
811
828
  },
812
829
  ],
813
- rules: [
814
- { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
815
- { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
816
- { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
817
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
818
- // Next RSC often imports data clients from routes; deny is ideal but day-one
819
- // ui-surface keeps this as a soft guidance edge (allowed) until ports exist —
820
- // Persistence → Presentation stays denied when that edge appears.
821
- {
822
- from: 'PresentationAdapters',
823
- to: 'PersistenceAdapters',
824
- allowed: true,
825
- message:
826
- 'UI/routes may reach data clients on day one (RSC); prefer application ports as the product grows.',
827
- },
828
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
829
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
830
- ],
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
+ ),
831
836
  },
832
837
  root
833
838
  ),
@@ -978,12 +983,7 @@ export const ARCHITECTURE_PRESETS = {
978
983
  { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
979
984
  { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
980
985
  { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
981
- { from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
982
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
983
- { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
984
- { from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
985
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
986
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
986
+ ...portsAndAdaptersRules(),
987
987
  { from: 'SharedKernel', to: 'DomainModel', allowed: false },
988
988
  { from: 'SharedKernel', to: 'ApplicationOrchestration', allowed: false },
989
989
  { from: 'SharedKernel', to: 'PresentationAdapters', allowed: false },
@@ -1078,21 +1078,9 @@ export const ARCHITECTURE_PRESETS = {
1078
1078
  optional: true,
1079
1079
  },
1080
1080
  ],
1081
- rules: [
1082
- { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
1083
- { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
1084
- { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
1085
- { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
1086
- {
1087
- from: 'PresentationAdapters',
1088
- to: 'PersistenceAdapters',
1089
- allowed: true,
1090
- message:
1091
- 'SPA day-one UI may reach data clients; prefer ports as the product grows.',
1092
- },
1093
- { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
1094
- { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
1095
- ],
1081
+ rules: uiSurfaceRules(
1082
+ 'SPA day-one UI may reach data clients; prefer ports as the product grows.'
1083
+ ),
1096
1084
  },
1097
1085
  root
1098
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
  /**