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,474 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/cloneDetection.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/clone-detection.mjs). Zero Node I/O.
9
+ */
10
+
11
+ const ARK_CLONE_DETECTION_SCHEMA_VERSION = '1.0';
12
+ /** Token alphabet: identifiers and literals collapse; every other node is `2 + kind`. */
13
+ export const TOKEN_IDENT = 0;
14
+ /**
15
+ * Literals and JSX text.
16
+ */
17
+ export const TOKEN_LITERAL = 1;
18
+ /**
19
+ * Every other node kind is emitted as `TOKEN_KIND_OFFSET + kind`.
20
+ */
21
+ export const TOKEN_KIND_OFFSET = 2;
22
+ /*
23
+ * Fixed constants (ADR 0038 D3). Calibrated on a corpus, not user tunables;
24
+ * they change only through an ADR amendment plus a corpus rerun.
25
+ */
26
+ /** Tokens per hashed gram (k). */
27
+ export const CLONE_GRAM = 20;
28
+ /** Winnowing window (w). Any shared run of `w + k - 1` tokens shares a fingerprint. */
29
+ export const CLONE_WINDOW = 31;
30
+ /** Minimum extended run, in tokens (`>= CLONE_WINDOW + CLONE_GRAM - 1` keeps the guarantee). */
31
+ export const CLONE_MIN_TOKENS = 50;
32
+ /**
33
+ * Minimum span on each side, in lines.
34
+ */
35
+ export const CLONE_MIN_LINES = 5;
36
+ /** Share of identifiers that must match position by position. */
37
+ const CLONE_NAME_AGREEMENT = 0.6;
38
+ /** A fingerprint shared by more places than this is boilerplate: skipped and counted. */
39
+ export const CLONE_BUCKET_CAP = 16;
40
+ /** Seed pairs kept before the candidate list is `partial`. */
41
+ const CLONE_MAX_SEED_PAIRS = 50_000;
42
+ /** Families listed. The totals always carry the full count. */
43
+ export const CLONE_FAMILY_LIST_CAP = 8;
44
+ /** Seeds kept per file pair; more seeds are the same copy seen again. */
45
+ const SEEDS_PER_FILE_PAIR = 64;
46
+ export const DUPLICATION_COMMAND = 'arkgate-check --doctor --all';
47
+ const HASH_BASE = 0x01000193;
48
+ const CROSSING_ORDER = {
49
+ 'cross-slice': 0,
50
+ 'cross-parent': 1,
51
+ 'cross-sibling': 2,
52
+ 'cross-layer-walled': 3,
53
+ 'cross-layer': 4,
54
+ };
55
+ function mix32(value) {
56
+ let h = value >>> 0;
57
+ h ^= h >>> 16;
58
+ h = Math.imul(h, 0x85ebca6b);
59
+ h ^= h >>> 13;
60
+ h = Math.imul(h, 0xc2b2ae35);
61
+ h ^= h >>> 16;
62
+ return h >>> 0;
63
+ }
64
+ /**
65
+ * Rolling polynomial hash over `k`-token grams (32-bit, `Math.imul`), then
66
+ * robust winnowing: the rightmost minimum of every `w`-gram window, recorded
67
+ * once. Any shared run of `w + k - 1` tokens shares at least one fingerprint.
68
+ * `offsets` are gram start offsets into `kinds`.
69
+ */
70
+ export function fingerprint(kinds, k = CLONE_GRAM, w = CLONE_WINDOW) {
71
+ const grams = kinds.length - k + 1;
72
+ if (grams < w || k < 1 || w < 1)
73
+ return { hashes: new Uint32Array(0), offsets: new Uint32Array(0) };
74
+ let top = 1;
75
+ for (let i = 1; i < k; i += 1)
76
+ top = Math.imul(top, HASH_BASE);
77
+ const gram = new Uint32Array(grams);
78
+ let h = 0;
79
+ for (let i = 0; i < k; i += 1)
80
+ h = (Math.imul(h, HASH_BASE) + (Number(kinds[i]) + 1)) >>> 0;
81
+ gram[0] = mix32(h);
82
+ for (let i = 1; i < grams; i += 1) {
83
+ const out = Math.imul(Number(kinds[i - 1]) + 1, top);
84
+ h = (Math.imul(h - out, HASH_BASE) + (Number(kinds[i + k - 1]) + 1)) >>> 0;
85
+ gram[i] = mix32(h);
86
+ }
87
+ const pickedHash = [];
88
+ const pickedAt = [];
89
+ let min = -1;
90
+ let last = -1;
91
+ for (let right = 0; right < grams; right += 1) {
92
+ const left = right - w + 1;
93
+ if (min < left || min === -1) {
94
+ min = right;
95
+ for (let j = right - 1; j >= Math.max(0, left); j -= 1) {
96
+ if (gram[j] < gram[min])
97
+ min = j;
98
+ }
99
+ }
100
+ else if (gram[right] <= gram[min]) {
101
+ min = right;
102
+ }
103
+ if (left >= 0 && min !== last) {
104
+ pickedHash.push(gram[min]);
105
+ pickedAt.push(min);
106
+ last = min;
107
+ }
108
+ }
109
+ return { hashes: Uint32Array.from(pickedHash), offsets: Uint32Array.from(pickedAt) };
110
+ }
111
+ /**
112
+ * Sort every `(hash, file, offset)` by hash and pair files that share one.
113
+ * A run longer than `bucketCap` is boilerplate: skipped and counted. Output is
114
+ * grouped per file pair (`a < b`) and sorted, so it is deterministic.
115
+ */
116
+ export function candidatePairs(table, options = {}) {
117
+ const bucketCap = options.bucketCap ?? CLONE_BUCKET_CAP;
118
+ const maxSeedPairs = options.maxSeedPairs ?? CLONE_MAX_SEED_PAIRS;
119
+ const { hashes, files, offsets } = table;
120
+ const order = new Uint32Array(table.count);
121
+ let maxFile = 0;
122
+ for (let i = 0; i < table.count; i += 1) {
123
+ order[i] = i;
124
+ if (files[i] > maxFile)
125
+ maxFile = files[i];
126
+ }
127
+ order.sort((x, y) => hashes[x] - hashes[y] ||
128
+ files[x] - files[y] ||
129
+ offsets[x] - offsets[y]);
130
+ const byPair = new Map();
131
+ let bucketsSkipped = 0;
132
+ let seedPairs = 0;
133
+ let truncated = false;
134
+ let start = 0;
135
+ while (start < order.length && !truncated) {
136
+ let end = start + 1;
137
+ const hash = hashes[order[start]];
138
+ while (end < order.length && hashes[order[end]] === hash)
139
+ end += 1;
140
+ if (end - start > bucketCap) {
141
+ bucketsSkipped += 1;
142
+ }
143
+ else {
144
+ for (let i = start; i < end && !truncated; i += 1) {
145
+ for (let j = i + 1; j < end; j += 1) {
146
+ const x = order[i];
147
+ const y = order[j];
148
+ const fx = files[x];
149
+ const fy = files[y];
150
+ if (fx === fy)
151
+ continue;
152
+ const [a, oa, b, ob] = fx < fy ? [fx, offsets[x], fy, offsets[y]] : [fy, offsets[y], fx, offsets[x]];
153
+ const key = a * (maxFile + 1) + b;
154
+ let row = byPair.get(key);
155
+ if (!row) {
156
+ row = { a, b, shared: 0, seeds: [] };
157
+ byPair.set(key, row);
158
+ }
159
+ row.shared += 1;
160
+ if (row.seeds.length < SEEDS_PER_FILE_PAIR * 2)
161
+ row.seeds.push(oa, ob);
162
+ seedPairs += 1;
163
+ if (seedPairs >= maxSeedPairs) {
164
+ truncated = true;
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ start = end;
171
+ }
172
+ const pairs = [...byPair.values()].sort((left, right) => left.a - right.a || left.b - right.b);
173
+ return { pairs, bucketsSkipped, seedPairs, truncated };
174
+ }
175
+ /** Grow a seed left and right over equal tokens. `null` when shorter than `minTokens`. */
176
+ export function extendMatch(a, b, seedA, seedB, minTokens = CLONE_MIN_TOKENS) {
177
+ if (seedA < 0 || seedB < 0 || seedA >= a.length || seedB >= b.length)
178
+ return null;
179
+ let back = 0;
180
+ while (seedA - back - 1 >= 0 && seedB - back - 1 >= 0 && a[seedA - back - 1] === b[seedB - back - 1])
181
+ back += 1;
182
+ let ahead = 0;
183
+ while (seedA + ahead < a.length && seedB + ahead < b.length && a[seedA + ahead] === b[seedB + ahead])
184
+ ahead += 1;
185
+ const tokens = back + ahead;
186
+ if (tokens < minTokens)
187
+ return null;
188
+ return { aStart: seedA - back, aEnd: seedA + ahead, bStart: seedB - back, bEnd: seedB + ahead, tokens };
189
+ }
190
+ /** Identifiers that match position by position. Counts, not a ratio. */
191
+ export function sameNames(identsA, identsB) {
192
+ const total = Math.max(identsA.length, identsB.length);
193
+ let same = 0;
194
+ for (let i = 0; i < Math.min(identsA.length, identsB.length); i += 1) {
195
+ if (identsA[i] === identsB[i])
196
+ same += 1;
197
+ }
198
+ return { same, total };
199
+ }
200
+ /** A region with no identifiers carries no name evidence and is not kept. */
201
+ export function namesAgree(names) {
202
+ return names.total > 0 && names.same / names.total >= CLONE_NAME_AGREEMENT;
203
+ }
204
+ /**
205
+ * Map the gate's two-way decision for a file pair to a crossing. A universe
206
+ * wall that could not classify a path (`fail-closed`) is never a wall crossing.
207
+ * A parent ↔ child copy counts as same slice: the child may import the parent.
208
+ */
209
+ export function classifyCrossing(input) {
210
+ if (!input.layerA || !input.layerB)
211
+ return 'unplaced';
212
+ const decisions = [input.forward, input.backward].filter((row) => Boolean(row));
213
+ const real = decisions.filter((row) => row.crossing !== 'fail-closed');
214
+ if (input.layerA !== input.layerB) {
215
+ const forward = input.forward && input.forward.crossing !== 'fail-closed';
216
+ const backward = input.backward && input.backward.crossing !== 'fail-closed';
217
+ return forward && backward ? 'cross-layer-walled' : 'cross-layer';
218
+ }
219
+ if (real.some((row) => row.crossing === 'cross-sibling'))
220
+ return 'cross-sibling';
221
+ const parent = real.find((row) => row.crossing === 'cross-parent');
222
+ if (parent)
223
+ return parent.childSlices ? 'cross-parent' : 'cross-slice';
224
+ if (decisions.length > real.length)
225
+ return 'fail-closed';
226
+ return 'same-slice';
227
+ }
228
+ /** Is this crossing one the section lists? */
229
+ export function isListedCrossing(outcome) {
230
+ return Object.prototype.hasOwnProperty.call(CROSSING_ORDER, outcome);
231
+ }
232
+ const GENERATED_HEADER = /GENERATED FILE|@generated|DO NOT EDIT|\bgenerated (?:from|by)\b/i;
233
+ /** First five lines carry a generated marker. */
234
+ export function isGeneratedHeader(text) {
235
+ const head = String(text).split('\n', 5).join('\n');
236
+ return GENERATED_HEADER.test(head);
237
+ }
238
+ function byText(left, right) {
239
+ return left < right ? -1 : left > right ? 1 : 0;
240
+ }
241
+ /**
242
+ * Union-find over verified pairs. Overlapping spans in one file are one member.
243
+ * Members and pairs come out sorted, so pair order never changes a family.
244
+ */
245
+ export function groupFamilies(pairs) {
246
+ const spans = [];
247
+ for (const pair of pairs)
248
+ spans.push(pair.a, pair.b);
249
+ const order = spans.map((_, index) => index);
250
+ order.sort((x, y) => {
251
+ const left = spans[x];
252
+ const right = spans[y];
253
+ return byText(left.path, right.path) || left.start - right.start || left.end - right.end;
254
+ });
255
+ const memberOf = new Array(spans.length).fill(-1);
256
+ const members = [];
257
+ for (const index of order) {
258
+ const span = spans[index];
259
+ const current = members[members.length - 1];
260
+ if (current && current.path === span.path && span.start < current.end) {
261
+ current.end = Math.max(current.end, span.end);
262
+ current.startLine = Math.min(current.startLine, span.startLine);
263
+ current.endLine = Math.max(current.endLine, span.endLine);
264
+ current.tokens = current.end - current.start;
265
+ }
266
+ else {
267
+ members.push({ ...span, tokens: span.end - span.start });
268
+ }
269
+ memberOf[index] = members.length - 1;
270
+ }
271
+ const parent = members.map((_, index) => index);
272
+ const find = (x) => {
273
+ let root = x;
274
+ while (parent[root] !== root)
275
+ root = parent[root];
276
+ while (parent[x] !== root) {
277
+ const next = parent[x];
278
+ parent[x] = root;
279
+ x = next;
280
+ }
281
+ return root;
282
+ };
283
+ pairs.forEach((_, index) => {
284
+ const left = find(memberOf[index * 2]);
285
+ const right = find(memberOf[index * 2 + 1]);
286
+ if (left !== right)
287
+ parent[Math.max(left, right)] = Math.min(left, right);
288
+ });
289
+ const groups = new Map();
290
+ members.forEach((_, index) => {
291
+ const root = find(index);
292
+ groups.set(root, [...(groups.get(root) ?? []), index]);
293
+ });
294
+ const families = [];
295
+ for (const indexes of groups.values()) {
296
+ const local = new Map(indexes.map((memberIndex, position) => [memberIndex, position]));
297
+ const familyPairs = [];
298
+ const seen = new Set();
299
+ pairs.forEach((pair, index) => {
300
+ const a = local.get(memberOf[index * 2]);
301
+ const b = local.get(memberOf[index * 2 + 1]);
302
+ if (a === undefined || b === undefined || a === b)
303
+ return;
304
+ const [low, high] = a < b ? [a, b] : [b, a];
305
+ const key = `${low}:${high}`;
306
+ if (seen.has(key))
307
+ return;
308
+ seen.add(key);
309
+ familyPairs.push({ a: low, b: high, crossing: pair.crossing, names: { ...pair.names } });
310
+ });
311
+ familyPairs.sort((left, right) => left.a - right.a || left.b - right.b || CROSSING_ORDER[left.crossing] - CROSSING_ORDER[right.crossing]);
312
+ families.push({
313
+ members: indexes.map((memberIndex) => {
314
+ const member = members[memberIndex];
315
+ return {
316
+ path: member.path,
317
+ ...(member.layer ? { layer: member.layer } : {}),
318
+ startLine: member.startLine,
319
+ endLine: member.endLine,
320
+ tokens: member.tokens,
321
+ };
322
+ }),
323
+ pairs: familyPairs,
324
+ });
325
+ }
326
+ return families.sort((left, right) => byText(left.members[0]?.path ?? '', right.members[0]?.path ?? ''));
327
+ }
328
+ function dirOf(rel) {
329
+ const at = rel.lastIndexOf('/');
330
+ return at === -1 ? '.' : rel.slice(0, at);
331
+ }
332
+ /**
333
+ * The pair that names a family: listing priority, then the best name agreement.
334
+ */
335
+ export function primaryPair(family) {
336
+ return [...family.pairs].sort((left, right) => CROSSING_ORDER[left.crossing] - CROSSING_ORDER[right.crossing] ||
337
+ right.names.same * left.names.total - left.names.same * right.names.total ||
338
+ left.a - right.a ||
339
+ left.b - right.b)[0];
340
+ }
341
+ function destinationText(destination) {
342
+ switch (destination.kind) {
343
+ case 'shared-root':
344
+ case 'universe-common':
345
+ return `move it to ${destination.path} with /ark-place`;
346
+ case 'lower-layer':
347
+ return `keep it in ${destination.layer}, the layer both sides may import, with /ark-place`;
348
+ case 'shared-layer':
349
+ return `move it to ${destination.layer}, a layer both sides may import, with /ark-place`;
350
+ case 'either-layer':
351
+ return `keep one copy in ${(destination.layers ?? []).join(' or ')} and import it from the other, with /ark-place`;
352
+ default:
353
+ return 'find a shared home your config lets both sides import with /ark-place';
354
+ }
355
+ }
356
+ /** One human line per family. Shared by the terminal and the report. */
357
+ export function cloneFamilyLine(family) {
358
+ const pair = primaryPair(family);
359
+ const left = family.members[pair?.a ?? 0];
360
+ const right = family.members[pair?.b ?? 1];
361
+ const more = family.members.length > 2 ? ` (+${family.members.length - 2} more)` : '';
362
+ const names = `${family.names.same} of ${family.names.total} names match`;
363
+ const next = destinationText(family.destination);
364
+ if (family.crossing === 'cross-layer' || family.crossing === 'cross-layer-walled') {
365
+ const wall = family.crossing === 'cross-layer-walled' ? ' Neither layer may import the other.' : '';
366
+ return `This code is copied between the ${left?.layer ?? '?'} and ${right?.layer ?? '?'} layers${more} (${names}).${wall} Next: ${next}.`;
367
+ }
368
+ return `This code is copied between ${dirOf(left?.path ?? '')} and ${dirOf(right?.path ?? '')}${more} (${names}). The wall stops the import, not the copy. Next: ${next}.`;
369
+ }
370
+ function method() {
371
+ return {
372
+ gram: CLONE_GRAM,
373
+ window: CLONE_WINDOW,
374
+ minTokens: CLONE_MIN_TOKENS,
375
+ minLines: CLONE_MIN_LINES,
376
+ nameAgreement: CLONE_NAME_AGREEMENT,
377
+ bucketCap: CLONE_BUCKET_CAP,
378
+ };
379
+ }
380
+ /** All-zero totals, for `not-run` / `unavailable`. */
381
+ export function emptyDuplicationTotals() {
382
+ return {
383
+ filesEligible: 0,
384
+ filesFingerprinted: 0,
385
+ filesSkipped: {},
386
+ fingerprints: 0,
387
+ seedPairs: 0,
388
+ bucketsSkipped: 0,
389
+ candidateFilePairs: 0,
390
+ pairsCrossBoundary: 0,
391
+ pairsVerified: 0,
392
+ pairsSameSliceUnexamined: 0,
393
+ pairsUnclassifiable: 0,
394
+ pairsUnplaced: 0,
395
+ clonePairs: 0,
396
+ families: 0,
397
+ };
398
+ }
399
+ function shell(status, headline, honesty) {
400
+ return {
401
+ schemaVersion: ARK_CLONE_DETECTION_SCHEMA_VERSION,
402
+ advisory: true,
403
+ notAScore: true,
404
+ status,
405
+ headline,
406
+ families: [],
407
+ truncated: 0,
408
+ totals: emptyDuplicationTotals(),
409
+ method: method(),
410
+ honesty,
411
+ };
412
+ }
413
+ /** Compact status and scoped runs: the JSON names the command that runs it. */
414
+ export function notRunDuplication(reason) {
415
+ return {
416
+ ...shell('not-run', 'Copies across a wall are checked in status details.', []),
417
+ next: DUPLICATION_COMMAND,
418
+ ...(reason ? { reason } : {}),
419
+ };
420
+ }
421
+ export function unavailableDuplication(note) {
422
+ return shell('unavailable', 'Copies across a wall could not be checked.', [note]);
423
+ }
424
+ /**
425
+ * Final section. Families are listed walled first, then by size, then by path,
426
+ * capped with an honest `truncated`. Any partial reason makes the status `partial`.
427
+ */
428
+ export function buildDuplicationAdvisory(input) {
429
+ const families = [];
430
+ for (const draft of input.families) {
431
+ const pair = primaryPair(draft);
432
+ if (!pair)
433
+ continue;
434
+ const walled = pair.crossing !== 'cross-layer';
435
+ const family = {
436
+ ruleId: walled ? 'CROSS_WALL_DUPLICATE' : 'CROSS_LAYER_DUPLICATE',
437
+ crossing: pair.crossing,
438
+ names: { ...pair.names },
439
+ members: draft.members.map((member) => ({ ...member })),
440
+ pairs: draft.pairs.map((row) => ({ ...row, names: { ...row.names } })),
441
+ destination: {
442
+ ...draft.destination,
443
+ ...(draft.destination.layers ? { layers: [...draft.destination.layers] } : {}),
444
+ },
445
+ line: '',
446
+ };
447
+ family.line = cloneFamilyLine(family);
448
+ families.push(family);
449
+ }
450
+ const size = (family) => Math.max(0, ...family.members.map((member) => member.tokens));
451
+ families.sort((left, right) => CROSSING_ORDER[left.crossing] - CROSSING_ORDER[right.crossing] ||
452
+ size(right) - size(left) ||
453
+ byText(left.members[0]?.path ?? '', right.members[0]?.path ?? ''));
454
+ const listed = families.slice(0, CLONE_FAMILY_LIST_CAP);
455
+ const partial = input.partialReasons.length > 0;
456
+ const count = families.length;
457
+ const headline = count === 0
458
+ ? partial
459
+ ? 'No copy across a wall or a layer was found in the files checked.'
460
+ : 'No copy crosses a wall or a layer.'
461
+ : `${count} cop${count === 1 ? 'y crosses' : 'ies cross'} a wall or a layer.`;
462
+ return {
463
+ schemaVersion: ARK_CLONE_DETECTION_SCHEMA_VERSION,
464
+ advisory: true,
465
+ notAScore: true,
466
+ status: partial ? 'partial' : 'complete',
467
+ headline,
468
+ families: listed,
469
+ truncated: count - listed.length,
470
+ totals: { ...input.totals, filesSkipped: { ...input.totals.filesSkipped }, families: count },
471
+ method: method(),
472
+ honesty: [...input.partialReasons, ...(input.notes ?? [])],
473
+ };
474
+ }
@@ -7,8 +7,9 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { execCommandParts } from '../ark-shared.mjs';
10
+ import { npxArkgatePrefixLength } from './package-manager.mjs';
10
11
 
11
- export const PREFERRED_CODEX_MCP_BIN = 'arkgate-mcp';
12
+ const PREFERRED_CODEX_MCP_BIN = 'arkgate-mcp';
12
13
 
13
14
  /** Where Codex loads slash-command prompts ($CODEX_HOME/prompts) — legacy, not the skill catalog. */
14
15
  export function codexPromptsDir() {
@@ -204,8 +205,9 @@ function codexArkMcpInvocation(command, args) {
204
205
  const argv = [command, ...args];
205
206
  if (isArkMcpToken(command)) return { binArgs: argv.slice(1) };
206
207
  const runner = executableName(command);
207
- if ((runner === 'npx' || runner === 'yarn') && isArkMcpToken(args[0])) {
208
- return { binArgs: argv.slice(2) };
208
+ const npxPrefix = runner === 'npx' ? npxArkgatePrefixLength(args) : 0;
209
+ if ((runner === 'npx' || runner === 'yarn') && isArkMcpToken(args[npxPrefix])) {
210
+ return { binArgs: argv.slice(npxPrefix + 2) };
209
211
  }
210
212
  if (runner === 'node') {
211
213
  const script = args[0]?.replace(/\\/g, '/');