agent-harness-kit 0.6.0 → 0.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -0
- package/bin/cli.mjs +15 -1
- package/package.json +1 -1
- package/src/core/detect-stack.mjs +16 -0
- package/src/core/doctor.mjs +23 -0
- package/src/core/render-templates.mjs +198 -6
- package/src/templates/.claude/hooks/hooks.json +111 -0
- package/src/templates/.claude/settings.json.hbs +1 -1
- package/src/templates/.claude/skills/doc-drift-scan/SKILL.md +15 -10
- package/src/templates/.claude/skills/doc-drift-scan/scripts/scan-paths.mjs +64 -0
- package/src/templates/.claude/skills/garbage-collection/SKILL.md.hbs +14 -5
- package/src/templates/.claude/skills/garbage-collection/scripts/gc-classify.mjs +77 -0
- package/src/templates/.claude/skills/inspect-module/SKILL.md.hbs +17 -14
- package/src/templates/.claude/skills/inspect-module/scripts/module-summary.mjs +144 -0
- package/src/templates/CLAUDE.md.hbs +10 -6
- package/src/templates/CLAUDE.md.vi.hbs +74 -0
- package/src/templates/_adapter-kotlin/harness/structural-check.mjs.hbs +286 -0
- package/src/templates/_adapter-rust/harness/structural-check.mjs.hbs +292 -100
- package/src/templates/_adapter-swift/harness/structural-check.mjs.hbs +285 -0
- package/src/templates/harness.config.json.hbs +5 -3
- package/src/templates/scripts/_lib/approx-tokens.mjs +48 -0
- package/src/templates/scripts/_lib/json-pick.mjs +278 -0
- package/src/templates/scripts/harness-report.mjs +95 -1
- package/src/templates/scripts/notify-on-block.sh.hbs +73 -0
- package/src/templates/scripts/pre-compact.sh.hbs +121 -0
- package/src/templates/scripts/pre-push.sh +28 -3
- package/src/templates/scripts/precompletion-checklist.sh.hbs +131 -22
- package/src/templates/scripts/pretooluse-bash-guard.sh.hbs +146 -0
- package/src/templates/scripts/session-end.sh.hbs +48 -0
- package/src/templates/scripts/session-start.sh.hbs +139 -0
- package/src/templates/scripts/statusline.mjs +63 -0
- package/src/templates/scripts/structural-test-on-edit.sh.hbs +31 -8
- package/src/templates/scripts/telemetry-on-skill.sh +32 -10
- package/src/templates/scripts/userprompt-guard.sh.hbs +100 -0
- package/src/templates/.claude/hooks/hooks.json.hbs +0 -39
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Reads harness.config.json. For each domain, walks every .rs file under
|
|
4
4
|
// the domain root (excluding target/, .git/, vendor/) and asserts no
|
|
5
|
-
//
|
|
5
|
+
// `use` statement imports a layer that comes BEFORE the source layer.
|
|
6
6
|
//
|
|
7
7
|
// Two layouts are supported:
|
|
8
8
|
//
|
|
@@ -18,13 +18,20 @@
|
|
|
18
18
|
// `use unibot_types::`). Both default to `{layer}` and preserve the
|
|
19
19
|
// legacy single-crate behavior.
|
|
20
20
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// -
|
|
27
|
-
//
|
|
21
|
+
// Approach (changed in v0.7): a proper Rust lexer-lite. The v0.5 / v0.6
|
|
22
|
+
// implementation ran regex over each line after stripping line comments
|
|
23
|
+
// and double-quoted strings on a per-line basis, which produced false
|
|
24
|
+
// positives on:
|
|
25
|
+
// - multi-line block comments containing `use crate::X`
|
|
26
|
+
// - raw strings (r"...", r#"..."#)
|
|
27
|
+
// - char literals confused with lifetimes
|
|
28
|
+
// and produced false negatives on:
|
|
29
|
+
// - braced use lists: `use crate::{types, service}` (only first ident matched)
|
|
30
|
+
// - nested braces: `use crate::{a::{b, c}, d}`
|
|
31
|
+
// The new approach: first pass blanks out every non-code character
|
|
32
|
+
// (preserving newlines for line numbers); second pass walks the code-only
|
|
33
|
+
// string from each `use`/`pub use` and recursively extracts every
|
|
34
|
+
// candidate-layer identifier through brace expansions.
|
|
28
35
|
//
|
|
29
36
|
// Exit codes:
|
|
30
37
|
// 0 — clean (or only baselined violations; or first-run baseline write)
|
|
@@ -77,13 +84,224 @@ function* walkRustFiles(root) {
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
//
|
|
87
|
+
// stripNonCode — blank out every byte that's inside a comment, string, raw
|
|
88
|
+
// string, or char literal. Newlines are preserved so line numbers stay
|
|
89
|
+
// accurate; everything else inside a skip-zone becomes a single space.
|
|
90
|
+
// This is the layer that catches false positives like
|
|
91
|
+
// /* use crate::service */ or r#"use crate::service"#
|
|
92
|
+
// which the prior per-line regex missed.
|
|
93
|
+
export function stripNonCode(src) {
|
|
94
|
+
const n = src.length;
|
|
95
|
+
const out = new Array(n);
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i < n) {
|
|
98
|
+
const c = src[i];
|
|
99
|
+
const next = src[i + 1];
|
|
100
|
+
// Line comment
|
|
101
|
+
if (c === "/" && next === "/") {
|
|
102
|
+
while (i < n && src[i] !== "\n") {
|
|
103
|
+
out[i] = src[i] === "\n" ? "\n" : " ";
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Block comment (Rust allows nesting)
|
|
109
|
+
if (c === "/" && next === "*") {
|
|
110
|
+
let depth = 1;
|
|
111
|
+
out[i] = " ";
|
|
112
|
+
out[i + 1] = " ";
|
|
113
|
+
i += 2;
|
|
114
|
+
while (i < n && depth > 0) {
|
|
115
|
+
if (src[i] === "/" && src[i + 1] === "*") {
|
|
116
|
+
depth++;
|
|
117
|
+
out[i] = " ";
|
|
118
|
+
out[i + 1] = " ";
|
|
119
|
+
i += 2;
|
|
120
|
+
} else if (src[i] === "*" && src[i + 1] === "/") {
|
|
121
|
+
depth--;
|
|
122
|
+
out[i] = " ";
|
|
123
|
+
out[i + 1] = " ";
|
|
124
|
+
i += 2;
|
|
125
|
+
} else {
|
|
126
|
+
out[i] = src[i] === "\n" ? "\n" : " ";
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Raw string: r"..." or r#..#"..."#..# (also br"..." / br#"..."#)
|
|
133
|
+
if ((c === "r" || (c === "b" && next === "r")) && (i === 0 || !/[a-zA-Z0-9_]/.test(src[i - 1]))) {
|
|
134
|
+
let j = i;
|
|
135
|
+
if (src[j] === "b") {
|
|
136
|
+
out[j] = " ";
|
|
137
|
+
j++;
|
|
138
|
+
}
|
|
139
|
+
if (src[j] === "r") {
|
|
140
|
+
let k = j + 1;
|
|
141
|
+
let hashes = 0;
|
|
142
|
+
while (src[k] === "#") {
|
|
143
|
+
hashes++;
|
|
144
|
+
k++;
|
|
145
|
+
}
|
|
146
|
+
if (src[k] === '"') {
|
|
147
|
+
// Confirmed raw string. Blot from i to closing "#####"
|
|
148
|
+
out[j] = " ";
|
|
149
|
+
for (let q = j + 1; q <= k; q++) out[q] = " ";
|
|
150
|
+
let m = k + 1;
|
|
151
|
+
const closeStr = '"' + "#".repeat(hashes);
|
|
152
|
+
while (m < n) {
|
|
153
|
+
if (src.slice(m, m + closeStr.length) === closeStr) {
|
|
154
|
+
for (let q = m; q < m + closeStr.length; q++) out[q] = " ";
|
|
155
|
+
m += closeStr.length;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
out[m] = src[m] === "\n" ? "\n" : " ";
|
|
159
|
+
m++;
|
|
160
|
+
}
|
|
161
|
+
i = m;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// Not a raw string — `r` was just an identifier letter. Fall through.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Regular string: "..." (handles \", \\, and embedded newlines)
|
|
168
|
+
if (c === '"') {
|
|
169
|
+
out[i] = " ";
|
|
170
|
+
i++;
|
|
171
|
+
while (i < n) {
|
|
172
|
+
if (src[i] === "\\" && i + 1 < n) {
|
|
173
|
+
out[i] = " ";
|
|
174
|
+
out[i + 1] = " ";
|
|
175
|
+
i += 2;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (src[i] === '"') {
|
|
179
|
+
out[i] = " ";
|
|
180
|
+
i++;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
out[i] = src[i] === "\n" ? "\n" : " ";
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Char literal vs lifetime. 'X' or '\X...' is a char; 'name (no closer)
|
|
189
|
+
// is a lifetime. Heuristic: find the closing `'` within the next 6
|
|
190
|
+
// chars; if present AND the body looks like a single char or short
|
|
191
|
+
// escape, treat as char. Otherwise leave as lifetime (raw identifier).
|
|
192
|
+
if (c === "'") {
|
|
193
|
+
// Look for closing '
|
|
194
|
+
let closeAt = -1;
|
|
195
|
+
for (let k = i + 1; k < Math.min(n, i + 8); k++) {
|
|
196
|
+
if (src[k] === "'") {
|
|
197
|
+
closeAt = k;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (src[k] === "\n") break; // lifetime can't span lines
|
|
201
|
+
}
|
|
202
|
+
const body = closeAt > -1 ? src.slice(i + 1, closeAt) : "";
|
|
203
|
+
// Char if body is length 1 (X) OR starts with `\` and is short.
|
|
204
|
+
const isChar =
|
|
205
|
+
closeAt > -1 &&
|
|
206
|
+
(body.length === 1 ||
|
|
207
|
+
(body.startsWith("\\") && body.length <= 6) ||
|
|
208
|
+
/^u\{[0-9a-fA-F]+\}$/.test(body));
|
|
209
|
+
if (isChar) {
|
|
210
|
+
for (let k = i; k <= closeAt; k++) out[k] = src[k] === "\n" ? "\n" : " ";
|
|
211
|
+
i = closeAt + 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Lifetime: pass through as-is.
|
|
215
|
+
out[i] = c;
|
|
216
|
+
i++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// Default: pass character through.
|
|
220
|
+
out[i] = c;
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
return out.join("");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// extractUseTargets — given the code-only text and the start index right
|
|
227
|
+
// after `crate::` (single-crate mode) or after `use ` (workspace mode),
|
|
228
|
+
// return every candidate layer identifier reachable through nested braces.
|
|
229
|
+
// Stops at `;` at top level.
|
|
81
230
|
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
231
|
+
// Examples (single-crate, called with start = position after "crate::"):
|
|
232
|
+
// "service;" → ["service"]
|
|
233
|
+
// "service::Foo;" → ["service"]
|
|
234
|
+
// "{a, b, c};" → ["a", "b", "c"]
|
|
235
|
+
// "{a::Foo, b::{x, y}};" → ["a", "b"]
|
|
236
|
+
// "*;" → []
|
|
237
|
+
// "service::Foo as Bar;" → ["service"]
|
|
238
|
+
export function extractUseTargets(src, start) {
|
|
239
|
+
let i = start;
|
|
240
|
+
function skipWs() {
|
|
241
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
242
|
+
}
|
|
243
|
+
function readIdent() {
|
|
244
|
+
skipWs();
|
|
245
|
+
const m = src.slice(i).match(/^[a-zA-Z_][a-zA-Z0-9_]*/);
|
|
246
|
+
if (!m) return null;
|
|
247
|
+
i += m[0].length;
|
|
248
|
+
return m[0];
|
|
249
|
+
}
|
|
250
|
+
// Parse one "use path item" starting at i, return list of first-ident layers.
|
|
251
|
+
function parseItem() {
|
|
252
|
+
skipWs();
|
|
253
|
+
if (src[i] === "{") {
|
|
254
|
+
i++; // {
|
|
255
|
+
const layers = [];
|
|
256
|
+
while (i < src.length) {
|
|
257
|
+
skipWs();
|
|
258
|
+
if (src[i] === "}") {
|
|
259
|
+
i++;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
// Could be `self`, `super`, `crate`, an ident, or `*`.
|
|
263
|
+
const inner = parseItem();
|
|
264
|
+
layers.push(...inner);
|
|
265
|
+
// Skip ahead to next `,` or `}` at depth 0.
|
|
266
|
+
let depth = 0;
|
|
267
|
+
while (i < src.length) {
|
|
268
|
+
const c = src[i];
|
|
269
|
+
if (c === "{") depth++;
|
|
270
|
+
else if (c === "}") {
|
|
271
|
+
if (depth === 0) break;
|
|
272
|
+
depth--;
|
|
273
|
+
} else if (c === "," && depth === 0) {
|
|
274
|
+
i++;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return layers;
|
|
281
|
+
}
|
|
282
|
+
if (src[i] === "*") {
|
|
283
|
+
i++;
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
const id = readIdent();
|
|
287
|
+
if (!id) return [];
|
|
288
|
+
// `self`, `super`, `crate` aren't layers; they may precede `::layer::...`.
|
|
289
|
+
if (id === "self" || id === "super" || id === "crate") {
|
|
290
|
+
skipWs();
|
|
291
|
+
if (src[i] === ":" && src[i + 1] === ":") {
|
|
292
|
+
i += 2;
|
|
293
|
+
return parseItem();
|
|
294
|
+
}
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
return [id];
|
|
298
|
+
}
|
|
299
|
+
return parseItem();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Returns { layer, domain } or null. Resolves the source file's layer
|
|
303
|
+
// (which dir bucket it lives in) by stripping the domain root prefix and
|
|
304
|
+
// applying the `layerDirPattern`.
|
|
87
305
|
function layerOf(relPath, cfg) {
|
|
88
306
|
for (const d of cfg.domains) {
|
|
89
307
|
const altPrefix = d.root + "/";
|
|
@@ -101,14 +319,10 @@ function layerOf(relPath, cfg) {
|
|
|
101
319
|
return null;
|
|
102
320
|
}
|
|
103
321
|
|
|
104
|
-
// Given a directory name and a pattern like `unibot-{layer}`, return the
|
|
105
|
-
// matching layer name from `layers` (or null). Handles the legacy
|
|
106
|
-
// `{layer}` pattern as the identity case.
|
|
107
322
|
function resolveLayerFromDir(dirName, pattern, layers) {
|
|
108
323
|
if (pattern === "{layer}") {
|
|
109
324
|
return layers.includes(dirName) ? dirName : null;
|
|
110
325
|
}
|
|
111
|
-
// Escape regex specials in the surrounding pattern fragments.
|
|
112
326
|
const [prefix, suffix] = pattern.split("{layer}");
|
|
113
327
|
const pre = (prefix || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114
328
|
const suf = (suffix || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -118,35 +332,7 @@ function resolveLayerFromDir(dirName, pattern, layers) {
|
|
|
118
332
|
return null;
|
|
119
333
|
}
|
|
120
334
|
|
|
121
|
-
// Capture the first identifier after `use ` (or `pub use ...`). The trailing
|
|
122
|
-
// `::` is optional — `use demo_service;` is legal Rust even though most
|
|
123
|
-
// real usage is `use demo_service::foo`.
|
|
124
|
-
const USE_RE = /\b(?:pub\s+)?use\s+([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
125
|
-
|
|
126
|
-
function parseUseTargets(line, domain) {
|
|
127
|
-
const useIdent = domain.useIdentPattern || "crate";
|
|
128
|
-
const matches = [...line.matchAll(USE_RE)];
|
|
129
|
-
const layers = [];
|
|
130
|
-
for (const m of matches) {
|
|
131
|
-
const ident = m[1];
|
|
132
|
-
const layer = resolveLayerFromUseIdent(ident, useIdent, domain.layers);
|
|
133
|
-
if (layer) layers.push(layer);
|
|
134
|
-
}
|
|
135
|
-
return layers;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Map a `use <ident>` to a layer. For single-crate mode this is
|
|
139
|
-
// `use crate::<layer>::...` — `ident == "crate"` and the layer is the
|
|
140
|
-
// SECOND segment. For workspace mode it is `use <crate>::...` where the
|
|
141
|
-
// crate name itself encodes the layer.
|
|
142
335
|
function resolveLayerFromUseIdent(ident, useIdentPattern, layers) {
|
|
143
|
-
if (useIdentPattern === "crate") {
|
|
144
|
-
// Legacy single-crate mode: only `use crate::...` matters; the layer
|
|
145
|
-
// is read from the captured ident only when ident === "crate" but
|
|
146
|
-
// the actual layer name is the segment AFTER `crate::` — see the
|
|
147
|
-
// separate `USE_CRATE_RE` path used by the caller for compatibility.
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
336
|
if (useIdentPattern === "{layer}") {
|
|
151
337
|
return layers.includes(ident) ? ident : null;
|
|
152
338
|
}
|
|
@@ -159,40 +345,51 @@ function resolveLayerFromUseIdent(ident, useIdentPattern, layers) {
|
|
|
159
345
|
return null;
|
|
160
346
|
}
|
|
161
347
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return [...line.matchAll(USE_CRATE_RE)].map((m) => m[1]);
|
|
167
|
-
}
|
|
348
|
+
// USE_RE — matches `use ` and `pub use ` at any position. We scan the
|
|
349
|
+
// code-only string for these tokens, then hand off to extractUseTargets
|
|
350
|
+
// from the position right after the matching prefix.
|
|
351
|
+
const USE_HEAD_RE = /\b(?:pub\s+)?use\s+/g;
|
|
168
352
|
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
353
|
+
// findUseLayers — top-level scanner. For each `use ... ;` in the code-only
|
|
354
|
+
// string, return [{layer, line}].
|
|
355
|
+
function findUseLayers(codeOnly, domain) {
|
|
356
|
+
const out = [];
|
|
357
|
+
const workspaceMode = !!domain.useIdentPattern;
|
|
358
|
+
USE_HEAD_RE.lastIndex = 0;
|
|
359
|
+
let m;
|
|
360
|
+
while ((m = USE_HEAD_RE.exec(codeOnly)) !== null) {
|
|
361
|
+
const after = m.index + m[0].length;
|
|
362
|
+
if (workspaceMode) {
|
|
363
|
+
// First ident after `use ` IS the crate; decode layer from it.
|
|
364
|
+
const id = codeOnly.slice(after).match(/^[a-zA-Z_][a-zA-Z0-9_]*/);
|
|
365
|
+
if (!id) continue;
|
|
366
|
+
const layer = resolveLayerFromUseIdent(id[0], domain.useIdentPattern, domain.layers);
|
|
367
|
+
if (layer) {
|
|
368
|
+
const line = codeOnly.slice(0, m.index).split("\n").length;
|
|
369
|
+
out.push({ layer, line });
|
|
184
370
|
}
|
|
185
|
-
if (c === '"') inStr = false;
|
|
186
371
|
continue;
|
|
187
372
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
373
|
+
// Single-crate: skip optional `crate::` prefix.
|
|
374
|
+
let cursor = after;
|
|
375
|
+
while (cursor < codeOnly.length && /\s/.test(codeOnly[cursor])) cursor++;
|
|
376
|
+
const head = codeOnly.slice(cursor).match(/^[a-zA-Z_][a-zA-Z0-9_]*/);
|
|
377
|
+
if (!head) continue;
|
|
378
|
+
if (head[0] !== "crate") continue; // external crates aren't layer imports
|
|
379
|
+
cursor += head[0].length;
|
|
380
|
+
while (cursor < codeOnly.length && /\s/.test(codeOnly[cursor])) cursor++;
|
|
381
|
+
if (codeOnly[cursor] !== ":" || codeOnly[cursor + 1] !== ":") continue;
|
|
382
|
+
cursor += 2;
|
|
383
|
+
// Now cursor sits at the position right after `crate::`.
|
|
384
|
+
const layers = extractUseTargets(codeOnly, cursor);
|
|
385
|
+
const line = codeOnly.slice(0, m.index).split("\n").length;
|
|
386
|
+
for (const layer of layers) {
|
|
387
|
+
if (domain.layers.includes(layer)) {
|
|
388
|
+
out.push({ layer, line });
|
|
389
|
+
}
|
|
191
390
|
}
|
|
192
|
-
if (c === "/" && i + 1 < line.length && line[i + 1] === "/") break;
|
|
193
|
-
result += c;
|
|
194
391
|
}
|
|
195
|
-
return
|
|
392
|
+
return out;
|
|
196
393
|
}
|
|
197
394
|
|
|
198
395
|
function main() {
|
|
@@ -209,36 +406,27 @@ function main() {
|
|
|
209
406
|
const srcIdx = src.domain.layers.indexOf(src.layer);
|
|
210
407
|
|
|
211
408
|
const content = readFileSync(file, "utf8");
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
violations.push({
|
|
228
|
-
file: relPath,
|
|
229
|
-
line: i + 1,
|
|
230
|
-
from: src.layer,
|
|
231
|
-
to: tgtLayer,
|
|
232
|
-
domain: src.domain.name,
|
|
233
|
-
key,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
409
|
+
const code = stripNonCode(content);
|
|
410
|
+
for (const { layer: tgtLayer, line } of findUseLayers(code, src.domain)) {
|
|
411
|
+
const tgtIdx = src.domain.layers.indexOf(tgtLayer);
|
|
412
|
+
if (tgtIdx === -1) continue;
|
|
413
|
+
if (srcIdx < tgtIdx) {
|
|
414
|
+
const key = `${relPath}::${tgtLayer}`;
|
|
415
|
+
if (baselineSet.has(key)) continue;
|
|
416
|
+
violations.push({
|
|
417
|
+
file: relPath,
|
|
418
|
+
line,
|
|
419
|
+
from: src.layer,
|
|
420
|
+
to: tgtLayer,
|
|
421
|
+
domain: src.domain.name,
|
|
422
|
+
key,
|
|
423
|
+
});
|
|
236
424
|
}
|
|
237
425
|
}
|
|
238
426
|
}
|
|
239
427
|
}
|
|
240
428
|
|
|
241
|
-
// First-run baseline
|
|
429
|
+
// First-run baseline.
|
|
242
430
|
if (!baselineExists && violations.length > 0) {
|
|
243
431
|
mkdirSync(join(repoRoot, ".harness"), { recursive: true });
|
|
244
432
|
const keys = [...new Set(violations.map((v) => v.key))].sort();
|
|
@@ -276,4 +464,8 @@ function main() {
|
|
|
276
464
|
process.exit(2);
|
|
277
465
|
}
|
|
278
466
|
|
|
279
|
-
|
|
467
|
+
// Only run when invoked directly; allow unit tests to import the helpers.
|
|
468
|
+
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
|
469
|
+
if (isMain) {
|
|
470
|
+
main();
|
|
471
|
+
}
|