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.
Files changed (75) hide show
  1. package/CHANGELOG.md +172 -5
  2. package/README.md +107 -117
  3. package/bin/ark-check-runtime.mjs +3 -4
  4. package/bin/ark-layer-match.mjs +110 -18
  5. package/bin/lib/analysis-engine.mjs +13 -6
  6. package/bin/lib/config-contract.mjs +5 -0
  7. package/bin/lib/diagnostic-catalog.mjs +1 -0
  8. package/bin/lib/doctor-advisories.mjs +20 -0
  9. package/bin/lib/doctor-human.mjs +11 -9
  10. package/bin/lib/doctor-plan.mjs +15 -14
  11. package/bin/lib/first-run-help.mjs +7 -1
  12. package/bin/lib/html-report-advisories.mjs +8 -0
  13. package/bin/lib/html-report-depth.mjs +9 -8
  14. package/bin/lib/invariant-coverage.mjs +417 -72
  15. package/bin/lib/managed-upgrade.mjs +59 -14
  16. package/bin/lib/pilot-loop.mjs +183 -37
  17. package/bin/lib/upgrade-behavior-changes.mjs +83 -0
  18. package/bin/lib/upgrade-command.mjs +69 -31
  19. package/bin/lib/upgrade-outcome.mjs +359 -0
  20. package/bin/lib/violations.mjs +109 -0
  21. package/dist/{configTypes-VVknqcTJ.d.ts → configTypes-CTELo87-.d.ts} +7 -0
  22. package/dist/{diagnosticCatalog-BPYWNyJR.d.ts → diagnosticCatalog-WD2TPSwZ.d.ts} +78 -5
  23. package/dist/eslint/index.cjs +4 -4
  24. package/dist/eslint/index.d.ts +26 -3
  25. package/dist/eslint/index.js +4 -4
  26. package/dist/index.cjs +32 -25
  27. package/dist/index.d.ts +9 -4
  28. package/dist/index.js +33 -26
  29. package/dist/nestjs/index.cjs +9 -9
  30. package/dist/nestjs/index.d.ts +3 -3
  31. package/dist/nestjs/index.js +9 -9
  32. package/dist/runtime/index.cjs +22 -15
  33. package/dist/runtime/index.d.ts +6 -6
  34. package/dist/runtime/index.js +22 -15
  35. package/dist/{types-p_fCp6P-.d.ts → types-fQ9-KrBj.d.ts} +1 -1
  36. package/dist/{types-Cs-46DRu.d.ts → types-yDNEQ603.d.ts} +1 -1
  37. package/docs/README.md +5 -3
  38. package/docs/agent-guide.md +15 -7
  39. package/docs/brownfield-adoption.md +4 -3
  40. package/docs/configuration.md +34 -11
  41. package/docs/diagnostics.md +12 -2
  42. package/docs/package-surface.md +3 -2
  43. package/docs/product-voice.md +11 -3
  44. package/package.json +2 -1
  45. package/schemas/ark.config.schema.json +8 -0
  46. package/server.json +2 -2
  47. package/templates/agent-skills/README.md +1 -1
  48. package/templates/agent-skills/ark-adopt/SKILL.md +5 -2
  49. package/templates/agent-skills/ark-architect/SKILL.md +2 -0
  50. package/templates/agent-skills/ark-autopilot/SKILL.md +6 -4
  51. package/templates/agent-skills/ark-contract/SKILL.md +2 -0
  52. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  53. package/templates/agent-skills/ark-explain/SKILL.md +2 -0
  54. package/templates/agent-skills/ark-explore/SKILL.md +4 -2
  55. package/templates/agent-skills/ark-fix/SKILL.md +2 -0
  56. package/templates/agent-skills/ark-loop/SKILL.md +2 -0
  57. package/templates/agent-skills/ark-order/SKILL.md +2 -0
  58. package/templates/agent-skills/ark-place/SKILL.md +2 -0
  59. package/templates/agent-skills/ark-runtime/SKILL.md +2 -0
  60. package/templates/agent-skills/ark-think/SKILL.md +2 -0
  61. package/templates/agent-skills/ark-upgrade/SKILL.md +20 -7
  62. package/templates/skills/ark-adopt.md +5 -2
  63. package/templates/skills/ark-architect.md +2 -0
  64. package/templates/skills/ark-autopilot.md +6 -4
  65. package/templates/skills/ark-contract.md +2 -0
  66. package/templates/skills/ark-coverage.md +2 -0
  67. package/templates/skills/ark-explain.md +2 -0
  68. package/templates/skills/ark-explore.md +4 -2
  69. package/templates/skills/ark-fix.md +2 -0
  70. package/templates/skills/ark-loop.md +2 -0
  71. package/templates/skills/ark-order.md +2 -0
  72. package/templates/skills/ark-place.md +2 -0
  73. package/templates/skills/ark-runtime.md +2 -0
  74. package/templates/skills/ark-think.md +2 -0
  75. package/templates/skills/ark-upgrade.md +20 -7
@@ -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` plus one star per extra directory) reuses
222
- * the shared-root anchor: it must sit at offset 0, or at offset 1 after `src/`
223
- * or `app/`. The slice id is the concrete directories the stars bind. A star
224
- * never binds the filename; a file directly under the last bound directory
225
- * keeps that directory as its slice.
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 the prefix. Literals must match. Each star binds one directory and
275
- * stops before the filename; leftover stars then end the id at the last
276
- * directory that did bind.
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
- return bound.length > 0 ? bound.join('/') : undefined;
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 ? sliceIdForPath(fromPath, folders) : undefined;
570
- const toSlice = toPath ? sliceIdForPath(toPath, folders) : undefined;
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))