arkgate 4.8.23 → 4.8.25

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 (201) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +41 -91
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +707 -121
  6. package/bin/ark-mcp-runtime.mjs +448 -169
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +42 -22
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/adoption-stance.mjs +2 -2
  13. package/bin/lib/adr-path.mjs +3 -3
  14. package/bin/lib/agent-gates.mjs +0 -61
  15. package/bin/lib/agent-homes.mjs +12 -21
  16. package/bin/lib/agent-projection-command.mjs +9 -5
  17. package/bin/lib/ambient-state.mjs +9 -30
  18. package/bin/lib/analysis-engine.mjs +15 -11
  19. package/bin/lib/architecture-scan.mjs +61 -15
  20. package/bin/lib/ark-order-doctor.mjs +4 -29
  21. package/bin/lib/ark-order-facts.mjs +319 -17
  22. package/bin/lib/ark-order-sensors.mjs +28 -101
  23. package/bin/lib/ark-order-types.mjs +3 -2
  24. package/bin/lib/ark-run-doctor.mjs +17 -28
  25. package/bin/lib/ark-run-facts.mjs +461 -48
  26. package/bin/lib/ark-run-sensors.mjs +6 -4
  27. package/bin/lib/arkgate-pins.mjs +91 -0
  28. package/bin/lib/arkrule-file-hints.mjs +2 -47
  29. package/bin/lib/arkrules-contract.mjs +124 -133
  30. package/bin/lib/arkrules-sensors.mjs +55 -252
  31. package/bin/lib/check-args.mjs +85 -1
  32. package/bin/lib/ci-and-commands.mjs +24 -78
  33. package/bin/lib/ci-merge-boundary.mjs +1 -1
  34. package/bin/lib/class-source-scan.mjs +479 -0
  35. package/bin/lib/clone-detection.mjs +474 -0
  36. package/bin/lib/codex-home.mjs +5 -3
  37. package/bin/lib/config-contract-slices.mjs +739 -0
  38. package/bin/lib/config-contract.mjs +42 -607
  39. package/bin/lib/config-extras.mjs +16 -22
  40. package/bin/lib/config-warnings.mjs +32 -0
  41. package/bin/lib/contract-smells.mjs +33 -15
  42. package/bin/lib/core-ratchet.mjs +0 -2
  43. package/bin/lib/dashboard-cli.mjs +62 -0
  44. package/bin/lib/deep-module-coach.mjs +2 -4
  45. package/bin/lib/deploy-path.mjs +13 -38
  46. package/bin/lib/design-delta.mjs +1 -1
  47. package/bin/lib/design-smells.mjs +2 -2
  48. package/bin/lib/diagnostic-catalog.mjs +23 -9
  49. package/bin/lib/doctor-advisories.mjs +78 -8
  50. package/bin/lib/doctor-green-cite.mjs +1 -1
  51. package/bin/lib/doctor-human.mjs +1 -1
  52. package/bin/lib/doctor-next-actions.mjs +19 -4
  53. package/bin/lib/doctor-plan.mjs +9 -13
  54. package/bin/lib/duplication-io.mjs +572 -0
  55. package/bin/lib/effective-contract-load.mjs +282 -83
  56. package/bin/lib/enforcement-profiles.mjs +40 -4
  57. package/bin/lib/entry-points-io.mjs +424 -0
  58. package/bin/lib/extra-merge-teeth.mjs +36 -0
  59. package/bin/lib/field-install.mjs +38 -14
  60. package/bin/lib/first-run-help.mjs +10 -5
  61. package/bin/lib/flat-parent-importers.mjs +139 -0
  62. package/bin/lib/flat-parent-pilot.mjs +40 -100
  63. package/bin/lib/gate-files.mjs +58 -90
  64. package/bin/lib/git-change-scope.mjs +109 -0
  65. package/bin/lib/github-enforcement.mjs +166 -29
  66. package/bin/lib/golden-pattern.mjs +1 -1
  67. package/bin/lib/hook-templates.mjs +17 -15
  68. package/bin/lib/host-support-matrix.mjs +3 -1
  69. package/bin/lib/html-report-advisories.mjs +4 -0
  70. package/bin/lib/html-report-depth.mjs +2 -2
  71. package/bin/lib/html-report.mjs +2 -2
  72. package/bin/lib/import-graph-projection.mjs +112 -0
  73. package/bin/lib/import-resolve.mjs +13 -1
  74. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  75. package/bin/lib/install-migrate.mjs +50 -73
  76. package/bin/lib/invariant-coverage-io.mjs +4 -3
  77. package/bin/lib/invariant-coverage.mjs +85 -11
  78. package/bin/lib/invariant-probe-cli.mjs +347 -0
  79. package/bin/lib/invariant-probe-io.mjs +165 -0
  80. package/bin/lib/invariant-probe-runner.mjs +268 -0
  81. package/bin/lib/invariant-probe-sites.mjs +174 -0
  82. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  83. package/bin/lib/invariant-probe.mjs +461 -0
  84. package/bin/lib/invariant-tests-path.mjs +16 -22
  85. package/bin/lib/literal-path-drift.mjs +4 -2
  86. package/bin/lib/managed-upgrade.mjs +2 -8
  87. package/bin/lib/mcp-adoption.mjs +13 -6
  88. package/bin/lib/mcp-hook-payload.mjs +403 -63
  89. package/bin/lib/orphan-modules-io.mjs +458 -0
  90. package/bin/lib/orphan-modules.mjs +251 -0
  91. package/bin/lib/outside-importers.mjs +197 -0
  92. package/bin/lib/package-manager.mjs +60 -2
  93. package/bin/lib/parse-health.mjs +1 -1
  94. package/bin/lib/passthrough-commands.mjs +83 -0
  95. package/bin/lib/persistence-write-hint.mjs +106 -0
  96. package/bin/lib/pin-dependent-gates.mjs +117 -0
  97. package/bin/lib/policy-delta-io.mjs +219 -14
  98. package/bin/lib/post-green-path.mjs +1 -1
  99. package/bin/lib/prepare-change.mjs +115 -7
  100. package/bin/lib/prepare-write.mjs +2 -2
  101. package/bin/lib/presets.mjs +59 -73
  102. package/bin/lib/product-copy.mjs +1 -6
  103. package/bin/lib/project-root.mjs +31 -62
  104. package/bin/lib/remediation.mjs +23 -4
  105. package/bin/lib/resolved-candidate-facts.mjs +405 -117
  106. package/bin/lib/rules-inventory.mjs +22 -3
  107. package/bin/lib/rules-under-contract.mjs +247 -9
  108. package/bin/lib/scan-files.mjs +36 -4
  109. package/bin/lib/schema-validation.mjs +130 -0
  110. package/bin/lib/sensor-promote-cli.mjs +2 -11
  111. package/bin/lib/sensor-promote-io.mjs +33 -22
  112. package/bin/lib/sensor-promotion.mjs +1 -1
  113. package/bin/lib/skill-catalog-apply.mjs +1 -1
  114. package/bin/lib/skill-install.mjs +42 -38
  115. package/bin/lib/snippet-analysis.mjs +140 -6
  116. package/bin/lib/source-policy.mjs +34 -0
  117. package/bin/lib/start-install-recovery.mjs +72 -2
  118. package/bin/lib/start-preview.mjs +31 -1
  119. package/bin/lib/states-transitions-presence.mjs +3 -3
  120. package/bin/lib/status-command.mjs +35 -22
  121. package/bin/lib/status-manifest.mjs +50 -30
  122. package/bin/lib/status-transition-catalog.mjs +0 -6
  123. package/bin/lib/suggestions.mjs +3 -3
  124. package/bin/lib/team-parliament-io.mjs +76 -88
  125. package/bin/lib/team-parliament.mjs +21 -10
  126. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  127. package/bin/lib/upgrade-command.mjs +2 -24
  128. package/bin/lib/upgrade-outcome.mjs +1 -1
  129. package/bin/lib/upstream-report.mjs +21 -10
  130. package/bin/lib/violations.mjs +10 -9
  131. package/bin/lib/weakest-link.mjs +13 -0
  132. package/bin/lib/write-path-capabilities.mjs +6 -4
  133. package/dist/chunk-A5EIY3HI.cjs +5 -0
  134. package/dist/chunk-AHACEKSV.js +16 -0
  135. package/dist/chunk-SVSQVGFJ.js +5 -0
  136. package/dist/chunk-UN5EH4QV.cjs +16 -0
  137. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
  138. package/dist/configTypes-GPvvdleM.d.ts +257 -0
  139. package/dist/eslint/index.cjs +18 -5
  140. package/dist/eslint/index.d.cts +368 -0
  141. package/dist/eslint/index.d.ts +139 -156
  142. package/dist/eslint/index.js +11 -5
  143. package/dist/index.cjs +36 -35
  144. package/dist/index.d.cts +5547 -0
  145. package/dist/index.d.ts +3283 -198
  146. package/dist/index.js +36 -35
  147. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  148. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  149. package/dist/nestjs/index.cjs +1 -18
  150. package/dist/nestjs/index.d.cts +22 -0
  151. package/dist/nestjs/index.d.ts +1 -3
  152. package/dist/nestjs/index.js +1 -18
  153. package/dist/order/index.cjs +1 -1
  154. package/dist/order/index.d.cts +155 -0
  155. package/dist/order/index.d.ts +15 -3
  156. package/dist/order/index.js +1 -1
  157. package/dist/runtime/index.cjs +13 -30
  158. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
  159. package/dist/runtime/index.d.ts +2741 -133
  160. package/dist/runtime/index.js +13 -30
  161. package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
  162. package/dist/types-CnauMQQL.d.ts +1800 -0
  163. package/docs/README.md +3 -1
  164. package/docs/agent-guide.md +217 -16
  165. package/docs/ai-gates.md +174 -26
  166. package/docs/arkorder.md +32 -15
  167. package/docs/brownfield-adoption.md +5 -1
  168. package/docs/configuration.md +230 -31
  169. package/docs/develop.md +2 -1
  170. package/docs/diagnostics.md +212 -19
  171. package/docs/package-surface.md +35 -9
  172. package/docs/product-voice.md +7 -1
  173. package/package.json +69 -19
  174. package/schemas/ark.analysis-result.schema.json +15 -0
  175. package/schemas/ark.config.schema.json +77 -12
  176. package/schemas/ark.invariant-probe.schema.json +368 -0
  177. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  178. package/schemas/ark.status-manifest.schema.json +22 -0
  179. package/server.json +3 -9
  180. package/templates/agent-skills/README.md +1 -1
  181. package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
  182. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  183. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  184. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  185. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  186. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  187. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  188. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  189. package/templates/arkrules/PersistenceAdapters.json +1 -1
  190. package/templates/arkrules/PresentationAdapters.json +1 -1
  191. package/templates/skills/ark-adopt.md +5 -1
  192. package/templates/skills/ark-contract.md +17 -0
  193. package/templates/skills/ark-coverage.md +2 -0
  194. package/templates/skills/ark-explore.md +7 -0
  195. package/templates/skills/ark-fix.md +20 -0
  196. package/templates/skills/ark-order.md +4 -2
  197. package/templates/skills/ark-place.md +8 -0
  198. package/templates/skills/ark-runtime.md +6 -1
  199. package/bin/lib/ark-order-error.mjs +0 -18
  200. package/bin/lib/ark-order-invariants.mjs +0 -346
  201. package/dist/types-Ik_OakcI.d.ts +0 -183
@@ -0,0 +1,106 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/persistenceWriteHint.ts
5
+ * Regenerate: node scripts/generate-cli-pure.mjs
6
+ * Drift check: node scripts/generate-cli-pure.mjs --check
7
+ *
8
+ * Pure CLI helper (bin/lib/persistence-write-hint.mjs). Zero Node I/O.
9
+ */
10
+
11
+ /** IO / ORM import evidence. postgres and drizzle-orm include package subpaths. */
12
+ const IO_IMPORT_HINT_RE = /\bfrom\s+['"](?:@?prisma\/client|@supabase\/|drizzle-orm(?:\/[^'"]+)?|postgres(?:\/[^'"]+)?|typeorm|knex|mongodb|pg|mysql2|mongoose|better-sqlite3|ioredis|redis|kysely|sequelize)['"]|require\(\s*['"](?:@?prisma\/client|pg|postgres(?:\/[^'"]+)?|drizzle-orm(?:\/[^'"]+)?|knex|typeorm|mongoose)/;
13
+ /** Path-alias / local db module (`@/lib/db`) without resolving tsconfig. */
14
+ const IO_ALIAS_IMPORT_RE = /\bfrom\s+['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle|orm)(?:\.[cm]?[jt]sx?)?['"]|require\(\s*['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle|orm)/;
15
+ /** Named imports from a local persistence module (same module names as IO_ALIAS_IMPORT_RE). */
16
+ const LOCAL_CLIENT_IMPORT_RE = /\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle|orm)(?:\.[cm]?[jt]sx?)?['"]/g;
17
+ /** Conventional client identifiers (PrismaClient included via the /i flag). */
18
+ const PERSISTENCE_CLIENT_NAMES = [
19
+ 'db',
20
+ 'tx',
21
+ 'client',
22
+ 'prisma',
23
+ 'prismaClient',
24
+ 'drizzle',
25
+ ];
26
+ const WRITE_VERB_SOURCE = '(?:insert(?:One|Many)?|update(?:One|Many)?|upsert|delete(?:One|Many)?|createMany|create|replaceOne|findOneAnd(?:Update|Delete|Replace))';
27
+ const SQL_WRITE_RE = /\b(?:INSERT\s+INTO|(?<!\b(?:FOR(?:\s+NO\s+KEY)?|DO)\s)UPDATE(?:\s+ONLY)?(?!\s+(?:OF|SET)\b)|DELETE\s+FROM(?:\s+ONLY)?|MERGE\s+INTO|TRUNCATE(?:\s+TABLE)?(?:\s+ONLY)?)\s+(?:(?:(?:"[^"]*"|[A-Za-z_][\w$]*)\s*\.\s*)?(?:"[^"]*"|[A-Za-z_][\w$]*)|\$\{[^}]+\})/i;
28
+ const NOT_A_SQL_TAG = /^(?:await|return|throw|yield|new|typeof|void|delete|if|else|case|of|in|instanceof|async|function|class|const|let|var|import|export|from|as|default|type)$/;
29
+ const TAGGED_SQL_RE = /(^|[^\w$])([A-Za-z_$][\w$]*)\s*`((?:[^`\\]|\\[\s\S])*)`/g;
30
+ const UPSERT_RE = /\bINSERT\s+INTO\b[\s\S]*?\bON\s+CONFLICT\b[\s\S]*?\bDO\s+UPDATE\s+SET\b[^;`]*/gi;
31
+ /** Call arguments with one level of nested parentheses: `({ log: fn() })`. */
32
+ const CALL_ARGS_SOURCE = '\\((?:[^()]|\\([^()]*\\))*\\)';
33
+ /** Receivers built inline: `new PrismaClient(...)` / `drizzle(...)`. */
34
+ const INLINE_CLIENT_SOURCE = `\\bnew\\s+PrismaClient\\s*${CALL_ARGS_SOURCE}|\\bdrizzle\\s*${CALL_ARGS_SOURCE}`;
35
+ /**
36
+ * `const orm = new PrismaClient()` / `private orm = new PrismaClient()` /
37
+ * `this.orm = new PrismaClient()` / `const sql = postgres(url)` / `db = drizzle(pool)`.
38
+ */
39
+ const DRIVER_BINDING_RE = /(?<![\w$])([A-Za-z_$][\w$]*)\s*[?!]?\s*(?::[^=;\n]+)?=(?![=>])\s*(?:await\s+)?(?:new\s+(?:PrismaClient|Pool|Client|MongoClient|Sequelize|Kysely|Knex)\b|(?:drizzle|knex|Knex|postgres)\s*\()/g;
40
+ /** `orm: PrismaClient` / `db: Kysely<DB>` / `tx: Prisma.TransactionClient` — params and fields. */
41
+ const DRIVER_ANNOTATION_RE = /(?<![\w$])([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*(?:Readonly\s*<\s*)?(?:Prisma\s*\.\s*TransactionClient|PrismaClient|Pool|PoolClient|MongoClient|Sequelize|Kysely|Knex|DataSource|EntityManager|NodePgDatabase|PostgresJsDatabase|BetterSQLite3Database|MySql2Database|LibSQLDatabase)\b/g;
42
+ function escapeRegExp(value) {
43
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ }
45
+ export function sourceImportsPersistenceDriverText(content) {
46
+ return IO_IMPORT_HINT_RE.test(content) || IO_ALIAS_IMPORT_RE.test(content);
47
+ }
48
+ function collectFirstGroup(re, content, names) {
49
+ const global = new RegExp(re.source, 'g');
50
+ let match;
51
+ while ((match = global.exec(content)) !== null) {
52
+ if (match[1])
53
+ names.add(match[1]);
54
+ }
55
+ }
56
+ /**
57
+ * Identifiers that hold a persistence client in this file (sorted, unique): bound to a
58
+ * driver constructor, annotated with a driver client type, or imported by name from a
59
+ * local persistence module.
60
+ */
61
+ function persistenceClientBindings(content) {
62
+ const names = new Set();
63
+ collectFirstGroup(DRIVER_BINDING_RE, content, names);
64
+ collectFirstGroup(DRIVER_ANNOTATION_RE, content, names);
65
+ const imports = new RegExp(LOCAL_CLIENT_IMPORT_RE.source, 'g');
66
+ let match;
67
+ while ((match = imports.exec(content)) !== null) {
68
+ for (const element of (match[1] ?? '').split(',')) {
69
+ const parts = element.trim().replace(/^type\s+/, '').split(/\s+as\s+/);
70
+ const local = (parts[1] ?? parts[0] ?? '').trim();
71
+ if (/^[A-Za-z_$][\w$]*$/.test(local))
72
+ names.add(local);
73
+ }
74
+ }
75
+ return [...names].sort();
76
+ }
77
+ function blankPersistenceComments(content) {
78
+ return content.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|`(?:[^`\\]|\\[\s\S])*`|'(?:[^'\\]|\\[\s\S])*'|"(?:[^"\\]|\\[\s\S])*"/g, (token) => token.charCodeAt(0) === 47
79
+ ? token.replace(/[^\n]/g, ' ')
80
+ : token.charCodeAt(0) === 96
81
+ ? token.replace(/--[^\n`]*/g, (comment) => ' '.repeat(comment.length))
82
+ : token);
83
+ }
84
+ export function sourceHasTaggedSqlWrite(content) {
85
+ const text = blankPersistenceComments(content);
86
+ TAGGED_SQL_RE.lastIndex = 0;
87
+ let match;
88
+ while ((match = TAGGED_SQL_RE.exec(text)) !== null) {
89
+ if (NOT_A_SQL_TAG.test(match[2] ?? ''))
90
+ continue;
91
+ if (SQL_WRITE_RE.test((match[3] ?? '').replace(UPSERT_RE, ' ')))
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+ /** Receiver-bound write evidence (ORM verb on a persistence client, or raw SQL write). */
97
+ export function sourceHasPersistenceWrite(content) {
98
+ const text = blankPersistenceComments(content);
99
+ const receivers = [...PERSISTENCE_CLIENT_NAMES, ...persistenceClientBindings(text)]
100
+ .map(escapeRegExp)
101
+ .join('|');
102
+ const re = new RegExp(`(?:\\b(?:${receivers})(?![\\w$])|${INLINE_CLIENT_SOURCE})(?:\\s*\\.\\s*[A-Za-z_]\\w*)*\\s*\\.\\s*${WRITE_VERB_SOURCE}\\s*\\(`, 'i');
103
+ if (re.test(text))
104
+ return true;
105
+ return SQL_WRITE_RE.test(text);
106
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Generated gate files whose Ark command only resolves through a local arkgate pin.
3
+ *
4
+ * `npx ark-check`, `pnpm exec arkgate-mcp` and `yarn ark-check` reach arkgate's bins only
5
+ * when package.json declares arkgate. Without that pin the same line asks npm for a package
6
+ * named `ark-check` (404) or runs a stale global copy. The pinned form
7
+ * (`npx -y -p arkgate@<exact>`) does not depend on the pin and is not reported.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { npxArkgatePrefixLength } from './package-manager.mjs';
12
+
13
+ const ARK_BIN = /^(?:arkgate-check|arkgate-mcp|arkgate|ark-check|ark-mcp|ark)$/;
14
+ const LOCAL_RUNNER_TEXT =
15
+ /(?:^|[\s"'`;&|(])(?:npx|pnpm(?:\s+--config\.verify-deps-before-run=false)?\s+exec|yarn)\s+(?:arkgate-check|arkgate-mcp|arkgate|ark-check|ark-mcp|ark)(?=[\s"'`;&|)]|$)/m;
16
+ const TOML_SERVER = /command\s*=\s*"([^"]*)"\s*\r?\nargs\s*=\s*\[([^\]]*)\]/g;
17
+
18
+ /** Host hooks, package scripts, and MCP registrations Ark generates. */
19
+ const PIN_DEPENDENT_CANDIDATES = [
20
+ '.claude/settings.json',
21
+ '.codex/hooks.json',
22
+ '.cursor/hooks.json',
23
+ '.grok/hooks/ark-write-gate.json',
24
+ '.agents/hooks.json',
25
+ '.mcp.json',
26
+ '.cursor/mcp.json',
27
+ '.agents/mcp_config.json',
28
+ 'opencode.json',
29
+ '.codex/config.toml',
30
+ '.grok/config.toml',
31
+ 'package.json',
32
+ ];
33
+
34
+ function readText(file) {
35
+ try {
36
+ return fs.readFileSync(file, 'utf8');
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /** True when this argv runs an Ark bin through a runner that needs the local pin. */
43
+ function argvDependsOnArkgatePin(command, args) {
44
+ if (typeof command !== 'string' || !Array.isArray(args)) return false;
45
+ const runner = path.basename(command.trim().replace(/\\/g, '/')).replace(/\.(?:cmd|exe)$/i, '');
46
+ if (runner === 'npx') return npxArkgatePrefixLength(args) === 0 && ARK_BIN.test(args[0] ?? '');
47
+ if (runner === 'yarn') return ARK_BIN.test(args[0] ?? '');
48
+ if (runner === 'pnpm') {
49
+ const exec = args.indexOf('exec');
50
+ return exec >= 0 && ARK_BIN.test(args[exec + 1] ?? '');
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function jsonServersDependOnPin(text) {
56
+ let json;
57
+ try {
58
+ json = JSON.parse(text);
59
+ } catch {
60
+ return false;
61
+ }
62
+ const servers = [json?.mcpServers?.ark, json?.servers?.ark].filter(
63
+ (server) => server && typeof server === 'object'
64
+ );
65
+ if (servers.some((server) => argvDependsOnArkgatePin(server.command, server.args ?? []))) {
66
+ return true;
67
+ }
68
+ const argv = json?.mcp?.ark?.command;
69
+ return Array.isArray(argv) && argvDependsOnArkgatePin(argv[0], argv.slice(1));
70
+ }
71
+
72
+ function tomlServersDependOnPin(text) {
73
+ for (const match of text.matchAll(TOML_SERVER)) {
74
+ const args = [...match[2].matchAll(/"([^"]*)"/g)].map((entry) => entry[1]);
75
+ if (argvDependsOnArkgatePin(match[1], args)) return true;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ function fileDependsOnPin(relativePath, text) {
81
+ if (LOCAL_RUNNER_TEXT.test(text)) return true;
82
+ if (relativePath.endsWith('.toml')) return tomlServersDependOnPin(text);
83
+ if (relativePath.endsWith('.json')) return jsonServersDependOnPin(text);
84
+ return false;
85
+ }
86
+
87
+ function workflowFiles(root) {
88
+ const dir = path.join(root, '.github', 'workflows');
89
+ try {
90
+ return fs
91
+ .readdirSync(dir)
92
+ .filter((name) => /\.ya?ml$/i.test(name))
93
+ .sort()
94
+ .map((name) => `.github/workflows/${name}`);
95
+ } catch {
96
+ return [];
97
+ }
98
+ }
99
+
100
+ /** Project-relative generated files whose Ark command needs arkgate in package.json. */
101
+ export function pinDependentGateFiles(root) {
102
+ const found = [];
103
+ for (const rel of [...PIN_DEPENDENT_CANDIDATES, ...workflowFiles(root)]) {
104
+ const text = readText(path.join(root, rel));
105
+ if (text && fileDependsOnPin(rel, text)) found.push(rel);
106
+ }
107
+ return found;
108
+ }
109
+
110
+ /**
111
+ * Attach `dependentGateFiles` to a PACKAGE_PIN_ABSENT truth so doctor can lead with the pin
112
+ * when a generated file would 404 without it. Other codes pass through unchanged.
113
+ */
114
+ export function withPinDependentGateFiles(root, truth) {
115
+ if (truth?.code !== 'PACKAGE_PIN_ABSENT') return truth;
116
+ return { ...truth, dependentGateFiles: pinDependentGateFiles(root) };
117
+ }
@@ -1,15 +1,24 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { analyzePolicyDelta } from './analysis-engine.mjs';
5
- import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
4
+ import { analyzePolicyDelta, stableSerialize } from './analysis-engine.mjs';
5
+ import { loadArkConfigContract } from './config-contract.mjs';
6
+ import {
7
+ directoryArkRulesReader,
8
+ loadEffectiveArkRules,
9
+ loadEffectiveArkRulesFromDisk,
10
+ normalizeProjectRelativePath,
11
+ objectArkRulesReader,
12
+ } from './effective-contract-load.mjs';
6
13
  import {
7
14
  coverageOptionsFromConfig,
8
15
  invariantIdsFromCatalog,
9
16
  loadInvariantCoverageInputs,
10
17
  } from './invariant-coverage-io.mjs';
11
18
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
19
+ import { attachProbeEvidence } from './invariant-probe-io.mjs';
12
20
  import { attachPolicyAdrNote } from './adr-path.mjs';
21
+ import { collectGovernedFiles } from './scan-files.mjs';
13
22
 
14
23
  function readJsonFile(filePath, label) {
15
24
  if (!fs.existsSync(filePath)) throw new Error(`${label} not found: ${filePath}`);
@@ -85,16 +94,72 @@ function configPathInRepository(root, configPath, top) {
85
94
  return relative;
86
95
  }
87
96
 
97
+ /**
98
+ * ArkRules files at the base ref, relative to the project root inside the repository.
99
+ * A path absent at the ref (added by the candidate) reads as `missing`.
100
+ */
101
+ function baseRefSourceFiles(top, ref, prefix) {
102
+ const shown = runGit(top, ['ls-tree', '-r', '--name-only', ref]);
103
+ if (shown.status !== 0) return [];
104
+ const rootPrefix = prefix ? `${prefix}/` : '';
105
+ const files = [];
106
+ for (const line of shown.stdout.split('\n')) {
107
+ if (!line) continue;
108
+ if (rootPrefix && !line.startsWith(rootPrefix)) continue;
109
+ const rel = rootPrefix ? line.slice(rootPrefix.length) : line;
110
+ if (/\.[cm]?[tj]sx?$/.test(rel) && !rel.endsWith('.d.ts')) files.push(rel);
111
+ }
112
+ return files;
113
+ }
114
+
115
+ function baseRefArkRulesReader(root, top, ref) {
116
+ const prefix = path
117
+ .relative(fs.realpathSync(top), fs.realpathSync(root))
118
+ .split(path.sep)
119
+ .join('/');
120
+ const spec = (rel) => `${ref}:${prefix ? `${prefix}/` : ''}${rel}`;
121
+ return {
122
+ files: baseRefSourceFiles(top, ref, prefix),
123
+ readArkRulesFile: (rel) => {
124
+ const shown = runGit(top, ['show', spec(rel)]);
125
+ if (shown.status === 0) return { ok: true, content: shown.stdout };
126
+ const exists = runGit(top, ['cat-file', '-e', spec(rel)]);
127
+ return {
128
+ ok: false,
129
+ missing: exists.status !== 0 && !shown.error,
130
+ message: `Cannot read policy base ArkRules ${spec(rel)}: ${
131
+ shown.stderr?.trim() || shown.error?.message || 'git show failed'
132
+ }`,
133
+ };
134
+ },
135
+ };
136
+ }
137
+
88
138
  export function resolvePolicyBaseConfig({
89
139
  root,
90
140
  configPath,
91
141
  basePath,
92
142
  baseRef,
143
+ adoptMissingBase = false,
93
144
  env = process.env,
94
145
  }) {
95
146
  if (basePath) {
96
147
  const absolute = path.isAbsolute(basePath) ? basePath : path.resolve(root, basePath);
97
- return { config: readJsonFile(absolute, 'Policy base'), source: absolute, ref: null };
148
+ const config = readJsonFile(absolute, 'Policy base');
149
+ // A base config FILE carries its ArkRules next to it (same relative paths as the
150
+ // project), never from the candidate working tree: that would compare the
151
+ // candidate catalog with itself and hide every demotion/deletion.
152
+ const baseRoot = fs.realpathSync(path.dirname(absolute));
153
+ const include = Array.isArray(config.include) && config.include.length > 0 ? config.include : ['src'];
154
+ return {
155
+ config,
156
+ source: absolute,
157
+ ref: null,
158
+ files: collectGovernedFiles(baseRoot, { ...config, include })
159
+ .map((file) => path.relative(baseRoot, file).split(path.sep).join('/'))
160
+ .filter((rel) => rel && !rel.startsWith('..')),
161
+ readArkRulesFile: directoryArkRulesReader(baseRoot),
162
+ };
98
163
  }
99
164
 
100
165
  const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF);
@@ -119,7 +184,8 @@ export function resolvePolicyBaseConfig({
119
184
  // A newly adopted contract has no predecessor to weaken. CI-provided and
120
185
  // auto-discovered bases may therefore omit the config; an explicit CLI ref
121
186
  // remains fail-closed because the caller asked to compare that exact input.
122
- if (refExists.status === 0 && !baseRef) return null;
187
+ // A team base bound by --base / discovery (adoptMissingBase) is not such a request.
188
+ if (refExists.status === 0 && (!baseRef || adoptMissingBase)) return null;
123
189
  if (requestedRef) {
124
190
  throw new Error(
125
191
  `Cannot read policy base ${ref}:${relativeConfig}: ${result.stderr.trim() || 'git show failed'}`
@@ -132,6 +198,7 @@ export function resolvePolicyBaseConfig({
132
198
  config: JSON.parse(result.stdout),
133
199
  source: `git:${ref}:${relativeConfig}`,
134
200
  ref,
201
+ ...baseRefArkRulesReader(root, top, ref),
135
202
  };
136
203
  } catch (error) {
137
204
  throw new Error(
@@ -148,6 +215,135 @@ export function readPolicyAcknowledgement(root, acknowledgementPath) {
148
215
  return readJsonFile(absolute, 'Policy acknowledgement');
149
216
  }
150
217
 
218
+ /**
219
+ * Effective ArkRules of the policy base. A path the base config references but the
220
+ * base ref does not contain yet counts as an empty layer (git refs only). Any other
221
+ * failure is fail-closed: silently dropping the base catalog would make every
222
+ * ArkRule demotion or deletion classify as neutral.
223
+ */
224
+ function loadBaseArkRules(base) {
225
+ const loaded = loadEffectiveArkRules(base.config, base.readArkRulesFile, {
226
+ missingAsEmpty: base.ref != null,
227
+ ...(Array.isArray(base.files) ? { files: base.files } : {}),
228
+ });
229
+ if (loaded.errors.length > 0) {
230
+ const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
231
+ const remedy =
232
+ base.ref == null
233
+ ? `\nA --policy-base file reads its ArkRules next to it: copy the base catalog to the same relative paths under ${path.dirname(base.source)}, or compare against a git ref with --policy-base-ref.`
234
+ : '';
235
+ throw new Error(
236
+ `Policy base ArkRules could not be loaded (${base.source}); ArkRules transitions cannot be classified:\n${message}${remedy}`
237
+ );
238
+ }
239
+ return loaded.arkRules;
240
+ }
241
+
242
+ function hasArkRulesMap(config) {
243
+ const refs = config?.arkRules;
244
+ return Boolean(refs && typeof refs === 'object' && Object.keys(refs).length > 0);
245
+ }
246
+
247
+ /** Paths in a supplied catalog. A slice rule file is enough for the child wall to name it. */
248
+ function catalogPaths(files) {
249
+ if (!files || typeof files !== 'object') return [];
250
+ const out = [];
251
+ for (const key of Object.keys(files)) {
252
+ const rel = normalizeProjectRelativePath(String(key));
253
+ if (rel) out.push(rel);
254
+ }
255
+ return out;
256
+ }
257
+
258
+ function loadedOrThrow(loaded, label) {
259
+ if (loaded.errors.length === 0) return loaded.arkRules;
260
+ const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
261
+ throw new Error(`${label} ArkRules could not be loaded:\n${message}`);
262
+ }
263
+
264
+ function normalizedArkRulesMap(config) {
265
+ let normalized = config;
266
+ try {
267
+ normalized = loadArkConfigContract(config, 'candidateConfig').config;
268
+ } catch {
269
+ // An invalid candidate is reported by analyzePolicyDelta; compare it as given.
270
+ }
271
+ return stableSerialize(hasArkRulesMap(normalized) ? normalized.arkRules : null);
272
+ }
273
+
274
+ /**
275
+ * True when a supplied `candidateConfig` names the same ArkRules catalog files as the
276
+ * loaded project config, so the candidate catalog is the one on disk. Both sides are
277
+ * normalised first: the project config in memory carries loader defaults (`$schema`,
278
+ * upgraded `schemaVersion`), so passing `ark.config.json` verbatim must still match.
279
+ */
280
+ export function candidateArkRulesMatchProject(candidateConfig, projectConfig) {
281
+ if (candidateConfig === undefined) return true;
282
+ if (!candidateConfig || typeof candidateConfig !== 'object' || Array.isArray(candidateConfig)) {
283
+ return false;
284
+ }
285
+ return normalizedArkRulesMap(candidateConfig) === normalizedArkRulesMap(projectConfig);
286
+ }
287
+
288
+ /**
289
+ * MCP `ark_policy_delta`: Effective ArkRules for both sides of a config transition.
290
+ * Base ArkRules must be SUPPLIED as data (`baseArkRuleFiles`, keyed by the project-
291
+ * relative paths in `baseConfig.arkRules`) whenever the base config maps any: the
292
+ * server only sees the candidate working tree, and reading the base catalog from it
293
+ * would classify every ArkRule demotion or deletion as neutral. The candidate comes
294
+ * from `candidateArkRuleFiles`, or from disk when the candidate is the project config.
295
+ */
296
+ export function resolvePolicyDeltaArkRules({
297
+ root,
298
+ baseConfig,
299
+ candidateConfig,
300
+ candidateIsProjectConfig,
301
+ baseArkRuleFiles,
302
+ candidateArkRuleFiles,
303
+ }) {
304
+ let baseArkRules;
305
+ if (hasArkRulesMap(baseConfig)) {
306
+ if (!baseArkRuleFiles || typeof baseArkRuleFiles !== 'object') {
307
+ throw new Error(
308
+ 'baseConfig maps arkRules, so the ArkRules transition cannot be classified without the base catalog: ' +
309
+ 'pass baseArkRuleFiles as { "<path from baseConfig.arkRules>": <ArkRules file JSON> }.'
310
+ );
311
+ }
312
+ baseArkRules = loadedOrThrow(
313
+ loadEffectiveArkRules(baseConfig, objectArkRulesReader(baseArkRuleFiles), {
314
+ files: catalogPaths(baseArkRuleFiles),
315
+ }),
316
+ 'Base'
317
+ );
318
+ } else {
319
+ baseArkRules = loadEffectiveArkRules(baseConfig, () => ({ ok: false, message: '' })).arkRules;
320
+ }
321
+
322
+ let candidateArkRules;
323
+ if (candidateArkRuleFiles && typeof candidateArkRuleFiles === 'object') {
324
+ candidateArkRules = loadedOrThrow(
325
+ loadEffectiveArkRules(candidateConfig, objectArkRulesReader(candidateArkRuleFiles), {
326
+ files: catalogPaths(candidateArkRuleFiles),
327
+ }),
328
+ 'Candidate'
329
+ );
330
+ } else if (candidateIsProjectConfig) {
331
+ candidateArkRules = loadedOrThrow(
332
+ loadEffectiveArkRulesFromDisk(root, candidateConfig),
333
+ 'Candidate'
334
+ );
335
+ } else if (hasArkRulesMap(candidateConfig)) {
336
+ throw new Error(
337
+ 'candidateConfig maps different arkRules files than the project contract: pass candidateArkRuleFiles ' +
338
+ 'as { "<path from candidateConfig.arkRules>": <ArkRules file JSON> }.'
339
+ );
340
+ } else {
341
+ candidateArkRules = loadEffectiveArkRules(candidateConfig, () => ({ ok: false, message: '' }))
342
+ .arkRules;
343
+ }
344
+ return { baseArkRules, candidateArkRules };
345
+ }
346
+
151
347
  export function analyzePolicyTransition({
152
348
  root,
153
349
  configPath,
@@ -155,18 +351,21 @@ export function analyzePolicyTransition({
155
351
  strictMerge,
156
352
  basePath,
157
353
  baseRef,
354
+ adoptMissingBase = false,
158
355
  acknowledgementPath,
159
356
  }) {
160
357
  if (!strictMerge && !basePath && !baseRef && !acknowledgementPath) return undefined;
161
- const base = resolvePolicyBaseConfig({ root, configPath, basePath, baseRef });
162
- if (!base && (basePath || baseRef || acknowledgementPath)) {
358
+ const base = resolvePolicyBaseConfig({ root, configPath, basePath, baseRef, adoptMissingBase });
359
+ if (!base && (basePath || (baseRef && !adoptMissingBase) || acknowledgementPath)) {
163
360
  throw new Error('Policy delta was requested but no policy base could be resolved.');
164
361
  }
165
362
  if (!base) return undefined;
166
363
 
167
364
  // AR02/AR11: load Effective ArkRules so mode edits inside arkrules/*.json classify,
168
365
  // and attach candidate coverage so covered advisory→enforced can auto-allow.
169
- const baseLoad = loadEffectiveArkRulesFromDisk(root, base.config);
366
+ // Base ArkRules come from the SAME source as the base config (git ref or the
367
+ // base file's directory), never from the candidate working tree.
368
+ const baseArkRules = loadBaseArkRules(base);
170
369
  const candidateLoad = loadEffectiveArkRulesFromDisk(root, candidateConfig);
171
370
  if (candidateLoad.errors.length > 0) {
172
371
  const message = candidateLoad.errors
@@ -174,8 +373,6 @@ export function analyzePolicyTransition({
174
373
  .join('\n');
175
374
  throw new Error(`Invalid candidate Effective Contract:\n${message}`);
176
375
  }
177
- // Base load errors: best-effort empty (base git tree may lack arkrules files on disk).
178
- const baseArkRules = baseLoad.errors.length > 0 ? undefined : baseLoad.arkRules;
179
376
  const candidateArkRules = candidateLoad.arkRules;
180
377
 
181
378
  let candidateInvariantCoverage;
@@ -189,12 +386,20 @@ export function analyzePolicyTransition({
189
386
  fileContents: coverageInputs.fileContents,
190
387
  testFiles: coverageInputs.testFiles,
191
388
  testGlobsMissing: coverageInputs.testGlobsMissing,
192
- // No coverageStats / coverageRoots: this caller reads coverage ROWS and
193
- // drops the violations, and both only shape violation messages. Passing
194
- // them would look like wiring while changing nothing observable here.
195
389
  coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
390
+ ...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
391
+ // The declared roots set `coverageRootsDeclared` / `outsideDeclaredRoots`
392
+ // on each row, and canPromoteInvariant refuses without them. Same inputs
393
+ // as `--promote` (sensor-promote-io), so both surfaces judge alike.
394
+ ...(coverageInputs.coverageRoots ? { coverageRoots: coverageInputs.coverageRoots } : {}),
196
395
  });
197
- candidateInvariantCoverage = evaluated.coverage;
396
+ // ADR 0039: the same committed probe evidence --promote reads, so both
397
+ // surfaces refuse alike. Read-only; nothing here runs a test.
398
+ candidateInvariantCoverage = attachProbeEvidence(
399
+ root,
400
+ evaluated.coverage,
401
+ candidateArkRules.invariants
402
+ ).rows;
198
403
  }
199
404
 
200
405
  const acknowledgement = readPolicyAcknowledgement(root, acknowledgementPath);
@@ -205,7 +410,7 @@ export function analyzePolicyTransition({
205
410
  acknowledgement,
206
411
  baseSource: base.source,
207
412
  candidateSource: path.isAbsolute(configPath) ? configPath : path.join(root, configPath),
208
- ...(baseArkRules ? { baseArkRules } : {}),
413
+ baseArkRules,
209
414
  candidateArkRules,
210
415
  ...(candidateInvariantCoverage ? { candidateInvariantCoverage } : {}),
211
416
  }),
@@ -21,7 +21,7 @@ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
21
21
  export const POST_GREEN_PRIMARY_ACTION = POST_GREEN_HUMAN;
22
22
 
23
23
  /** Short label for tables / metrics. */
24
- export const POST_GREEN_PRIMARY_SHORT =
24
+ const POST_GREEN_PRIMARY_SHORT =
25
25
  '/ark-explore shape-focus → /ark-autopilot (apply one B pilot) # Shape residual';
26
26
 
27
27
  /** Placement coaching while design residual remains (new code only). */