arkgate 2.6.1 → 2.8.0
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 +62 -0
- package/README.md +8 -3
- package/bin/ark-check.mjs +19 -993
- package/bin/ark-layer-match.mjs +148 -171
- package/bin/ark-shared.mjs +9 -159
- package/bin/lib/agent-gates.mjs +48 -228
- package/bin/lib/architecture-scan.mjs +299 -0
- package/bin/lib/ast-scan.mjs +427 -0
- package/bin/lib/baseline-key.mjs +23 -0
- package/bin/lib/codex-home.mjs +320 -0
- package/bin/lib/config-warnings.mjs +228 -0
- package/bin/lib/doctor-plan.mjs +2 -0
- package/bin/lib/graph-cycles.mjs +56 -0
- package/bin/lib/remediation.mjs +182 -0
- package/bin/lib/scan-files.mjs +69 -0
- package/bin/lib/ts-resolve.mjs +216 -0
- package/bin/lib/violations.mjs +3 -9
- package/dist/eslint/index.cjs +21 -3
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.cts +5 -3
- package/dist/eslint/index.d.ts +5 -3
- package/dist/eslint/index.js +21 -3
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +3080 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.cts +2 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2998 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/{types-DpdVN7Lm.d.cts → types-CP3KkwZt.d.cts} +1 -1
- package/dist/{types-DpdVN7Lm.d.ts → types-CP3KkwZt.d.ts} +1 -1
- package/docs/agent-guide.md +5 -2
- package/docs/ai-gates.md +41 -7
- package/docs/brownfield-adoption.md +7 -0
- package/docs/demos/03-copilot-autopilot.md +3 -2
- package/docs/enthusiast/reference-commands.md +2 -2
- package/docs/migrate-from-ark-runtime-kernel.md +4 -2
- package/docs/package-surface.md +72 -0
- package/docs/production-hardening.md +3 -0
- package/package.json +12 -1
- package/server.json +2 -2
- package/templates/skills/ark-explain.md +3 -2
- package/templates/skills/ark-loop.md +2 -1
package/bin/ark-layer-match.mjs
CHANGED
|
@@ -1,197 +1,174 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/layerMatch.ts
|
|
5
|
+
* Regenerate: node scripts/generate-layer-match.mjs
|
|
6
|
+
* Drift check: node scripts/generate-layer-match.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure layer-glob matching for ark.config.json (CLI load path).
|
|
9
|
+
* CLI-only layerForFile (Node path resolution) is appended below the pure core.
|
|
5
10
|
*/
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
|
|
8
|
-
const _regexpCache = new Map();
|
|
9
11
|
|
|
12
|
+
const regexpCache = new Map();
|
|
10
13
|
function escapeLiteral(ch) {
|
|
11
|
-
|
|
14
|
+
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
12
15
|
}
|
|
13
|
-
|
|
14
|
-
/** True only when every `{` has a matching `}` (ignoring backslash-escaped braces). */
|
|
15
|
-
function bracesBalanced(glob) {
|
|
16
|
-
let depth = 0;
|
|
17
|
-
for (let i = 0; i < glob.length; i += 1) {
|
|
18
|
-
const c = glob[i];
|
|
19
|
-
if (c === '\\') {
|
|
20
|
-
i += 1; // skip the escaped character
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (c === '{') depth += 1;
|
|
24
|
-
else if (c === '}') {
|
|
25
|
-
depth -= 1;
|
|
26
|
-
if (depth < 0) return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return depth === 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
16
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* IMPORTANT: the double-star is expanded in a SINGLE pass. A chained two-step replace
|
|
37
|
-
* (double-star to dot-star, then single-star to a no-slash class) corrupts the double-star,
|
|
38
|
-
* because the second step re-matches the star inside the substitution the first step just
|
|
39
|
-
* inserted. That made "src/kernel/**" stop matching nested paths, silently unclassifying
|
|
40
|
-
* every file in a subdirectory. Scanning one character at a time also lets us support
|
|
41
|
-
* brace alternation ("*.{ts,tsx}") and backslash escapes ("\\{" → literal brace).
|
|
42
|
-
*
|
|
43
|
-
* Brace alternation is only enabled when braces are balanced; an unbalanced brace (a config
|
|
44
|
-
* typo) is treated as a literal so the gate never crashes on `new RegExp`.
|
|
17
|
+
* Normalize path separators to `/` without destroying glob escape sequences.
|
|
18
|
+
* `src\domain\x` → `src/domain/x` (Windows paths); `src/\{legacy\}/**` keeps `\{` / `\}`.
|
|
19
|
+
* A plain `pattern.split('\\').join('/')` would eat those escapes.
|
|
45
20
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (glob[i + 2] === '/') {
|
|
62
|
-
out += '(?:.*/)?'; // `**/` matches zero or more path segments
|
|
63
|
-
i += 2;
|
|
64
|
-
} else {
|
|
65
|
-
out += '.*'; // `**` matches across `/`
|
|
66
|
-
i += 1;
|
|
21
|
+
function normalizeGlobSeparators(pattern) {
|
|
22
|
+
let out = '';
|
|
23
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
24
|
+
const c = pattern[i];
|
|
25
|
+
if (c === '\\' && i + 1 < pattern.length) {
|
|
26
|
+
const next = pattern[i + 1];
|
|
27
|
+
// Keep escapes for glob metacharacters (and escaped backslash).
|
|
28
|
+
if ('*?{}[],'.includes(next) || next === '\\') {
|
|
29
|
+
out += '\\' + next;
|
|
30
|
+
i += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Otherwise treat `\` as a path separator (Windows).
|
|
34
|
+
out += '/';
|
|
35
|
+
continue;
|
|
67
36
|
}
|
|
68
|
-
|
|
69
|
-
out += '[^/]*'; // `*` matches within a single segment
|
|
70
|
-
}
|
|
71
|
-
} else if (c === '?') {
|
|
72
|
-
out += '[^/]';
|
|
73
|
-
} else if (c === '{' && useBraces) {
|
|
74
|
-
out += '(?:';
|
|
75
|
-
braceDepth += 1;
|
|
76
|
-
} else if (c === '}' && useBraces && braceDepth > 0) {
|
|
77
|
-
out += ')';
|
|
78
|
-
braceDepth -= 1;
|
|
79
|
-
} else if (c === ',' && useBraces && braceDepth > 0) {
|
|
80
|
-
out += '|';
|
|
81
|
-
} else {
|
|
82
|
-
out += escapeLiteral(c);
|
|
37
|
+
out += c;
|
|
83
38
|
}
|
|
84
|
-
|
|
85
|
-
const re = new RegExp(`^${out}$`);
|
|
86
|
-
_regexpCache.set(pattern, re);
|
|
87
|
-
return re;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Specificity score for a layer glob: more literal path segments before the first wildcard
|
|
91
|
-
// wins, then longer literal text. So `src/kernel/app/**` (3 literal segments) beats
|
|
92
|
-
// `src/kernel/**` (2), and an exact file like `src/kernel/events.ts` beats both. This is what
|
|
93
|
-
// makes a facade split (a KernelApi surface layer overlapping a KernelInternal catch-all)
|
|
94
|
-
// resolve to the surface REGARDLESS of layer declaration order — the intuitive result.
|
|
95
|
-
export function patternSpecificity(pattern) {
|
|
96
|
-
const glob = String(pattern).split(path.sep).join('/');
|
|
97
|
-
const beforeWildcard = glob.split('*')[0];
|
|
98
|
-
const literalSegments = beforeWildcard.split('/').filter(Boolean).length;
|
|
99
|
-
const literalLength = glob.replace(/\*/g, '').length;
|
|
100
|
-
return literalSegments * 10000 + literalLength;
|
|
39
|
+
return out;
|
|
101
40
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*/
|
|
118
|
-
export function layerForFile(root, file, layers) {
|
|
119
|
-
const abs = path.isAbsolute(file) ? file : path.resolve(root, file);
|
|
120
|
-
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
121
|
-
let bestName;
|
|
122
|
-
let bestScore = -1;
|
|
123
|
-
for (const layer of layers ?? []) {
|
|
124
|
-
if ((layer.exclude ?? []).some((pattern) => globToRegExp(pattern).test(rel))) {
|
|
125
|
-
continue;
|
|
41
|
+
function bracesBalanced(glob) {
|
|
42
|
+
let depth = 0;
|
|
43
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
44
|
+
const c = glob[i];
|
|
45
|
+
if (c === '\\') {
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (c === '{')
|
|
50
|
+
depth += 1;
|
|
51
|
+
else if (c === '}') {
|
|
52
|
+
depth -= 1;
|
|
53
|
+
if (depth < 0)
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
126
56
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
57
|
+
return depth === 0;
|
|
58
|
+
}
|
|
59
|
+
export function globToRegExp(pattern) {
|
|
60
|
+
const cached = regexpCache.get(pattern);
|
|
61
|
+
if (cached)
|
|
62
|
+
return cached;
|
|
63
|
+
const glob = normalizeGlobSeparators(pattern);
|
|
64
|
+
const useBraces = bracesBalanced(glob);
|
|
65
|
+
let out = '';
|
|
66
|
+
let braceDepth = 0;
|
|
67
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
68
|
+
const c = glob[i];
|
|
69
|
+
if (c === '\\' && i + 1 < glob.length) {
|
|
70
|
+
out += escapeLiteral(glob[i + 1]);
|
|
71
|
+
i += 1;
|
|
72
|
+
}
|
|
73
|
+
else if (c === '*') {
|
|
74
|
+
if (glob[i + 1] === '*') {
|
|
75
|
+
if (glob[i + 2] === '/') {
|
|
76
|
+
out += '(?:.*/)?';
|
|
77
|
+
i += 2;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
out += '.*';
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
out += '[^/]*';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (c === '?') {
|
|
89
|
+
out += '[^/]';
|
|
90
|
+
}
|
|
91
|
+
else if (c === '{' && useBraces) {
|
|
92
|
+
out += '(?:';
|
|
93
|
+
braceDepth += 1;
|
|
94
|
+
}
|
|
95
|
+
else if (c === '}' && useBraces && braceDepth > 0) {
|
|
96
|
+
out += ')';
|
|
97
|
+
braceDepth -= 1;
|
|
98
|
+
}
|
|
99
|
+
else if (c === ',' && useBraces && braceDepth > 0) {
|
|
100
|
+
out += '|';
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
out += escapeLiteral(c);
|
|
133
104
|
}
|
|
134
|
-
}
|
|
135
105
|
}
|
|
136
|
-
|
|
137
|
-
|
|
106
|
+
const re = new RegExp(`^${out}$`);
|
|
107
|
+
regexpCache.set(pattern, re);
|
|
108
|
+
return re;
|
|
109
|
+
}
|
|
110
|
+
export function patternSpecificity(pattern) {
|
|
111
|
+
const glob = normalizeGlobSeparators(String(pattern));
|
|
112
|
+
const beforeWildcard = glob.split('*')[0];
|
|
113
|
+
const literalSegments = beforeWildcard.split('/').filter(Boolean).length;
|
|
114
|
+
const literalLength = glob.replace(/\*/g, '').length;
|
|
115
|
+
return literalSegments * 10000 + literalLength;
|
|
138
116
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/** Classify a project-relative path (posix) without needing an absolute root. */
|
|
142
117
|
export function layerForRelativePath(relPath, layers) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
118
|
+
// File paths (not globs): any OS separator → posix relative.
|
|
119
|
+
const rel = String(relPath).split(/[/\\]/).join('/');
|
|
120
|
+
let bestName;
|
|
121
|
+
let bestScore = -1;
|
|
122
|
+
for (const layer of layers ?? []) {
|
|
123
|
+
if ((layer.exclude ?? []).some((pattern) => globToRegExp(pattern).test(rel))) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const pattern of layer.patterns ?? []) {
|
|
127
|
+
if (globToRegExp(pattern).test(rel)) {
|
|
128
|
+
const score = patternSpecificity(pattern);
|
|
129
|
+
if (score > bestScore) {
|
|
130
|
+
bestScore = score;
|
|
131
|
+
bestName = layer.name;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
156
134
|
}
|
|
157
|
-
}
|
|
158
135
|
}
|
|
159
|
-
|
|
160
|
-
return bestName;
|
|
136
|
+
return bestName;
|
|
161
137
|
}
|
|
162
|
-
|
|
163
|
-
/** True when rules[] explicitly deny from→to. Missing rule = allowed (implicit). */
|
|
164
138
|
export function isEdgeDenied(rules, from, to) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
139
|
+
if (from === to)
|
|
140
|
+
return false;
|
|
141
|
+
const hit = (rules ?? []).find((r) => r.from === from && r.to === to);
|
|
142
|
+
return hit?.allowed === false;
|
|
168
143
|
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Codegen / generated source globs skipped by the default scan.
|
|
172
|
-
* Universal (TanStack Router routeTree.gen, many `*.generated.ts` tools, etc.).
|
|
173
|
-
* Opt out with `excludeGenerated: false` in ark.config.json; add more via top-level `exclude`.
|
|
174
|
-
*/
|
|
144
|
+
/** Codegen globs skipped by default scan (emitted into the CLI derived matcher). */
|
|
175
145
|
export const DEFAULT_GENERATED_FILE_GLOBS = [
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
146
|
+
'**/*.gen.ts',
|
|
147
|
+
'**/*.gen.tsx',
|
|
148
|
+
'**/*.generated.ts',
|
|
149
|
+
'**/*.generated.tsx',
|
|
180
150
|
];
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Globs that remove files from ark-check scan (cycles, layers, coverage).
|
|
184
|
-
* @param {{ exclude?: string[], excludeGenerated?: boolean } | null | undefined} config
|
|
185
|
-
*/
|
|
186
151
|
export function scanExcludePatterns(config) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
152
|
+
const custom = Array.isArray(config?.exclude)
|
|
153
|
+
? config.exclude.filter((p) => typeof p === 'string')
|
|
154
|
+
: [];
|
|
155
|
+
const generated = config?.excludeGenerated === false ? [] : DEFAULT_GENERATED_FILE_GLOBS;
|
|
156
|
+
return [...generated, ...custom];
|
|
191
157
|
}
|
|
192
|
-
|
|
193
|
-
/** Relative path (posix) matches any scan-exclude glob. */
|
|
194
158
|
export function isScanExcludedRelative(relPath, config) {
|
|
195
|
-
|
|
196
|
-
|
|
159
|
+
const rel = String(relPath).split(/[/\\]/).join('/');
|
|
160
|
+
return scanExcludePatterns(config).some((pattern) => globToRegExp(pattern).test(rel));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
import path from 'node:path';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a file's architecture layer from ark.config.json layer glob patterns.
|
|
168
|
+
* Uses Node path resolution, then the pure layerForRelativePath classifier.
|
|
169
|
+
*/
|
|
170
|
+
export function layerForFile(root, file, layers) {
|
|
171
|
+
const abs = path.isAbsolute(file) ? file : path.resolve(root, file);
|
|
172
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
173
|
+
return layerForRelativePath(rel, layers);
|
|
197
174
|
}
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -380,7 +380,7 @@ export function collectForbiddenGlobalUses(ts, sourceFile, forbidden) {
|
|
|
380
380
|
return uses;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// Layer glob matching —
|
|
383
|
+
// Layer glob matching — generated from canonical src/domain/layerMatch.ts (see generate:layer-match).
|
|
384
384
|
export {
|
|
385
385
|
globToRegExp,
|
|
386
386
|
patternSpecificity,
|
|
@@ -431,102 +431,15 @@ export function looksLikeIntent(value) {
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
/**
|
|
434
|
-
* Co-pilot Phase F —
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
* - 'mechanical-safe' : behavior-preserving AND gate-verifiable → an agent may auto-apply it.
|
|
438
|
-
* - 'judgment' : real coupling or a design choice → Ark PROPOSES it, a human decides.
|
|
439
|
-
* - 'deferred' : not enough signal to place it → a human should look first.
|
|
440
|
-
*
|
|
441
|
-
* Deliberately biased toward 'judgment': a false 'mechanical-safe' that auto-lands a bad edit
|
|
442
|
-
* is the failure mode that sinks trust. Only statically-provable type-surface fixes earn 'auto':
|
|
443
|
-
* (1) whole source file is pure type-surface + type-only edge → relocate the file
|
|
444
|
-
* (2) import/export already marked type-only → move/re-export the type
|
|
445
|
-
* (3) static value-syntax import of a pure type-only *target* module → import type
|
|
446
|
-
* Pure function of one violation object so CLI, MCP, and apply-loop classify identically.
|
|
447
|
-
* Returns { class, confidence, rationale, remediationKind? }.
|
|
434
|
+
* Co-pilot Phase F — work classifier + fix-class enrich (R4).
|
|
435
|
+
* Canonical TypeScript: src/domain/remediation.ts
|
|
436
|
+
* Generated CLI load path: bin/lib/remediation.mjs (`npm run generate:cli-pure`).
|
|
448
437
|
*/
|
|
449
|
-
export
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// Pure type-only *source file* with a type-only edge: relocating the whole file is
|
|
455
|
-
// behavior-preserving (no runtime body). Distinct from a single import-type move.
|
|
456
|
-
if (violation.typeOnly && violation.sourcePureTypeModule) {
|
|
457
|
-
return {
|
|
458
|
-
class: 'mechanical-safe',
|
|
459
|
-
confidence: 0.88,
|
|
460
|
-
remediationKind: 'pure-type-file-relocate',
|
|
461
|
-
rationale:
|
|
462
|
-
'Whole source file is type-only surface (no runtime statements) with a type-only cross-layer edge: relocate the file to the owning layer (or extract the type there). Behavior-preserving; gate verifies.',
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
if (violation.typeOnly) {
|
|
466
|
-
return {
|
|
467
|
-
class: 'mechanical-safe',
|
|
468
|
-
confidence: 0.9,
|
|
469
|
-
remediationKind: 'type-only-import-move',
|
|
470
|
-
rationale:
|
|
471
|
-
'Type-only import (erased at runtime): move the type to the layer that owns it and re-export for back-compat. Behavior-preserving, and the gate verifies it.',
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
// Target module is a pure type-surface file AND the edge is a static import/export
|
|
475
|
-
// (flag only set on those edges). Value-syntax `import { T }` → convert to import type.
|
|
476
|
-
// require()/import() never get this flag (runtime load). Mixed modules stay judgment.
|
|
477
|
-
if (violation.targetTypeOnlyExports) {
|
|
478
|
-
const kind = violation.edgeKind;
|
|
479
|
-
if (kind === 'require' || kind === 'dynamic-import') {
|
|
480
|
-
return {
|
|
481
|
-
class: 'judgment',
|
|
482
|
-
confidence: 0.75,
|
|
483
|
-
rationale:
|
|
484
|
-
'Runtime module load (require/import()) of a type-only module still executes the target file — not auto-safe; rewrite to a static import type if appropriate.',
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
return {
|
|
488
|
-
class: 'mechanical-safe',
|
|
489
|
-
confidence: 0.85,
|
|
490
|
-
remediationKind: 'import-type-from-pure-type-module',
|
|
491
|
-
rationale:
|
|
492
|
-
'Static import targets a pure type-only module: convert to `import type` (erased at runtime) and place the type in a shared/owning layer. No runtime coupling; gate verifies.',
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
return {
|
|
496
|
-
class: 'judgment',
|
|
497
|
-
confidence: 0.7,
|
|
498
|
-
rationale:
|
|
499
|
-
'Value import — real runtime coupling. Relocating it (e.g. a route reaching the DB → a repository) is a refactor whose organization is a human choice.',
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
if (ruleId === 'FORBIDDEN_GLOBAL') {
|
|
503
|
-
return {
|
|
504
|
-
class: 'judgment',
|
|
505
|
-
confidence: 0.8,
|
|
506
|
-
rationale:
|
|
507
|
-
'Ambient global in a pure layer: inject the capability through a port (Clock, Config, Http). Introducing the port is a design decision.',
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
if (ruleId === 'CIRCULAR_DEPENDENCY') {
|
|
511
|
-
return {
|
|
512
|
-
class: 'judgment',
|
|
513
|
-
confidence: 0.7,
|
|
514
|
-
rationale: 'Dependency cycle: breaking it means deciding which side owns the shared abstraction.',
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
if (typeof ruleId === 'string' && ruleId.length > 0) {
|
|
518
|
-
return {
|
|
519
|
-
class: 'judgment',
|
|
520
|
-
confidence: 0.6,
|
|
521
|
-
rationale: 'Needs a human decision on how to satisfy the contract without weakening the gate.',
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
return {
|
|
525
|
-
class: 'deferred',
|
|
526
|
-
confidence: 0.3,
|
|
527
|
-
rationale: 'Unrecognized violation shape — a human should look before anything is changed.',
|
|
528
|
-
};
|
|
529
|
-
}
|
|
438
|
+
export {
|
|
439
|
+
REMEDIATION_CLASSES,
|
|
440
|
+
classifyRemediation,
|
|
441
|
+
enrichViolationWithFixClass,
|
|
442
|
+
} from './lib/remediation.mjs';
|
|
530
443
|
|
|
531
444
|
/**
|
|
532
445
|
* Normalize a required/imported TypeScript module for ark-check's host.
|
|
@@ -1547,69 +1460,6 @@ export function shouldShowNewHereNudge(root, configPath, governedPercent, config
|
|
|
1547
1460
|
return false;
|
|
1548
1461
|
}
|
|
1549
1462
|
|
|
1550
|
-
/**
|
|
1551
|
-
* Deterministic fix-class labels for JSON output (English, shared with future skills).
|
|
1552
|
-
*/
|
|
1553
|
-
export function enrichViolationWithFixClass(violation) {
|
|
1554
|
-
const enriched = { ...violation };
|
|
1555
|
-
switch (violation.ruleId) {
|
|
1556
|
-
case 'LAYER_IMPORT_VIOLATION':
|
|
1557
|
-
if (violation.typeOnly || violation.targetTypeOnlyExports) {
|
|
1558
|
-
enriched.fixClass = 'file-move';
|
|
1559
|
-
enriched.effort = 'small';
|
|
1560
|
-
enriched.enthusiastHint = violation.targetTypeOnlyExports
|
|
1561
|
-
? 'The imported module only exports types — use `import type` and place the type in a layer both sides may share.'
|
|
1562
|
-
: 'This is a type-only import — move the type to a layer both sides may share, or relocate the file to match its role.';
|
|
1563
|
-
} else {
|
|
1564
|
-
enriched.fixClass = 'port-inversion';
|
|
1565
|
-
enriched.effort = 'medium';
|
|
1566
|
-
enriched.enthusiastHint = `${violation.fromLayer ?? 'This layer'} must not import ${violation.toLayer ?? 'that layer'} directly. Define an interface (port) where you need the capability and inject the implementation from the outer layer.`;
|
|
1567
|
-
}
|
|
1568
|
-
break;
|
|
1569
|
-
case 'FORBIDDEN_GLOBAL':
|
|
1570
|
-
enriched.fixClass = 'inject-port';
|
|
1571
|
-
enriched.effort = 'small';
|
|
1572
|
-
enriched.enthusiastHint = `Do not call "${violation.target ?? 'that global'}" here. Pass the capability in through a small interface (for example a Clock, HttpPort, or Config provider).`;
|
|
1573
|
-
break;
|
|
1574
|
-
case 'RAW_EVENT_PUBLISH':
|
|
1575
|
-
enriched.fixClass = 'registered-intent';
|
|
1576
|
-
enriched.effort = 'small';
|
|
1577
|
-
enriched.enthusiastHint =
|
|
1578
|
-
'Register the event intent first, then publish through the creator returned by the registry — not a raw string or object.';
|
|
1579
|
-
break;
|
|
1580
|
-
case 'PUBLISH_MISSING_SOURCE':
|
|
1581
|
-
enriched.fixClass = 'add-source-metadata';
|
|
1582
|
-
enriched.effort = 'small';
|
|
1583
|
-
enriched.enthusiastHint =
|
|
1584
|
-
'Add metadata.source to the publish call so Ark knows which layer is publishing the event.';
|
|
1585
|
-
break;
|
|
1586
|
-
case 'PUBLISH_SOURCE_LAYER_MISMATCH':
|
|
1587
|
-
enriched.fixClass = 'fix-source-layer';
|
|
1588
|
-
enriched.effort = 'small';
|
|
1589
|
-
enriched.enthusiastHint =
|
|
1590
|
-
'Use a source intent that belongs to the same layer as this file, or move the publish call to the layer that owns the source.';
|
|
1591
|
-
break;
|
|
1592
|
-
case 'LAYER_INTENT_REFERENCE_VIOLATION':
|
|
1593
|
-
enriched.fixClass = 'intent-relocation';
|
|
1594
|
-
enriched.effort = 'small';
|
|
1595
|
-
enriched.enthusiastHint =
|
|
1596
|
-
'Reference that intent from a layer allowed to know about it — usually an adapter or application layer, not the domain core.';
|
|
1597
|
-
break;
|
|
1598
|
-
case 'CIRCULAR_DEPENDENCY':
|
|
1599
|
-
enriched.fixClass = 'break-cycle';
|
|
1600
|
-
enriched.effort = 'medium';
|
|
1601
|
-
enriched.enthusiastHint =
|
|
1602
|
-
'Two modules import each other in a loop. Extract shared code, invert one dependency behind a port, or merge them if they are really one unit.';
|
|
1603
|
-
break;
|
|
1604
|
-
default:
|
|
1605
|
-
enriched.fixClass = 'review-contract';
|
|
1606
|
-
enriched.effort = 'small';
|
|
1607
|
-
enriched.enthusiastHint =
|
|
1608
|
-
'Read the violation message and the layer rules in ark.config.json, then adjust imports or move code to the correct layer.';
|
|
1609
|
-
}
|
|
1610
|
-
return enriched;
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
1463
|
export function formatArchitectureRecommendationHuman(recommendation) {
|
|
1614
1464
|
const lines = [];
|
|
1615
1465
|
lines.push('Ark architecture recommendation (application shape, not vendor stack)');
|