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.
- package/CHANGELOG.md +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +5 -1
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- 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
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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 {
|
|
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,
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1085
|
-
|
|
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
|
),
|
package/bin/lib/product-copy.mjs
CHANGED
|
@@ -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)';
|
package/bin/lib/project-root.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
:
|
|
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';
|