arkgate 4.8.24 → 4.8.26

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 (166) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +2 -2
  3. package/bin/ark-check-runtime.mjs +17 -14
  4. package/bin/ark-layer-match.mjs +115 -13
  5. package/bin/ark-mcp-runtime.mjs +36 -31
  6. package/bin/ark-shared.mjs +4 -23
  7. package/bin/lib/adoption-stance.mjs +2 -2
  8. package/bin/lib/adr-path.mjs +3 -3
  9. package/bin/lib/agent-gates.mjs +0 -61
  10. package/bin/lib/agent-homes.mjs +12 -21
  11. package/bin/lib/agent-projection-command.mjs +4 -4
  12. package/bin/lib/ambient-state.mjs +9 -30
  13. package/bin/lib/analysis-engine.mjs +14 -14
  14. package/bin/lib/architecture-scan.mjs +11 -9
  15. package/bin/lib/ark-order-doctor.mjs +3 -28
  16. package/bin/lib/ark-order-facts.mjs +6 -99
  17. package/bin/lib/ark-order-types.mjs +1 -2
  18. package/bin/lib/ark-run-doctor.mjs +3 -28
  19. package/bin/lib/ark-run-facts.mjs +30 -40
  20. package/bin/lib/ark-run-sensors.mjs +1 -1
  21. package/bin/lib/arkrule-file-hints.mjs +2 -47
  22. package/bin/lib/arkrules-contract.mjs +124 -133
  23. package/bin/lib/arkrules-sensors.mjs +7 -5
  24. package/bin/lib/check-args.mjs +63 -1
  25. package/bin/lib/ci-and-commands.mjs +8 -72
  26. package/bin/lib/ci-merge-boundary.mjs +1 -1
  27. package/bin/lib/class-source-scan.mjs +5 -5
  28. package/bin/lib/clone-detection.mjs +474 -0
  29. package/bin/lib/codex-home.mjs +1 -1
  30. package/bin/lib/config-contract-slices.mjs +44 -6
  31. package/bin/lib/config-contract.mjs +40 -117
  32. package/bin/lib/config-extras.mjs +16 -22
  33. package/bin/lib/contract-smells.mjs +33 -15
  34. package/bin/lib/core-ratchet.mjs +0 -2
  35. package/bin/lib/dashboard-cli.mjs +2 -2
  36. package/bin/lib/deep-module-coach.mjs +2 -4
  37. package/bin/lib/deploy-path.mjs +13 -38
  38. package/bin/lib/design-smells.mjs +2 -2
  39. package/bin/lib/diagnostic-catalog.mjs +8 -1
  40. package/bin/lib/doctor-advisories.mjs +55 -9
  41. package/bin/lib/doctor-green-cite.mjs +1 -1
  42. package/bin/lib/doctor-human.mjs +1 -1
  43. package/bin/lib/doctor-next-actions.mjs +2 -2
  44. package/bin/lib/doctor-plan.mjs +4 -6
  45. package/bin/lib/duplication-io.mjs +572 -0
  46. package/bin/lib/effective-contract-load.mjs +181 -44
  47. package/bin/lib/entry-points-io.mjs +424 -0
  48. package/bin/lib/extra-merge-teeth.mjs +36 -0
  49. package/bin/lib/field-install.mjs +38 -14
  50. package/bin/lib/first-run-help.mjs +7 -3
  51. package/bin/lib/flat-parent-importers.mjs +5 -3
  52. package/bin/lib/gate-files.mjs +30 -89
  53. package/bin/lib/github-enforcement.mjs +165 -28
  54. package/bin/lib/golden-pattern.mjs +1 -1
  55. package/bin/lib/hook-templates.mjs +16 -16
  56. package/bin/lib/html-report-advisories.mjs +4 -0
  57. package/bin/lib/html-report-depth.mjs +2 -2
  58. package/bin/lib/html-report.mjs +2 -2
  59. package/bin/lib/import-graph-projection.mjs +112 -0
  60. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  61. package/bin/lib/invariant-coverage-io.mjs +4 -3
  62. package/bin/lib/invariant-coverage.mjs +26 -0
  63. package/bin/lib/invariant-probe-cli.mjs +347 -0
  64. package/bin/lib/invariant-probe-io.mjs +165 -0
  65. package/bin/lib/invariant-probe-runner.mjs +268 -0
  66. package/bin/lib/invariant-probe-sites.mjs +174 -0
  67. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  68. package/bin/lib/invariant-probe.mjs +461 -0
  69. package/bin/lib/invariant-tests-path.mjs +16 -22
  70. package/bin/lib/literal-path-drift.mjs +4 -2
  71. package/bin/lib/managed-upgrade.mjs +2 -8
  72. package/bin/lib/mcp-adoption.mjs +4 -5
  73. package/bin/lib/mcp-hook-payload.mjs +4 -4
  74. package/bin/lib/orphan-modules-io.mjs +458 -0
  75. package/bin/lib/orphan-modules.mjs +251 -0
  76. package/bin/lib/outside-importers.mjs +197 -0
  77. package/bin/lib/package-manager.mjs +3 -3
  78. package/bin/lib/parse-health.mjs +1 -1
  79. package/bin/lib/persistence-write-hint.mjs +33 -8
  80. package/bin/lib/pin-dependent-gates.mjs +2 -2
  81. package/bin/lib/policy-delta-io.mjs +60 -12
  82. package/bin/lib/post-green-path.mjs +1 -1
  83. package/bin/lib/prepare-change.mjs +3 -9
  84. package/bin/lib/prepare-write.mjs +1 -2
  85. package/bin/lib/presets.mjs +56 -68
  86. package/bin/lib/product-copy.mjs +1 -6
  87. package/bin/lib/project-root.mjs +43 -85
  88. package/bin/lib/resolved-candidate-facts.mjs +90 -62
  89. package/bin/lib/rules-under-contract.mjs +72 -8
  90. package/bin/lib/scan-files.mjs +4 -4
  91. package/bin/lib/schema-validation.mjs +130 -0
  92. package/bin/lib/sensor-promote-cli.mjs +2 -11
  93. package/bin/lib/sensor-promote-io.mjs +33 -22
  94. package/bin/lib/sensor-promotion.mjs +1 -1
  95. package/bin/lib/skill-catalog-apply.mjs +1 -1
  96. package/bin/lib/skill-install.mjs +42 -38
  97. package/bin/lib/snippet-analysis.mjs +1 -1
  98. package/bin/lib/start-install-recovery.mjs +1 -1
  99. package/bin/lib/start-preview.mjs +1 -1
  100. package/bin/lib/states-transitions-presence.mjs +3 -3
  101. package/bin/lib/status-command.mjs +2 -18
  102. package/bin/lib/status-manifest.mjs +18 -26
  103. package/bin/lib/status-transition-catalog.mjs +0 -6
  104. package/bin/lib/suggestions.mjs +3 -3
  105. package/bin/lib/team-parliament-io.mjs +3 -17
  106. package/bin/lib/team-parliament.mjs +15 -9
  107. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  108. package/bin/lib/upgrade-command.mjs +2 -24
  109. package/bin/lib/upgrade-outcome.mjs +1 -1
  110. package/bin/lib/upstream-report.mjs +21 -10
  111. package/bin/lib/violations.mjs +10 -9
  112. package/bin/lib/weakest-link.mjs +13 -0
  113. package/bin/lib/write-path-capabilities.mjs +2 -2
  114. package/dist/chunk-G3JRBJGH.js +16 -0
  115. package/dist/chunk-R3AJ5RSZ.cjs +16 -0
  116. package/dist/{configTypes-BJbaeoj4.d.cts → configTypes-GPvvdleM.d.cts} +12 -1
  117. package/dist/{configTypes-BJbaeoj4.d.ts → configTypes-GPvvdleM.d.ts} +12 -1
  118. package/dist/eslint/index.cjs +11 -11
  119. package/dist/eslint/index.d.cts +10 -1
  120. package/dist/eslint/index.d.ts +10 -1
  121. package/dist/eslint/index.js +11 -11
  122. package/dist/index.cjs +34 -34
  123. package/dist/index.d.cts +587 -515
  124. package/dist/index.d.ts +587 -515
  125. package/dist/index.js +34 -34
  126. package/dist/nestjs/index.cjs +1 -1
  127. package/dist/nestjs/index.d.cts +1 -1
  128. package/dist/nestjs/index.d.ts +1 -1
  129. package/dist/nestjs/index.js +1 -1
  130. package/dist/order/index.cjs +1 -1
  131. package/dist/order/index.js +1 -1
  132. package/dist/runtime/index.cjs +12 -12
  133. package/dist/runtime/index.d.cts +174 -136
  134. package/dist/runtime/index.d.ts +174 -136
  135. package/dist/runtime/index.js +12 -12
  136. package/dist/{types-DRinG-vT.d.cts → types-CnauMQQL.d.cts} +33 -25
  137. package/dist/{types-DRinG-vT.d.ts → types-CnauMQQL.d.ts} +33 -25
  138. package/docs/README.md +3 -1
  139. package/docs/agent-guide.md +140 -6
  140. package/docs/ai-gates.md +15 -1
  141. package/docs/configuration.md +40 -11
  142. package/docs/diagnostics.md +119 -3
  143. package/docs/package-surface.md +4 -1
  144. package/docs/product-voice.md +7 -1
  145. package/package.json +11 -2
  146. package/schemas/ark.config.schema.json +31 -3
  147. package/schemas/ark.invariant-probe.schema.json +368 -0
  148. package/server.json +2 -2
  149. package/templates/agent-skills/README.md +1 -1
  150. package/templates/agent-skills/ark-adopt/SKILL.md +1 -0
  151. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  152. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  153. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  154. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  155. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  156. package/templates/skills/ark-adopt.md +1 -0
  157. package/templates/skills/ark-contract.md +17 -0
  158. package/templates/skills/ark-coverage.md +2 -0
  159. package/templates/skills/ark-explore.md +7 -0
  160. package/templates/skills/ark-fix.md +20 -0
  161. package/templates/skills/ark-place.md +8 -0
  162. package/bin/lib/ark-order-error.mjs +0 -18
  163. package/bin/lib/ark-order-invariants.mjs +0 -388
  164. package/bin/lib/stable-hash.mjs +0 -90
  165. package/dist/chunk-MBXCFLO4.js +0 -16
  166. package/dist/chunk-RNQHRDCQ.cjs +0 -16
@@ -7,7 +7,6 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { arkCommand } from '../ark-shared.mjs';
9
9
  import {
10
- parseSkillDescriptionVersion,
11
10
  stampSkillDescription,
12
11
  stripSkillDescriptionVersion,
13
12
  } from './agent-skills-package.mjs';
@@ -227,14 +226,14 @@ export function canonicalSkillPath(name) {
227
226
  * Hosts that natively load `.agents/skills` — do not also copy bytes there under
228
227
  * a second name. Cursor/Codex list every path they scan; two copies = two picker rows.
229
228
  */
230
- export const SKILL_NATIVE_AGENTS_HOSTS = Object.freeze(['codex', 'cursor', 'antigravity']);
229
+ const SKILL_NATIVE_AGENTS_HOSTS = Object.freeze(['codex', 'cursor', 'antigravity']);
231
230
 
232
231
  /**
233
232
  * Hosts that do not scan `.agents/skills`. Adapter is a relative symlink to the
234
233
  * canonical catalog so Grok/Claude/OpenCode see the same bytes.
235
234
  * Cursor also scans `.claude/skills` — doctor warns; still one body + visible version.
236
235
  */
237
- export const SKILL_ADAPTER_LINKS = Object.freeze({
236
+ const SKILL_ADAPTER_LINKS = Object.freeze({
238
237
  claude: (name) => ({
239
238
  link: `.claude/skills/${name}`,
240
239
  target: `../../${SKILL_CANONICAL_DIR}/${name}`,
@@ -299,10 +298,10 @@ export function arkPackageVersion() {
299
298
  }
300
299
 
301
300
  /**
302
- * Rewrite a YAML `description:` line with a visible `arkgate@<version>. ` prefix.
303
- * Preserves quoting. Hosts show this string in the skill picker (unlike arkVersion).
301
+ * Rewrite the value of a YAML `description:` line, preserving its quoting.
302
+ * `transform` returns the new value, or null to keep the line untouched.
304
303
  */
305
- function stampDescriptionYamlLine(line, version) {
304
+ function rewriteDescriptionYamlLine(line, transform) {
306
305
  const match = String(line).match(/^(description:\s*)(.*)$/);
307
306
  if (!match) return line;
308
307
  let raw = match[2] ?? '';
@@ -311,26 +310,26 @@ function stampDescriptionYamlLine(line, version) {
311
310
  (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2);
312
311
  const quote = quoted ? raw[0] : '';
313
312
  const value = quoted ? raw.slice(1, -1) : raw;
314
- const stamped = stampSkillDescription(value, version);
315
- if (!quoted) return `${match[1]}${stamped}`;
316
- const escaped = stamped.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
313
+ const next = transform(value);
314
+ if (next === null) return line;
315
+ if (!quoted) return `${match[1]}${next}`;
316
+ const escaped = next.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
317
317
  return `${match[1]}${quote}${escaped}${quote}`;
318
318
  }
319
319
 
320
+ /**
321
+ * Rewrite a YAML `description:` line with a visible `arkgate@<version>. ` prefix.
322
+ * Preserves quoting. Hosts show this string in the skill picker (unlike arkVersion).
323
+ */
324
+ function stampDescriptionYamlLine(line, version) {
325
+ return rewriteDescriptionYamlLine(line, (value) => stampSkillDescription(value, version));
326
+ }
327
+
320
328
  function managedDescriptionYamlLine(line) {
321
- const match = String(line).match(/^(description:\s*)(.*)$/);
322
- if (!match) return line;
323
- let raw = match[2] ?? '';
324
- const quoted =
325
- (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
326
- (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2);
327
- const quote = quoted ? raw[0] : '';
328
- const value = quoted ? raw.slice(1, -1) : raw;
329
- const stripped = stripSkillDescriptionVersion(value);
330
- if (stripped === value) return line;
331
- if (!quoted) return `${match[1]}${stripped}`;
332
- const escaped = stripped.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
333
- return `${match[1]}${quote}${escaped}${quote}`;
329
+ return rewriteDescriptionYamlLine(line, (value) => {
330
+ const stripped = stripSkillDescriptionVersion(value);
331
+ return stripped === value ? null : stripped;
332
+ });
334
333
  }
335
334
 
336
335
  // Insert `arkVersion: <v>` and a visible `arkgate@<v>. ` description prefix.
@@ -593,7 +592,7 @@ export function skillTemplates() {
593
592
  * Relative symlink so clones keep working. Fallback copy when the OS refuses links.
594
593
  * @returns {'linked'|'copied'|'current'|'skipped-customized'|'missing-canonical'}
595
594
  */
596
- export function ensureSkillAdapterLink(root, name, adapter, force = false) {
595
+ function ensureSkillAdapterLink(root, name, adapter, force = false) {
597
596
  const canonicalDir = path.join(root, SKILL_CANONICAL_DIR, name);
598
597
  const canonicalFile = path.join(canonicalDir, 'SKILL.md');
599
598
  if (!fs.existsSync(canonicalFile)) return 'missing-canonical';
@@ -693,8 +692,6 @@ export function pruneHomeArkSkillDuplicates(root, skillNames = skillTemplateName
693
692
  return { ok: true, reason: 'pruned', removed };
694
693
  }
695
694
 
696
- export { parseSkillDescriptionVersion, stripSkillDescriptionVersion };
697
-
698
695
  // Skill names only, silent on a missing templates dir — for the freshness
699
696
  // advisory below, which must not print packaging warnings on every check run.
700
697
  export function skillTemplateNames() {
@@ -821,20 +818,15 @@ function readCodexHomeCatalogMetadata(file, kind) {
821
818
  }
822
819
  }
823
820
 
824
- function codexHomeCatalogState(skillsDir) {
825
- const catalog = readCodexHomeCatalogMetadata(
826
- path.join(skillsDir, CODEX_HOME_CATALOG),
827
- 'catalog'
828
- );
829
- const pending = readCodexHomeCatalogMetadata(
830
- path.join(skillsDir, CODEX_HOME_PENDING_CATALOG),
831
- 'pending'
832
- );
821
+ /**
822
+ * Merge a home skill catalog and its pending catalog metadata: the floor is the
823
+ * newer of the two versions; either file present means metadata is in play.
824
+ * @param {{ exists: boolean, valid: boolean, version: string|null }} catalog
825
+ * @param {{ exists: boolean, valid: boolean, version: string|null }} pending
826
+ */
827
+ export function homeCatalogFloorState(catalog, pending) {
833
828
  let floorVersion = catalog.version;
834
- if (
835
- pending.version &&
836
- (!floorVersion || isVersionOlder(floorVersion, pending.version))
837
- ) {
829
+ if (pending.version && (!floorVersion || isVersionOlder(floorVersion, pending.version))) {
838
830
  floorVersion = pending.version;
839
831
  }
840
832
  return {
@@ -846,6 +838,18 @@ function codexHomeCatalogState(skillsDir) {
846
838
  };
847
839
  }
848
840
 
841
+ function codexHomeCatalogState(skillsDir) {
842
+ const catalog = readCodexHomeCatalogMetadata(
843
+ path.join(skillsDir, CODEX_HOME_CATALOG),
844
+ 'catalog'
845
+ );
846
+ const pending = readCodexHomeCatalogMetadata(
847
+ path.join(skillsDir, CODEX_HOME_PENDING_CATALOG),
848
+ 'pending'
849
+ );
850
+ return homeCatalogFloorState(catalog, pending);
851
+ }
852
+
849
853
  function newerCodexHomeCatalogVersion(skillsDir, packageVersion, state = null) {
850
854
  if (!isValidSemver(packageVersion)) return null;
851
855
  const floorVersion = (state ?? codexHomeCatalogState(skillsDir)).floorVersion;
@@ -12,7 +12,7 @@ import { getDiagnosticCatalogEntry } from './diagnostic-catalog.mjs';
12
12
  import { demoteExtraPlaneTeethUnderClassificationFloor } from './extra-merge-teeth.mjs';
13
13
 
14
14
  /** Hook-only guidance: a hook deny is already the verdict for that write. */
15
- export const HOOK_DENY_PREPARE_CHANGE_NOTE = 'Do not call ark_prepare_change from a hook deny.';
15
+ const HOOK_DENY_PREPARE_CHANGE_NOTE = 'Do not call ark_prepare_change from a hook deny.';
16
16
 
17
17
  function surfaceNote(context) {
18
18
  return context?.surface === 'hook' ? ` ${HOOK_DENY_PREPARE_CHANGE_NOTE}` : '';
@@ -35,7 +35,7 @@ function hostOutputTail(hostOutput) {
35
35
  * @param {string|undefined} installCommand
36
36
  * @returns {string|null}
37
37
  */
38
- export function resolveStartInstallPackagePin(packageVersion, installCommand) {
38
+ function resolveStartInstallPackagePin(packageVersion, installCommand) {
39
39
  const fromArg = String(packageVersion ?? '')
40
40
  .trim()
41
41
  .replace(/^arkgate@/i, '')
@@ -147,7 +147,7 @@ export function setupBudget(changes) {
147
147
  }
148
148
 
149
149
  /** One next write when compact start is the wrong size. `--force` does not unlock. */
150
- export const START_SETUP_BUDGET_NEXT = 'arkgate-check --init';
150
+ const START_SETUP_BUDGET_NEXT = 'arkgate-check --init';
151
151
 
152
152
  function formatSetupKb(bytes) {
153
153
  return `${Math.max(1, Math.round(Number(bytes) / 1024))} KB`;
@@ -9,7 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
 
11
11
  /** Product-domain / domain-model homes. Closed list — no tree walk. */
12
- export const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
12
+ const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
13
13
  'docs/domain.md',
14
14
  'docs/data-model.md',
15
15
  'docs/product-domain.md',
@@ -19,7 +19,7 @@ export const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
19
19
  ]);
20
20
 
21
21
  /** Evolved dedicated homes. Adopt if present; do not force a filename. */
22
- export const STATES_TRANSITIONS_DEDICATED_HOMES = Object.freeze([
22
+ const STATES_TRANSITIONS_DEDICATED_HOMES = Object.freeze([
23
23
  'docs/states.md',
24
24
  'docs/transitions.md',
25
25
  'docs/states-transitions.md',
@@ -32,7 +32,7 @@ export const STATES_TRANSITIONS_ASK_MISSING =
32
32
  export const STATES_TRANSITIONS_ASK_INCOMPLETE =
33
33
  'The states heading is there, but it is not a map yet — no table or link.';
34
34
 
35
- export const STATES_TRANSITIONS_NEXT =
35
+ const STATES_TRANSITIONS_NEXT =
36
36
  'Add a short table (entity · states · allowed from → to) — or one link — on the domain doc. Names from the code. No flag soup.';
37
37
 
38
38
  function readMarkdown(abs) {
@@ -30,6 +30,7 @@ import { projectStatusArkRun } from './ark-run-doctor.mjs';
30
30
  import { projectStatusArkOrder } from './ark-order-doctor.mjs';
31
31
  import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
32
32
  import { classifyAdopted, readAdoptionStance } from './adoption-stance.mjs';
33
+ import { lastCheckVerdictFacts } from './upstream-report.mjs';
33
34
 
34
35
  function sha256Hex(value) {
35
36
  return createHash('sha256').update(value, 'utf8').digest('hex');
@@ -115,24 +116,7 @@ export function lastCheckFactsFromSnapshot(latest, baseline) {
115
116
  frozenResidual,
116
117
  };
117
118
  }
118
- const at =
119
- typeof latest.generatedAt === 'string'
120
- ? latest.generatedAt
121
- : typeof latest.at === 'string'
122
- ? latest.at
123
- : null;
124
- const active =
125
- typeof latest.activeViolations === 'number'
126
- ? latest.activeViolations
127
- : typeof latest.violations?.active === 'number'
128
- ? latest.violations.active
129
- : null;
130
- let verdict = null;
131
- if (latest.ok === true && (active == null || active === 0)) verdict = 'pass';
132
- else if (latest.ok === false || (typeof active === 'number' && active > 0)) verdict = 'fail';
133
- else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
134
- verdict = 'incomplete';
135
- } else if (latest.ok === true) verdict = 'pass';
119
+ const { at, active, verdict } = lastCheckVerdictFacts(latest);
136
120
  return {
137
121
  lastCheckAt: at,
138
122
  lastCheckVerdict: verdict,
@@ -10,6 +10,22 @@
10
10
 
11
11
  import { projectStatusArkRun } from './ark-run-doctor.mjs';
12
12
  import { projectStatusArkOrder } from './ark-order-doctor.mjs';
13
+ /** Status slice schema of one extra plane (ArkRun / ArkOrder). */
14
+ function extraPlaneStatusSchema(plane) {
15
+ return {
16
+ type: 'object',
17
+ description: `${plane} extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.`,
18
+ additionalProperties: false,
19
+ required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
20
+ properties: {
21
+ notAScore: { const: true },
22
+ present: { type: 'boolean' },
23
+ mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
24
+ extraMergeTeeth: { type: 'boolean' },
25
+ residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
26
+ },
27
+ };
28
+ }
13
29
  export const ARK_STATUS_MANIFEST_SCHEMA_VERSION = '1.0';
14
30
  export const ARK_STATUS_MANIFEST_SCHEMA_URL = 'https://unpkg.com/arkgate@4/schemas/ark.status-manifest.schema.json';
15
31
  /**
@@ -653,31 +669,7 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
653
669
  baselineGrew: { type: 'boolean' },
654
670
  },
655
671
  },
656
- arkRun: {
657
- type: 'object',
658
- description: 'ArkRun extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.',
659
- additionalProperties: false,
660
- required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
661
- properties: {
662
- notAScore: { const: true },
663
- present: { type: 'boolean' },
664
- mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
665
- extraMergeTeeth: { type: 'boolean' },
666
- residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
667
- },
668
- },
669
- arkOrder: {
670
- type: 'object',
671
- description: 'ArkOrder extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.',
672
- additionalProperties: false,
673
- required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
674
- properties: {
675
- notAScore: { const: true },
676
- present: { type: 'boolean' },
677
- mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
678
- extraMergeTeeth: { type: 'boolean' },
679
- residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
680
- },
681
- },
672
+ arkRun: extraPlaneStatusSchema('ArkRun'),
673
+ arkOrder: extraPlaneStatusSchema('ArkOrder'),
682
674
  },
683
675
  };
@@ -17,12 +17,6 @@ import { layerForFile } from '../ark-shared.mjs';
17
17
  import { isDomainRoleLayerName } from './arkrules-sensors.mjs';
18
18
  import { collectStatesTransitionsResidual } from './states-transitions-presence.mjs';
19
19
 
20
- export const STATUS_CATALOG_ASK =
21
- 'Domain code already names statuses, but the states → transitions map is still thin.';
22
-
23
- export const STATUS_CATALOG_NEXT =
24
- 'Add those names to the table on the domain doc (entity · states · allowed from → to). No flag soup.';
25
-
26
20
  const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs']);
27
21
  const MAX_FILE_BYTES = 200_000;
28
22
  const MAX_ENTITIES = 8;
@@ -19,7 +19,7 @@ export function dirSegmentsFromGlob(pattern) {
19
19
  * Basename / path tokens that score as Persistence (data clients, auth, CRM).
20
20
  * Used so adopt never maps lib/turso → Presentation (NEW-ADOPT-LIB-AS-PRESENTATION).
21
21
  */
22
- export const PERSISTENCE_PATH_TOKENS = Object.freeze([
22
+ const PERSISTENCE_PATH_TOKENS = Object.freeze([
23
23
  'turso',
24
24
  'prisma',
25
25
  'supabase',
@@ -64,7 +64,7 @@ export function isPersistenceClientPath(relPath) {
64
64
  }
65
65
 
66
66
  /** True when a path looks like pure domain folders. */
67
- export function isDomainPath(relPath) {
67
+ function isDomainPath(relPath) {
68
68
  const posix = String(relPath || '')
69
69
  .split(/[/\\]/)
70
70
  .filter(Boolean)
@@ -81,7 +81,7 @@ let _layerByDir;
81
81
  // Map<dirBasename, string[] layers>. A basename mapping to >1 layer (e.g. `app` — Application
82
82
  // orchestration in the 11-layer defaults, but Presentation in the monorepo/Next preset) is
83
83
  // genuinely ambiguous; every candidate is surfaced rather than silently picked.
84
- export function layerByDir() {
84
+ function layerByDir() {
85
85
  if (_layerByDir) return _layerByDir;
86
86
  const map = new Map();
87
87
  const add = (segment, layer) => {
@@ -20,7 +20,6 @@ import {
20
20
  suggestStewards,
21
21
  } from './team-parliament.mjs';
22
22
  import {
23
- SPAWN_TIMEOUT_MS,
24
23
  TEAM_BASE_CANDIDATES,
25
24
  discoverTeamBaseRef,
26
25
  gitShowText,
@@ -29,21 +28,8 @@ import {
29
28
  safeGitRef,
30
29
  } from './git-change-scope.mjs';
31
30
 
32
- export {
33
- baselineKeysFromDocument,
34
- classifyBaselineKeyDelta,
35
- classifyChangeSet,
36
- evaluateTeamGate,
37
- formatVsBaseLine,
38
- isTeamPersona,
39
- mapPolicyClassToKind,
40
- parseCodeownersHandles,
41
- personaCheckBudget,
42
- resolveStewardHandle,
43
- suggestStewards,
44
- };
45
31
  // prettier-ignore
46
- export { SPAWN_TIMEOUT_MS, TEAM_BASE_CANDIDATES, discoverTeamBaseRef, gitShowText, listChangedPaths, safeGitRef };
32
+ export { discoverTeamBaseRef, safeGitRef };
47
33
 
48
34
  export function contractSessionFrom(args, env = process.env) {
49
35
  if (args?.contractSession === true) return true;
@@ -269,12 +255,12 @@ export function runTeamPreflight({ root, args, config, policyDelta, teamBase })
269
255
  }
270
256
 
271
257
  /** `--changed` / `--persona touch|contributor|agent` without a resolvable merge base. */
272
- export function changedNeedsBaseMessage(args) {
258
+ function changedNeedsBaseMessage(args) {
273
259
  const flag = args?.persona ? `--persona ${args.persona}` : '--changed';
274
260
  return `${flag} needs a git merge base to know which files changed, and none was found (tried ${TEAM_BASE_CANDIDATES.join(', ')}; or this is not a git repository). Pass --base <ref> (for example --base HEAD or --base origin/main), or run without ${flag} for a full-tree check.`;
275
261
  }
276
262
 
277
- export function contractDiffNeedsBaseMessage(args) {
263
+ function contractDiffNeedsBaseMessage(args) {
278
264
  const flag = args?.persona ? `--persona ${args.persona}` : '--contract-diff';
279
265
  return `${flag} compares the contract and .ark-baseline.json against a git base, and none was found (tried ${TEAM_BASE_CANDIDATES.join(', ')}; or this is not a git repository). Pass --base <ref> (for example --base origin/main), or run a plain full-tree check without ${flag}.`;
280
266
  }
@@ -8,8 +8,8 @@
8
8
  * Pure CLI helper (bin/lib/team-parliament.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- export const TEAM_PERSONAS = ['touch', 'contributor', 'agent', 'steward'];
12
- export const CONTRACT_DIFF_KINDS = [
11
+ const TEAM_PERSONAS = ['touch', 'contributor', 'agent', 'steward'];
12
+ const CONTRACT_DIFF_KINDS = [
13
13
  'unchanged',
14
14
  'tighten',
15
15
  'loosen',
@@ -35,11 +35,14 @@ export function isLawRelativePath(relPath) {
35
35
  if ((n.startsWith('arkrules/') || n.includes('/arkrules/')) && n.endsWith('.json')) {
36
36
  return true;
37
37
  }
38
+ // Slice file named by childSlices.arkRulesFile (`arkrules.<Layer>.json`), including a lookalike.
39
+ if (/^arkrules\.[^/]+\.json$/i.test(base))
40
+ return true;
38
41
  return false;
39
42
  }
40
43
  const PRODUCT_SOURCE = /\.(tsx?|jsx?|mjs|cjs)$/i;
41
44
  /** Governable product source (not law, not tests-only heuristic — basename extension). */
42
- export function isProductSourceRelativePath(relPath) {
45
+ function isProductSourceRelativePath(relPath) {
43
46
  const n = posixRel(relPath);
44
47
  if (!n || isLawRelativePath(n))
45
48
  return false;
@@ -100,20 +103,20 @@ export function classifyBaselineKeyDelta(baseKeys, candidateKeys) {
100
103
  kinds.push('baseline-shrink');
101
104
  return { grow, shrink, kinds };
102
105
  }
103
- export function normalizeStewardId(value) {
106
+ function normalizeStewardId(value) {
104
107
  if (typeof value !== 'string')
105
108
  return null;
106
109
  const trimmed = value.trim().replace(/^@/, '').toLowerCase();
107
110
  return trimmed.length > 0 ? trimmed : null;
108
111
  }
109
112
  /** GitHub handle (login), not a display name. Spaces and emails are not handles. */
110
- export function isGitHubHandle(value) {
113
+ function isGitHubHandle(value) {
111
114
  const id = normalizeStewardId(value);
112
115
  if (!id || isAutomationAuthor(id) || id.includes(' ') || id.includes('@'))
113
116
  return false;
114
117
  return /^(?!-)[a-z0-9-]{1,39}(?<!-)$/.test(id);
115
118
  }
116
- export function isGitHubEmail(value) {
119
+ function isGitHubEmail(value) {
117
120
  if (typeof value !== 'string')
118
121
  return false;
119
122
  const email = value.trim().toLowerCase();
@@ -127,7 +130,7 @@ export function isGitHubEmail(value) {
127
130
  return dot > 0 && dot < parts[1].length - 1;
128
131
  }
129
132
  /** `123+login@users.noreply.github.com` or `login@users.noreply.github.com` → login. */
130
- export function githubHandleFromEmail(email) {
133
+ function githubHandleFromEmail(email) {
131
134
  if (typeof email !== 'string')
132
135
  return null;
133
136
  const match = email.trim().match(/^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i);
@@ -150,7 +153,7 @@ export function canonicalStewardId(value) {
150
153
  return id;
151
154
  return null;
152
155
  }
153
- export function formatStewardMention(id) {
156
+ function formatStewardMention(id) {
154
157
  return id.includes('@') ? id : `@${id}`;
155
158
  }
156
159
  /**
@@ -178,7 +181,7 @@ export function isSteward(author, stewards) {
178
181
  return stewards.some((entry) => canonicalStewardId(entry) === who);
179
182
  }
180
183
  const BOT_STEWARD = /bot\b|\[bot\]|dependabot|renovate|github-actions|imgbot|codecov/i;
181
- export function isAutomationAuthor(value) {
184
+ function isAutomationAuthor(value) {
182
185
  const id = normalizeStewardId(value);
183
186
  return !id || BOT_STEWARD.test(id);
184
187
  }
@@ -318,6 +321,9 @@ export function personaCheckBudget(persona) {
318
321
  denyLoosenUnlessSteward: true,
319
322
  };
320
323
  }
324
+ /**
325
+ * Narrow a string to a team persona.
326
+ */
321
327
  export function isTeamPersona(value) {
322
328
  return TEAM_PERSONAS.includes(value);
323
329
  }
@@ -46,7 +46,7 @@ function versionParts(version) {
46
46
  * @param {string} right
47
47
  * @returns {-1|0|1}
48
48
  */
49
- export function compareUpgradeVersions(left, right) {
49
+ function compareUpgradeVersions(left, right) {
50
50
  const a = versionParts(left);
51
51
  const b = versionParts(right);
52
52
  const length = Math.max(a.length, b.length);
@@ -61,7 +61,7 @@ export function compareUpgradeVersions(left, right) {
61
61
  * @param {UpgradeBehaviorChange} row
62
62
  * @returns {string}
63
63
  */
64
- export function formatBehaviorChange(row) {
64
+ function formatBehaviorChange(row) {
65
65
  return `${row.version}: ${row.note}`;
66
66
  }
67
67
 
@@ -18,6 +18,7 @@ import {
18
18
  upgradeOutcome,
19
19
  } from './upgrade-outcome.mjs';
20
20
  import { __packageRoot } from './gate-files.mjs';
21
+ import { compareSemverCore } from './upgrade-package-decision.mjs';
21
22
  import {
22
23
  applyManagedUpgrade,
23
24
  managedUpgradeJson,
@@ -51,30 +52,7 @@ export function resolveProjectArkgatePackageJson(root) {
51
52
  }
52
53
  }
53
54
 
54
- /**
55
- * Compare numeric major.minor.patch cores (prerelease / build ignored).
56
- * @returns {-1|0|1}
57
- */
58
- export function compareSemverCore(a, b) {
59
- const parse = (value) => {
60
- const core = String(value ?? '')
61
- .trim()
62
- .replace(/^v/i, '')
63
- .split(/[-+]/)[0];
64
- const parts = core.split('.').map((part) => {
65
- const n = Number.parseInt(part, 10);
66
- return Number.isFinite(n) ? n : 0;
67
- });
68
- return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
69
- };
70
- const left = parse(a);
71
- const right = parse(b);
72
- for (let i = 0; i < 3; i += 1) {
73
- if (left[i] < right[i]) return -1;
74
- if (left[i] > right[i]) return 1;
75
- }
76
- return 0;
77
- }
55
+ export { compareSemverCore };
78
56
 
79
57
  /** True when candidate is the same path as root or a descendant (after resolve). */
80
58
  export function isPathInside(candidate, root) {
@@ -144,7 +144,7 @@ function sampleForViolation(violation, ruleId) {
144
144
  * @param {string} stdout
145
145
  * @returns {Record<string, unknown>|null}
146
146
  */
147
- export function parseCheckStdout(stdout) {
147
+ function parseCheckStdout(stdout) {
148
148
  const text = typeof stdout === 'string' ? stdout.trim() : '';
149
149
  if (!text) return null;
150
150
  try {
@@ -10,7 +10,7 @@ import readline from 'node:readline/promises';
10
10
  import { __packageRoot } from './gate-files.mjs';
11
11
 
12
12
  export const UPSTREAM_OWNER_REPO = 'pedroknigge/arkgate';
13
- export const SUBMIT_PROMPT = 'Type submit to send';
13
+ const SUBMIT_PROMPT = 'Type submit to send';
14
14
 
15
15
  export function ownerRepoFromGithubUrl(url) {
16
16
  if (typeof url !== 'string' || url.trim() === '') return null;
@@ -22,7 +22,7 @@ export function ownerRepoFromGithubUrl(url) {
22
22
  return `${owner}/${repo}`;
23
23
  }
24
24
 
25
- export function resolveUpstreamRepo(pkg) {
25
+ function resolveUpstreamRepo(pkg) {
26
26
  const fromBugs = ownerRepoFromGithubUrl(pkg?.bugs?.url);
27
27
  if (fromBugs) return fromBugs;
28
28
  const repository = pkg?.repository;
@@ -32,7 +32,7 @@ export function resolveUpstreamRepo(pkg) {
32
32
  return UPSTREAM_OWNER_REPO;
33
33
  }
34
34
 
35
- export function posixSingleQuote(value) {
35
+ function posixSingleQuote(value) {
36
36
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
37
37
  }
38
38
 
@@ -40,7 +40,7 @@ export function formatGhIssueCreateCommand({ repo, title, body }) {
40
40
  return `gh issue create --repo ${posixSingleQuote(repo)} --title ${posixSingleQuote(title)} --body ${posixSingleQuote(body)}`;
41
41
  }
42
42
 
43
- export function reportUsage() {
43
+ function reportUsage() {
44
44
  return `arkgate report (alias ark report) — draft an upstream GitHub issue for ArkGate.
45
45
 
46
46
  Target: pedroknigge/arkgate (this package's package.json bugs.url). Never the consumer repo.
@@ -59,8 +59,13 @@ gh issue create --repo pedroknigge/arkgate command, then exits 2.
59
59
  `;
60
60
  }
61
61
 
62
- export function lastCheckSnippet(latest) {
63
- if (!latest || typeof latest !== 'object') return null;
62
+ /**
63
+ * Timestamp, active violation count, and verdict of a `.ark/reports/latest.json`
64
+ * snapshot object. Verdict: pass / fail / incomplete, or null when unknown.
65
+ * @param {object} latest
66
+ * @returns {{ at: string|null, active: number|null, verdict: 'pass'|'fail'|'incomplete'|null }}
67
+ */
68
+ export function lastCheckVerdictFacts(latest) {
64
69
  const at =
65
70
  typeof latest.generatedAt === 'string'
66
71
  ? latest.generatedAt
@@ -79,11 +84,17 @@ export function lastCheckSnippet(latest) {
79
84
  else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
80
85
  verdict = 'incomplete';
81
86
  } else if (latest.ok === true) verdict = 'pass';
87
+ return { at, active, verdict };
88
+ }
89
+
90
+ function lastCheckSnippet(latest) {
91
+ if (!latest || typeof latest !== 'object') return null;
92
+ const { at, active, verdict } = lastCheckVerdictFacts(latest);
82
93
  if (at == null && verdict == null && active == null) return null;
83
94
  return { at, verdict, activeViolations: active };
84
95
  }
85
96
 
86
- export function readLastCheckSnapshot(root) {
97
+ function readLastCheckSnapshot(root) {
87
98
  const latestPath = path.join(root, '.ark', 'reports', 'latest.json');
88
99
  try {
89
100
  return JSON.parse(fs.readFileSync(latestPath, 'utf8'));
@@ -92,7 +103,7 @@ export function readLastCheckSnapshot(root) {
92
103
  }
93
104
  }
94
105
 
95
- export function buildIssueDraft({ repo, arkgateVersion, lastCheck, finding, title }) {
106
+ function buildIssueDraft({ repo, arkgateVersion, lastCheck, finding, title }) {
96
107
  const resolvedTitle =
97
108
  typeof title === 'string' && title.trim()
98
109
  ? title.trim()
@@ -127,7 +138,7 @@ export function buildIssueDraft({ repo, arkgateVersion, lastCheck, finding, titl
127
138
  return { repo, title: resolvedTitle, body: lines.join('\n') };
128
139
  }
129
140
 
130
- export function defaultRunGh(argv, options = {}) {
141
+ function defaultRunGh(argv, options = {}) {
131
142
  const env = { ...(options.env ?? process.env) };
132
143
  delete env.GH_REPO;
133
144
  const result = spawnSync('gh', argv, {
@@ -182,7 +193,7 @@ async function defaultPromptSubmit(stdin, stdout) {
182
193
  }
183
194
  }
184
195
 
185
- export function parseReportArgv(argv = []) {
196
+ function parseReportArgv(argv = []) {
186
197
  const out = {
187
198
  submit: false,
188
199
  iConfirmSubmit: false,