arkgate 4.8.13 → 4.8.15

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 (126) hide show
  1. package/CHANGELOG.md +219 -4
  2. package/README.md +33 -22
  3. package/SECURITY.md +5 -3
  4. package/bin/ark-check-runtime.mjs +25 -57
  5. package/bin/ark-mcp-runtime.mjs +111 -2
  6. package/bin/ark-shared.mjs +140 -11
  7. package/bin/ark.mjs +7 -25
  8. package/bin/lib/adr-presence.mjs +97 -0
  9. package/bin/lib/agent-projection-formatters.mjs +2 -0
  10. package/bin/lib/agent-skills-package.mjs +241 -8
  11. package/bin/lib/analysis-completeness.mjs +38 -2
  12. package/bin/lib/analysis-engine.mjs +6 -6
  13. package/bin/lib/architecture-scan.mjs +33 -5
  14. package/bin/lib/ark-order-doctor.mjs +7 -1
  15. package/bin/lib/ark-order-report.mjs +2 -1
  16. package/bin/lib/ark-run-doctor.mjs +6 -0
  17. package/bin/lib/ark-run-report.mjs +6 -2
  18. package/bin/lib/arkrules-sensors.mjs +81 -0
  19. package/bin/lib/baseline-key.mjs +4 -1
  20. package/bin/lib/check-args.mjs +52 -2
  21. package/bin/lib/ci-and-commands.mjs +7 -2
  22. package/bin/lib/config-contract.mjs +64 -1
  23. package/bin/lib/design-smells.mjs +21 -1
  24. package/bin/lib/diagnostic-catalog.mjs +5 -3
  25. package/bin/lib/doctor-advisories.mjs +109 -17
  26. package/bin/lib/doctor-human.mjs +20 -6
  27. package/bin/lib/doctor-next-actions.mjs +14 -0
  28. package/bin/lib/doctor-plan.mjs +32 -2
  29. package/bin/lib/enforcement-honesty.mjs +47 -2
  30. package/bin/lib/first-run-help.mjs +19 -3
  31. package/bin/lib/gate-files.mjs +40 -3
  32. package/bin/lib/host-support-matrix.mjs +9 -1
  33. package/bin/lib/html-report-depth.mjs +2 -0
  34. package/bin/lib/html-report.mjs +13 -2
  35. package/bin/lib/import-resolve.mjs +74 -13
  36. package/bin/lib/improvement-compass-doctor.mjs +6 -1
  37. package/bin/lib/improvement-compass-map.mjs +3 -1
  38. package/bin/lib/install-migrate.mjs +23 -0
  39. package/bin/lib/layer-description.mjs +90 -8
  40. package/bin/lib/mcp-hook-payload.mjs +57 -1
  41. package/bin/lib/no-domain-frontend.mjs +91 -0
  42. package/bin/lib/presets.mjs +3 -4
  43. package/bin/lib/product-copy.mjs +19 -0
  44. package/bin/lib/projected-governed-coverage.mjs +114 -0
  45. package/bin/lib/recommend-cli.mjs +54 -0
  46. package/bin/lib/remediation.mjs +6 -2
  47. package/bin/lib/resolved-candidate-facts.mjs +225 -100
  48. package/bin/lib/rules-under-contract.mjs +117 -2
  49. package/bin/lib/scan-files.mjs +39 -0
  50. package/bin/lib/snippet-analysis.mjs +14 -8
  51. package/bin/lib/start-preview.mjs +8 -2
  52. package/bin/lib/states-transitions-presence.mjs +212 -0
  53. package/bin/lib/status-command.mjs +2 -0
  54. package/bin/lib/status-transition-catalog.mjs +410 -0
  55. package/bin/lib/team-parliament-io.mjs +10 -0
  56. package/bin/lib/upgrade-whats-new.mjs +3 -3
  57. package/bin/lib/violations.mjs +43 -1
  58. package/bin/lib/write-path-capabilities.mjs +20 -5
  59. package/bin/lib/write-path-detect.mjs +27 -2
  60. package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
  61. package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
  62. package/dist/eslint/index.cjs +4 -4
  63. package/dist/eslint/index.d.ts +1 -1
  64. package/dist/eslint/index.js +4 -4
  65. package/dist/index.cjs +31 -31
  66. package/dist/index.d.ts +126 -19
  67. package/dist/index.js +31 -31
  68. package/dist/nestjs/index.cjs +5 -5
  69. package/dist/nestjs/index.d.ts +3 -3
  70. package/dist/nestjs/index.js +5 -5
  71. package/dist/runtime/index.cjs +15 -15
  72. package/dist/runtime/index.d.ts +6 -6
  73. package/dist/runtime/index.js +15 -15
  74. package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
  75. package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
  76. package/docs/README.md +8 -6
  77. package/docs/agent-guide.md +35 -17
  78. package/docs/ai-gates.md +13 -4
  79. package/docs/arkorder.md +11 -4
  80. package/docs/brownfield-adoption.md +4 -1
  81. package/docs/configuration.md +63 -14
  82. package/docs/develop.md +4 -1
  83. package/docs/diagnostics.md +30 -10
  84. package/docs/enthusiast/README.md +6 -1
  85. package/docs/enthusiast/how-to-agent-gates.md +5 -0
  86. package/docs/enthusiast/how-to-gallery-starter.md +2 -1
  87. package/docs/enthusiast/how-to-pick-shape.md +1 -1
  88. package/docs/package-surface.md +10 -6
  89. package/docs/product-voice.md +32 -6
  90. package/docs/threat-model.md +2 -2
  91. package/docs/typescript-support.md +3 -3
  92. package/docs/use.md +23 -11
  93. package/package.json +1 -1
  94. package/schemas/ark.config.schema.json +23 -2
  95. package/server.json +2 -2
  96. package/templates/agent-skills/README.md +7 -4
  97. package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
  98. package/templates/agent-skills/ark-architect/SKILL.md +5 -18
  99. package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
  100. package/templates/agent-skills/ark-contract/SKILL.md +9 -20
  101. package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
  102. package/templates/agent-skills/ark-explain/SKILL.md +11 -5
  103. package/templates/agent-skills/ark-explore/SKILL.md +39 -5
  104. package/templates/agent-skills/ark-fix/SKILL.md +15 -20
  105. package/templates/agent-skills/ark-loop/SKILL.md +14 -20
  106. package/templates/agent-skills/ark-order/SKILL.md +200 -0
  107. package/templates/agent-skills/ark-place/SKILL.md +49 -11
  108. package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
  109. package/templates/agent-skills/ark-think/SKILL.md +24 -126
  110. package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
  111. package/templates/arkrules/DomainModel.json +14 -1
  112. package/templates/skills/ark-adopt.md +91 -6
  113. package/templates/skills/ark-architect.md +5 -18
  114. package/templates/skills/ark-autopilot.md +14 -6
  115. package/templates/skills/ark-contract.md +9 -20
  116. package/templates/skills/ark-coverage.md +13 -9
  117. package/templates/skills/ark-explain.md +11 -5
  118. package/templates/skills/ark-explore.md +39 -5
  119. package/templates/skills/ark-fix.md +15 -20
  120. package/templates/skills/ark-loop.md +14 -20
  121. package/templates/skills/ark-order.md +200 -0
  122. package/templates/skills/ark-place.md +49 -11
  123. package/templates/skills/ark-runtime.md +21 -6
  124. package/templates/skills/ark-think.md +24 -126
  125. package/templates/skills/ark-upgrade.md +14 -3
  126. package/templates/tests/ark-adoption-gaps.test.ts +5 -4
@@ -19,7 +19,74 @@ export const FLAT_SKILL_TEMPLATES_RELATIVE_ROOT = 'templates/skills';
19
19
  /** Required entry filename inside each skill directory (Agent Skills standard). */
20
20
  export const AGENT_SKILL_ENTRY_FILENAME = 'SKILL.md';
21
21
  /**
22
- * Closed skill-name freeze (ACS / ADR skill freeze). Exactly these 13 names ship.
22
+ * North-star filter for first-class doors. Not a score. Not enforcement.
23
+ * Skills speak these so an agent picks the right sibling.
24
+ */
25
+ export const ARK_SKILL_NORTH_STAR = Object.freeze(['Contener', 'Guiar', 'Ordenar']);
26
+ /**
27
+ * First-class doors. Each must have crisp when / not when / handoff.
28
+ * Sorted alphabetically.
29
+ */
30
+ export const ARK_FIRST_CLASS_SKILL_NAMES = Object.freeze([
31
+ 'ark-adopt',
32
+ 'ark-autopilot',
33
+ 'ark-coverage',
34
+ 'ark-explain',
35
+ 'ark-explore',
36
+ 'ark-order',
37
+ 'ark-place',
38
+ 'ark-runtime',
39
+ 'ark-upgrade',
40
+ ]);
41
+ /**
42
+ * One-release redirect stubs. Muscle memory / old docs still resolve.
43
+ * Values are first-class doors. Capability surface must stay zero-loss.
44
+ */
45
+ export const ARK_SKILL_STUB_REDIRECTS = Object.freeze({
46
+ 'ark-architect': 'ark-adopt',
47
+ 'ark-contract': 'ark-adopt',
48
+ 'ark-fix': 'ark-autopilot',
49
+ 'ark-loop': 'ark-autopilot',
50
+ 'ark-think': 'ark-explore',
51
+ });
52
+ /**
53
+ * Product surface → first-class doors that exercise it.
54
+ * Standing check: every surface has at least one first-class door whose
55
+ * body mentions that surface. Table keys must match
56
+ * {@link ARK_SKILL_REQUIRED_SURFACES}.
57
+ */
58
+ export const ARK_SKILL_CAPACITY = Object.freeze({
59
+ Layers: ['ark-adopt', 'ark-place', 'ark-explore', 'ark-autopilot', 'ark-coverage', 'ark-explain'],
60
+ ArkRules: ['ark-adopt', 'ark-explore', 'ark-autopilot'],
61
+ ArkRun: ['ark-adopt', 'ark-runtime', 'ark-place', 'ark-autopilot'],
62
+ ArkOrder: ['ark-adopt', 'ark-order', 'ark-place', 'ark-autopilot'],
63
+ Contener: ['ark-adopt', 'ark-place', 'ark-upgrade'],
64
+ Guiar: ['ark-explore', 'ark-autopilot', 'ark-explain', 'ark-coverage', 'ark-runtime'],
65
+ Ordenar: ['ark-order'],
66
+ });
67
+ /**
68
+ * Closed product + north-star surfaces the skill *set* must exercise.
69
+ * Independent of {@link ARK_SKILL_CAPACITY} so deleting a table key fails closed.
70
+ */
71
+ export const ARK_SKILL_REQUIRED_SURFACES = Object.freeze([
72
+ 'Layers',
73
+ 'ArkRules',
74
+ 'ArkRun',
75
+ 'ArkOrder',
76
+ 'Contener',
77
+ 'Guiar',
78
+ 'Ordenar',
79
+ ]);
80
+ /** Named first-class door that must stay on these surfaces (no leftover substitute). */
81
+ export const ARK_SKILL_CAPACITY_DEDICATED_DOORS = Object.freeze({
82
+ ArkRun: 'ark-runtime',
83
+ ArkOrder: 'ark-order',
84
+ Ordenar: 'ark-order',
85
+ });
86
+ const NORTH_STAR_PHRASE = 'Contener · Guiar · Ordenar';
87
+ const FORBIDDEN_ORDER_FREEZE = 'Do not invent `/ark-order`';
88
+ /**
89
+ * Closed shipped catalog (first-class + one-release stubs).
23
90
  * Sorted alphabetically for deterministic inventory diffs.
24
91
  */
25
92
  export const ARK_SKILL_NAMES = Object.freeze([
@@ -32,12 +99,13 @@ export const ARK_SKILL_NAMES = Object.freeze([
32
99
  'ark-explore',
33
100
  'ark-fix',
34
101
  'ark-loop',
102
+ 'ark-order',
35
103
  'ark-place',
36
104
  'ark-runtime',
37
105
  'ark-think',
38
106
  'ark-upgrade',
39
107
  ]);
40
- /** Count of frozen skill names (must stay 13 until a ROADMAP item lifts the freeze). */
108
+ /** Count of shipped skill names (first-class + stubs). */
41
109
  export const ARK_SKILL_NAME_COUNT = ARK_SKILL_NAMES.length;
42
110
  const AGENT_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
43
111
  /**
@@ -51,10 +119,20 @@ export function isValidAgentSkillName(name) {
51
119
  return false;
52
120
  return AGENT_SKILL_NAME_PATTERN.test(name);
53
121
  }
54
- /** True when `name` is one of the frozen 13 Ark skill names. */
122
+ /** True when `name` is in the closed shipped catalog. */
55
123
  export function isArkSkillName(name) {
56
124
  return ARK_SKILL_NAMES.includes(name);
57
125
  }
126
+ /** True when `name` is a first-class door (not a redirect stub). */
127
+ export function isFirstClassArkSkillName(name) {
128
+ return ARK_FIRST_CLASS_SKILL_NAMES.includes(name);
129
+ }
130
+ /** First-class door a leftover name redirects to, or null. */
131
+ export function arkSkillStubRedirect(name) {
132
+ if (!(name in ARK_SKILL_STUB_REDIRECTS))
133
+ return null;
134
+ return ARK_SKILL_STUB_REDIRECTS[name];
135
+ }
58
136
  /**
59
137
  * Parse a skill markdown document with optional YAML frontmatter.
60
138
  * Supports the simple `key: value` / `key: "quoted"` form used by Ark templates
@@ -135,7 +213,7 @@ export function validateAgentSkillDocument(input) {
135
213
  else if (requireArk && !isArkSkillName(name)) {
136
214
  issues.push({
137
215
  code: 'UNKNOWN_SKILL_NAME',
138
- message: `Skill name "${name}" is not in the frozen Ark 13-skill catalog (no new skill names).`,
216
+ message: `Skill name "${name}" is not in the closed Ark skill catalog (ARK_SKILL_NAMES).`,
139
217
  skillName: name,
140
218
  });
141
219
  }
@@ -163,7 +241,7 @@ export function validateAgentSkillDocument(input) {
163
241
  return issues;
164
242
  }
165
243
  /**
166
- * Validate a full Agent Skills package inventory against the frozen 13-name catalog.
244
+ * Validate a full Agent Skills package inventory against the closed catalog.
167
245
  * Detects missing, extra, duplicate, invalid, and (when supplied) flat-template drift.
168
246
  */
169
247
  export function validateAgentSkillsPackage(entries) {
@@ -201,7 +279,7 @@ export function validateAgentSkillsPackage(entries) {
201
279
  if (!seen.has(expected)) {
202
280
  issues.push({
203
281
  code: 'MISSING_SKILL',
204
- message: `Missing frozen skill "${expected}" from Agent Skills package.`,
282
+ message: `Missing catalog skill "${expected}" from Agent Skills package.`,
205
283
  skillName: expected,
206
284
  });
207
285
  }
@@ -212,14 +290,14 @@ export function validateAgentSkillsPackage(entries) {
212
290
  if (!issues.some((i) => i.code === 'UNKNOWN_SKILL_NAME' && i.skillName === name)) {
213
291
  issues.push({
214
292
  code: 'EXTRA_SKILL',
215
- message: `Extra skill "${name}" is not in the frozen Ark 13-skill catalog.`,
293
+ message: `Extra skill "${name}" is not in the closed Ark skill catalog.`,
216
294
  skillName: name,
217
295
  });
218
296
  }
219
297
  else {
220
298
  issues.push({
221
299
  code: 'EXTRA_SKILL',
222
- message: `Extra skill "${name}" is not in the frozen Ark 13-skill catalog.`,
300
+ message: `Extra skill "${name}" is not in the closed Ark skill catalog.`,
223
301
  skillName: name,
224
302
  });
225
303
  }
@@ -234,6 +312,161 @@ export function validateAgentSkillsPackage(entries) {
234
312
  presentCount: names.length,
235
313
  };
236
314
  }
315
+ function pushCapacityIssue(issues, issue) {
316
+ issues.push(issue);
317
+ }
318
+ function skillMentionsSurface(body, surface) {
319
+ return body.includes(surface);
320
+ }
321
+ /**
322
+ * Observe skill bodies (and optional hubs) against the closed product surfaces.
323
+ * Inventory/layout stay on {@link validateAgentSkillsPackage}. This is the
324
+ * fail-closed tooth so a plane cannot silently drop out of the skill set.
325
+ */
326
+ export function validateSkillProductCapacity(input) {
327
+ const issues = [];
328
+ const skills = input.skills ?? {};
329
+ const tableKeys = Object.keys(ARK_SKILL_CAPACITY);
330
+ for (const surface of ARK_SKILL_REQUIRED_SURFACES) {
331
+ if (!tableKeys.includes(surface)) {
332
+ pushCapacityIssue(issues, {
333
+ code: 'CAPACITY_SURFACE_MISSING',
334
+ surface,
335
+ message: `The capacity table dropped ${surface}. Skills must keep covering ` +
336
+ `Layers, ArkRules, ArkRun, ArkOrder plus Contener · Guiar · Ordenar. ` +
337
+ `Put ${surface} back on ARK_SKILL_CAPACITY.`,
338
+ });
339
+ }
340
+ }
341
+ for (const key of tableKeys) {
342
+ if (!ARK_SKILL_REQUIRED_SURFACES.includes(key)) {
343
+ pushCapacityIssue(issues, {
344
+ code: 'CAPACITY_SURFACE_UNMAPPED',
345
+ surface: key,
346
+ message: `The capacity table lists ${key}, which is not a product surface. ` +
347
+ `Keep the table to Layers, ArkRules, ArkRun, ArkOrder and Contener · Guiar · Ordenar.`,
348
+ });
349
+ }
350
+ }
351
+ for (const surface of ARK_SKILL_REQUIRED_SURFACES) {
352
+ const doors = ARK_SKILL_CAPACITY[surface];
353
+ if (!doors || doors.length === 0)
354
+ continue;
355
+ for (const door of doors) {
356
+ if (!isFirstClassArkSkillName(door)) {
357
+ pushCapacityIssue(issues, {
358
+ code: 'CAPACITY_DOOR_NOT_FIRST_CLASS',
359
+ surface,
360
+ skillName: door,
361
+ message: `/${door} is listed for ${surface} but is not a first-class door. ` +
362
+ `Leftover names are shortcuts. Point ${surface} at a first-class skill.`,
363
+ });
364
+ }
365
+ }
366
+ const covering = doors.filter((door) => {
367
+ const body = skills[door];
368
+ return typeof body === 'string' && skillMentionsSurface(body, surface);
369
+ });
370
+ if (covering.length === 0) {
371
+ const hint = surface === 'ArkOrder' || surface === 'Ordenar'
372
+ ? ' — usually /ark-order'
373
+ : surface === 'ArkRun'
374
+ ? ' — usually /ark-runtime'
375
+ : '';
376
+ pushCapacityIssue(issues, {
377
+ code: 'CAPACITY_BODY_GAP',
378
+ surface,
379
+ message: `Skills no longer cover ${surface}. The product has four parts ` +
380
+ `(Layers, ArkRules, ArkRun, ArkOrder) plus Contener · Guiar · Ordenar. ` +
381
+ `Put ${surface} back in a first-class skill${hint} so agents still know when to use it.`,
382
+ });
383
+ }
384
+ }
385
+ for (const [surface, door] of Object.entries(ARK_SKILL_CAPACITY_DEDICATED_DOORS)) {
386
+ const doors = ARK_SKILL_CAPACITY[surface];
387
+ if (!doors?.includes(door) || !isFirstClassArkSkillName(door)) {
388
+ pushCapacityIssue(issues, {
389
+ code: 'CAPACITY_DEDICATED_DOOR',
390
+ surface,
391
+ skillName: door,
392
+ message: `/${door} is the first-class door for ${surface}. Keep it on that surface. ` +
393
+ `Leftover names like /ark-think are shortcuts, not a replacement.`,
394
+ });
395
+ continue;
396
+ }
397
+ const body = skills[door];
398
+ if (typeof body !== 'string' || !skillMentionsSurface(body, surface)) {
399
+ pushCapacityIssue(issues, {
400
+ code: 'CAPACITY_DEDICATED_DOOR',
401
+ surface,
402
+ skillName: door,
403
+ message: `/${door} no longer talks about ${surface}. That door is how agents reach ` +
404
+ `this part of the product. Put ${surface} back in the skill body.`,
405
+ });
406
+ }
407
+ }
408
+ for (const name of ARK_FIRST_CLASS_SKILL_NAMES) {
409
+ const body = skills[name];
410
+ if (typeof body !== 'string') {
411
+ pushCapacityIssue(issues, {
412
+ code: 'CAPACITY_ROUTING_GAP',
413
+ skillName: name,
414
+ message: `/${name} is a first-class door but has no skill body in this check. ` +
415
+ `Ship the template so agents know when to use it.`,
416
+ });
417
+ continue;
418
+ }
419
+ const hasNorthStar = body.includes(NORTH_STAR_PHRASE);
420
+ const hasWhen = /When \/ not when|\*\*When:\*\*/.test(body);
421
+ const hasNotWhen = /Not when|Do \*\*not\*\* use|Prefer instead/.test(body);
422
+ const hasHandoff = /Handoff|hand off|`\/ark-/i.test(body);
423
+ if (!hasNorthStar || !hasWhen || !hasNotWhen || !hasHandoff) {
424
+ pushCapacityIssue(issues, {
425
+ code: 'CAPACITY_ROUTING_GAP',
426
+ skillName: name,
427
+ message: `/${name} is missing when / not when / handoff or Contener · Guiar · Ordenar. ` +
428
+ `First-class skills must say when to use them and which sibling to call next.`,
429
+ });
430
+ }
431
+ if (body.includes(FORBIDDEN_ORDER_FREEZE)) {
432
+ pushCapacityIssue(issues, {
433
+ code: 'CAPACITY_ROUTING_GAP',
434
+ skillName: name,
435
+ message: `/${name} still says not to invent /ark-order. That freeze is outdated — ` +
436
+ `/ark-order is a first-class door.`,
437
+ });
438
+ }
439
+ }
440
+ const hubs = input.hubs ?? {};
441
+ for (const [hub, text] of Object.entries(hubs)) {
442
+ const body = String(text ?? '');
443
+ if (!body.includes('/ark-order')) {
444
+ pushCapacityIssue(issues, {
445
+ code: 'CAPACITY_HUB_GAP',
446
+ hub,
447
+ message: `${hub} no longer names /ark-order. Host instructions must stay at 100% of ` +
448
+ `the product. Add /ark-order back, or you left the order plane behind.`,
449
+ });
450
+ }
451
+ if (!body.includes(NORTH_STAR_PHRASE)) {
452
+ pushCapacityIssue(issues, {
453
+ code: 'CAPACITY_HUB_GAP',
454
+ hub,
455
+ message: `${hub} no longer says Contener · Guiar · Ordenar. Keep that filter in the ` +
456
+ `living docs so agents pick the right sibling.`,
457
+ });
458
+ }
459
+ if (body.includes(FORBIDDEN_ORDER_FREEZE)) {
460
+ pushCapacityIssue(issues, {
461
+ code: 'CAPACITY_HUB_GAP',
462
+ hub,
463
+ message: `${hub} still says not to invent /ark-order. That freeze is outdated — ` +
464
+ `/ark-order is a first-class door.`,
465
+ });
466
+ }
467
+ }
468
+ return { ok: issues.length === 0, issues };
469
+ }
237
470
  /**
238
471
  * Visible package stamp at the start of Agent Skills `description`.
239
472
  * Hosts show `description` in the picker; `arkVersion:` in YAML is invisible there.
@@ -60,8 +60,15 @@ export const EMPTY_ANALYSIS_RULE_ID = 'ANALYSIS_COVERS_NO_FILES';
60
60
  * `countUngovernedSourceFiles` in scan-files.mjs. Feeding it a count that honours
61
61
  * `config.exclude` reopens the false green through `exclude: ["**"]`.
62
62
  *
63
+ * `classifiedFileCount` is optional. When omitted, included files are treated as
64
+ * classified (the historical meaning of `governedFileCount`). When provided and
65
+ * zero while include still matched files, this is the same vacuous green: import
66
+ * rules cannot run on unclassified source. Omit the count when `layers` is empty
67
+ * so `CONFIG_NO_LAYERS` stays the next step. Partial unclassified stays a warning.
68
+ *
63
69
  * @param {{
64
70
  * governedFileCount?: number,
71
+ * classifiedFileCount?: number,
65
72
  * ungovernedSourceCount?: number,
66
73
  * ungovernedSourceCap?: number,
67
74
  * root?: string,
@@ -72,8 +79,29 @@ export const EMPTY_ANALYSIS_RULE_ID = 'ANALYSIS_COVERS_NO_FILES';
72
79
  * @returns {{ ruleId: string, message: string, nextAction: string } | null}
73
80
  */
74
81
  export function emptyAnalysisRefusal(input = {}) {
75
- const count = Number(input.governedFileCount);
76
- if (!Number.isFinite(count) || count !== 0) return null;
82
+ const included = Number(input.governedFileCount);
83
+ if (!Number.isFinite(included)) return null;
84
+
85
+ const classifiedRaw = input.classifiedFileCount;
86
+ const classifiedProvided =
87
+ classifiedRaw !== undefined && classifiedRaw !== null && Number.isFinite(Number(classifiedRaw));
88
+ const classified = classifiedProvided ? Number(classifiedRaw) : included;
89
+
90
+ if (included > 0 && classified === 0) {
91
+ const root = String(input.root ?? '');
92
+ const configPath = String(input.configPath ?? '');
93
+ const message =
94
+ `Analysis covered 0 files: ${included} included file(s) exist under ${root} but none ` +
95
+ `matched a layer pattern in ${configPath}. Every rule is vacuously satisfied on an empty ` +
96
+ 'set, so a pass here would certify nothing.';
97
+ const nextAction =
98
+ `Extend layer patterns or narrow include in ${configPath} so every included file has a ` +
99
+ 'layer. `npx arkgate-check --root . --coverage` lists unclassified files, and `/ark-place` ' +
100
+ 'picks a folder. `--plan` and `--doctor` report this without refusing.';
101
+ return { ruleId: EMPTY_ANALYSIS_RULE_ID, message, nextAction };
102
+ }
103
+
104
+ if (included !== 0) return null;
77
105
 
78
106
  const ungoverned = Math.max(0, Number(input.ungovernedSourceCount) || 0);
79
107
  const cap = Math.max(0, Number(input.ungovernedSourceCap) || 0);
@@ -112,3 +140,11 @@ export function emptyAnalysisRefusal(input = {}) {
112
140
 
113
141
  return { ruleId: EMPTY_ANALYSIS_RULE_ID, message, nextAction };
114
142
  }
143
+
144
+ /** Path-only classified count. `undefined` when no layers — caller keeps the include-only meaning. */
145
+ export function classifiedCountFromFiles(files, layers, layerForFile, root) {
146
+ if (!Array.isArray(layers) || layers.length === 0 || typeof layerForFile !== 'function') {
147
+ return undefined;
148
+ }
149
+ return files.filter((abs) => layerForFile(root, abs, layers)).length;
150
+ }