arkgate 4.8.20 → 4.8.22
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 +172 -5
- package/README.md +107 -117
- package/bin/ark-check-runtime.mjs +3 -4
- package/bin/ark-layer-match.mjs +110 -18
- package/bin/lib/analysis-engine.mjs +13 -6
- package/bin/lib/config-contract.mjs +5 -0
- package/bin/lib/diagnostic-catalog.mjs +1 -0
- package/bin/lib/doctor-advisories.mjs +20 -0
- package/bin/lib/doctor-human.mjs +11 -9
- package/bin/lib/doctor-plan.mjs +15 -14
- package/bin/lib/first-run-help.mjs +7 -1
- package/bin/lib/html-report-advisories.mjs +8 -0
- package/bin/lib/html-report-depth.mjs +9 -8
- package/bin/lib/invariant-coverage.mjs +417 -72
- package/bin/lib/managed-upgrade.mjs +59 -14
- package/bin/lib/pilot-loop.mjs +183 -37
- package/bin/lib/upgrade-behavior-changes.mjs +83 -0
- package/bin/lib/upgrade-command.mjs +69 -31
- package/bin/lib/upgrade-outcome.mjs +359 -0
- package/bin/lib/violations.mjs +109 -0
- package/dist/{configTypes-VVknqcTJ.d.ts → configTypes-CTELo87-.d.ts} +7 -0
- package/dist/{diagnosticCatalog-BPYWNyJR.d.ts → diagnosticCatalog-WD2TPSwZ.d.ts} +78 -5
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +26 -3
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +32 -25
- package/dist/index.d.ts +9 -4
- package/dist/index.js +33 -26
- package/dist/nestjs/index.cjs +9 -9
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +9 -9
- package/dist/runtime/index.cjs +22 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +22 -15
- package/dist/{types-p_fCp6P-.d.ts → types-fQ9-KrBj.d.ts} +1 -1
- package/dist/{types-Cs-46DRu.d.ts → types-yDNEQ603.d.ts} +1 -1
- package/docs/README.md +5 -3
- package/docs/agent-guide.md +15 -7
- package/docs/brownfield-adoption.md +4 -3
- package/docs/configuration.md +34 -11
- package/docs/diagnostics.md +12 -2
- package/docs/package-surface.md +3 -2
- package/docs/product-voice.md +11 -3
- package/package.json +2 -1
- package/schemas/ark.config.schema.json +8 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -2
- package/templates/agent-skills/ark-architect/SKILL.md +2 -0
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -4
- package/templates/agent-skills/ark-contract/SKILL.md +2 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explain/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +4 -2
- package/templates/agent-skills/ark-fix/SKILL.md +2 -0
- package/templates/agent-skills/ark-loop/SKILL.md +2 -0
- package/templates/agent-skills/ark-order/SKILL.md +2 -0
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +2 -0
- package/templates/agent-skills/ark-think/SKILL.md +2 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +20 -7
- package/templates/skills/ark-adopt.md +5 -2
- package/templates/skills/ark-architect.md +2 -0
- package/templates/skills/ark-autopilot.md +6 -4
- package/templates/skills/ark-contract.md +2 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explain.md +2 -0
- package/templates/skills/ark-explore.md +4 -2
- package/templates/skills/ark-fix.md +2 -0
- package/templates/skills/ark-loop.md +2 -0
- package/templates/skills/ark-order.md +2 -0
- package/templates/skills/ark-place.md +2 -0
- package/templates/skills/ark-runtime.md +2 -0
- package/templates/skills/ark-think.md +2 -0
- package/templates/skills/ark-upgrade.md +20 -7
package/bin/ark-layer-match.mjs
CHANGED
|
@@ -218,15 +218,18 @@ export function layerForRelativePath(relPath, layers) {
|
|
|
218
218
|
*
|
|
219
219
|
* A bare name stays that unanchored one-segment match. The child segment is
|
|
220
220
|
* never the filename, so a flat file under the parent is not its own slice.
|
|
221
|
-
* A starred prefix (`lib/features`
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
221
|
+
* A starred prefix (`lib` / `features` / `*` / `*`) reuses the shared-root
|
|
222
|
+
* anchor: it must sit at offset 0, or at offset 1 after `src/` or `app/`.
|
|
223
|
+
* With `sliceIdentity` `"path"` (the default) the slice id includes the
|
|
224
|
+
* literal prefix plus the star bindings (`lib/features/projects/rfi`), so
|
|
225
|
+
* parallel trees get different ids. With `"stars"` it is the last literal
|
|
226
|
+
* plus those star bindings. A star never binds the filename; a file directly
|
|
227
|
+
* under the last bound directory keeps that directory as its slice.
|
|
226
228
|
*/
|
|
227
|
-
export function sliceIdForPath(relPath, sliceFolders) {
|
|
229
|
+
export function sliceIdForPath(relPath, sliceFolders, sliceIdentity) {
|
|
228
230
|
if (!sliceFolders?.length)
|
|
229
231
|
return undefined;
|
|
232
|
+
const identity = sliceIdentity === 'stars' ? 'stars' : 'path';
|
|
230
233
|
const parts = String(relPath)
|
|
231
234
|
.split(/[/\\]/)
|
|
232
235
|
.filter(Boolean);
|
|
@@ -237,7 +240,7 @@ export function sliceIdForPath(relPath, sliceFolders) {
|
|
|
237
240
|
if (typeof raw !== 'string' || raw.length === 0)
|
|
238
241
|
continue;
|
|
239
242
|
if (isAnchoredSliceEntry(raw)) {
|
|
240
|
-
const anchored = anchoredSliceId(parts, raw);
|
|
243
|
+
const anchored = anchoredSliceId(parts, raw, identity);
|
|
241
244
|
if (anchored)
|
|
242
245
|
return anchored;
|
|
243
246
|
continue;
|
|
@@ -257,26 +260,35 @@ function isAnchoredSliceEntry(entry) {
|
|
|
257
260
|
return false;
|
|
258
261
|
return segments.some((part) => part === '*');
|
|
259
262
|
}
|
|
260
|
-
function anchoredSliceId(parts, raw) {
|
|
263
|
+
function anchoredSliceId(parts, raw, identity) {
|
|
261
264
|
const pattern = raw
|
|
262
265
|
.split(/[/\\]/)
|
|
263
266
|
.filter((part) => part.length > 0)
|
|
264
267
|
.map((part) => part.toLowerCase());
|
|
265
268
|
const offsets = anchorOffsets(parts, pattern[0] ?? '');
|
|
266
269
|
for (const offset of offsets) {
|
|
267
|
-
const id = bindAnchoredSlice(parts, pattern, offset);
|
|
270
|
+
const id = bindAnchoredSlice(parts, pattern, offset, identity);
|
|
268
271
|
if (id)
|
|
269
272
|
return id;
|
|
270
273
|
}
|
|
271
274
|
return undefined;
|
|
272
275
|
}
|
|
273
276
|
/**
|
|
274
|
-
* Walk
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
+
* Walk an anchored pattern from `offset`. Literals must match. Each matching
|
|
278
|
+
* literal is part of the bound segments, and each `*` binds one directory.
|
|
279
|
+
* A star stops before the filename; leftover stars then end the id at the
|
|
280
|
+
* last directory that did bind. Segments `lib` / `features` / `*` / `*`
|
|
281
|
+
* (the pattern `lib/features` plus two stars) therefore yield
|
|
282
|
+
* `lib/features/projects/rfi` under `path`.
|
|
283
|
+
*
|
|
284
|
+
* `path` returns every bound segment (literal prefix plus star bindings), so
|
|
285
|
+
* a parallel tree (`components` / `features` / `*` / `*`) gets a different
|
|
286
|
+
* id. `stars` drops the literal prefix before the last literal and keeps
|
|
287
|
+
* that literal plus the star bindings.
|
|
277
288
|
*/
|
|
278
|
-
function bindAnchoredSlice(parts, pattern, offset) {
|
|
289
|
+
function bindAnchoredSlice(parts, pattern, offset, identity = 'path') {
|
|
279
290
|
const bound = [];
|
|
291
|
+
let lastLiteralAt = -1;
|
|
280
292
|
let index = offset;
|
|
281
293
|
for (let pi = 0; pi < pattern.length; pi += 1) {
|
|
282
294
|
const segment = pattern[pi];
|
|
@@ -297,9 +309,87 @@ function bindAnchoredSlice(parts, pattern, offset) {
|
|
|
297
309
|
if (parts[index].toLowerCase() !== segment)
|
|
298
310
|
return undefined;
|
|
299
311
|
bound.push(parts[index].toLowerCase());
|
|
312
|
+
lastLiteralAt = bound.length - 1;
|
|
300
313
|
index += 1;
|
|
301
314
|
}
|
|
302
|
-
|
|
315
|
+
if (bound.length === 0)
|
|
316
|
+
return undefined;
|
|
317
|
+
if (identity === 'stars') {
|
|
318
|
+
if (lastLiteralAt < 0)
|
|
319
|
+
return undefined;
|
|
320
|
+
return bound.slice(lastLiteralAt).join('/');
|
|
321
|
+
}
|
|
322
|
+
return bound.join('/');
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Doctor warning input. Silent unless `sliceIdentity` is `"stars"` and two
|
|
326
|
+
* different anchored prefixes share a stem (last literal plus the stars after it).
|
|
327
|
+
*/
|
|
328
|
+
export function sliceIdentityCollisions(rules) {
|
|
329
|
+
const out = [];
|
|
330
|
+
for (const rule of rules ?? []) {
|
|
331
|
+
if (!rule || rule.sliceIdentity !== 'stars')
|
|
332
|
+
continue;
|
|
333
|
+
const groups = new Map();
|
|
334
|
+
const seen = new Map();
|
|
335
|
+
for (const raw of rule.sliceFolders ?? []) {
|
|
336
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
337
|
+
continue;
|
|
338
|
+
const stem = starsIdentityStem(raw);
|
|
339
|
+
if (!stem)
|
|
340
|
+
continue;
|
|
341
|
+
const key = raw.split(/[/\\]/).filter((part) => part.length > 0).join('/').toLowerCase();
|
|
342
|
+
const groupSeen = seen.get(stem) ?? new Set();
|
|
343
|
+
if (groupSeen.has(key))
|
|
344
|
+
continue;
|
|
345
|
+
groupSeen.add(key);
|
|
346
|
+
seen.set(stem, groupSeen);
|
|
347
|
+
const paths = groups.get(stem) ?? [];
|
|
348
|
+
paths.push(raw);
|
|
349
|
+
groups.set(stem, paths);
|
|
350
|
+
}
|
|
351
|
+
for (const [stem, paths] of groups) {
|
|
352
|
+
if (paths.length < 2)
|
|
353
|
+
continue;
|
|
354
|
+
out.push({
|
|
355
|
+
from: rule.from,
|
|
356
|
+
to: rule.to,
|
|
357
|
+
stem,
|
|
358
|
+
paths,
|
|
359
|
+
message: formatSliceIdentityCollision(rule.from, rule.to, paths, stem),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
/** Last literal plus the stars that follow it. `**` never forms a stem. */
|
|
366
|
+
function starsIdentityStem(raw) {
|
|
367
|
+
if (!isAnchoredSliceEntry(raw))
|
|
368
|
+
return undefined;
|
|
369
|
+
const segments = raw
|
|
370
|
+
.split(/[/\\]/)
|
|
371
|
+
.filter((part) => part.length > 0)
|
|
372
|
+
.map((part) => part.toLowerCase());
|
|
373
|
+
let lastLiteral = -1;
|
|
374
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
375
|
+
const part = segments[i] ?? '';
|
|
376
|
+
if (part === '**')
|
|
377
|
+
return undefined;
|
|
378
|
+
if (part !== '*')
|
|
379
|
+
lastLiteral = i;
|
|
380
|
+
}
|
|
381
|
+
if (lastLiteral < 0)
|
|
382
|
+
return undefined;
|
|
383
|
+
const tail = segments.slice(lastLiteral);
|
|
384
|
+
if (tail.some((part, index) => index > 0 && part !== '*'))
|
|
385
|
+
return undefined;
|
|
386
|
+
return tail.join('/');
|
|
387
|
+
}
|
|
388
|
+
function formatSliceIdentityCollision(from, to, paths, stem) {
|
|
389
|
+
const named = paths.length === 2
|
|
390
|
+
? `${paths[0]} and ${paths[1]}`
|
|
391
|
+
: `${paths.slice(0, -1).join(', ')}, and ${paths[paths.length - 1]}`;
|
|
392
|
+
return `${from} → ${to}: sliceIdentity "stars" gives one slice id to ${named}. They bind as ${stem}.`;
|
|
303
393
|
}
|
|
304
394
|
/** Bare names: leftmost folder, plus the next directory. Never the filename. */
|
|
305
395
|
function bareSliceId(parts, names) {
|
|
@@ -566,8 +656,10 @@ export function findDeniedEdgeDecision(rules, from, to, options) {
|
|
|
566
656
|
const fromPath = options?.fromPath;
|
|
567
657
|
const toPath = options?.toPath;
|
|
568
658
|
const folders = resolveSliceFolders(rule, from, options?.layers);
|
|
569
|
-
const fromSlice = fromPath
|
|
570
|
-
|
|
659
|
+
const fromSlice = fromPath
|
|
660
|
+
? sliceIdForPath(fromPath, folders, rule.sliceIdentity)
|
|
661
|
+
: undefined;
|
|
662
|
+
const toSlice = toPath ? sliceIdForPath(toPath, folders, rule.sliceIdentity) : undefined;
|
|
571
663
|
const decision = peerIsolationDecision({
|
|
572
664
|
fromPath,
|
|
573
665
|
toPath,
|
|
@@ -609,8 +701,8 @@ export function findSharedImportsSliceBridge(rules, from, to, options) {
|
|
|
609
701
|
if (rule.sharedImportsSlice === 'deny')
|
|
610
702
|
continue;
|
|
611
703
|
const folders = resolveSliceFolders(rule, from, options?.layers);
|
|
612
|
-
const fromSlice = sliceIdForPath(fromPath, folders);
|
|
613
|
-
const toSlice = sliceIdForPath(toPath, folders);
|
|
704
|
+
const fromSlice = sliceIdForPath(fromPath, folders, rule.sliceIdentity);
|
|
705
|
+
const toSlice = sliceIdForPath(toPath, folders, rule.sliceIdentity);
|
|
614
706
|
if (fromSlice || !toSlice)
|
|
615
707
|
continue;
|
|
616
708
|
if (!pathUnderSharedRoot(fromPath, rule.sharedRoots))
|