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
@@ -8,8 +8,9 @@
8
8
  * Pure CLI helper (bin/lib/arkrules-contract.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
+ import { isObject, validateSchemaNode, valueType } from './schema-validation.mjs';
11
12
  export const ARK_RULES_SCHEMA_VERSION = '1.0';
12
- export const ARK_RULES_SCHEMA_URL = 'https://unpkg.com/arkgate/schemas/ark.arkrules.schema.json';
13
+ const ARK_RULES_SCHEMA_URL = 'https://unpkg.com/arkgate/schemas/ark.arkrules.schema.json';
13
14
  /** Closed sensor vocabulary — keep in lockstep with arkRulesTypes.ARK_RULE_SENSOR_IDS. */
14
15
  export const ARK_RULE_SENSORS = [
15
16
  'aggregate-private-state',
@@ -105,113 +106,6 @@ export class ArkRulesValidationError extends Error {
105
106
  this.issues = issues;
106
107
  }
107
108
  }
108
- function isObject(value) {
109
- return value !== null && typeof value === 'object' && !Array.isArray(value);
110
- }
111
- function propertyPath(parent, key) {
112
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
113
- ? `${parent}.${key}`
114
- : `${parent}[${JSON.stringify(key)}]`;
115
- }
116
- function valueType(value) {
117
- if (value === null)
118
- return 'null';
119
- if (Array.isArray(value))
120
- return 'array';
121
- return typeof value;
122
- }
123
- function resolveSchemaRef(ref, root) {
124
- const prefix = '#/$defs/';
125
- if (!ref.startsWith(prefix))
126
- return undefined;
127
- return root.$defs[ref.slice(prefix.length)];
128
- }
129
- function validateNode(value, schema, path, root, issues) {
130
- if (schema.$ref) {
131
- const referenced = resolveSchemaRef(schema.$ref, root);
132
- if (!referenced) {
133
- issues.push({ path, message: `schema reference ${schema.$ref} cannot be resolved` });
134
- return;
135
- }
136
- validateNode(value, referenced, path, root, issues);
137
- return;
138
- }
139
- if (schema.const !== undefined && !Object.is(value, schema.const)) {
140
- issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
141
- return;
142
- }
143
- if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
144
- issues.push({ path, message: `must be one of ${schema.enum.map(String).join(', ')}` });
145
- return;
146
- }
147
- if (schema.type === 'object') {
148
- if (!isObject(value)) {
149
- issues.push({ path, message: `must be an object; received ${valueType(value)}` });
150
- return;
151
- }
152
- const properties = schema.properties ?? {};
153
- for (const key of schema.required ?? []) {
154
- if (value[key] === undefined) {
155
- issues.push({ path: propertyPath(path, key), message: 'is required' });
156
- }
157
- }
158
- if (schema.additionalProperties === false) {
159
- for (const key of Object.keys(value)) {
160
- if (!(key in properties)) {
161
- issues.push({ path: propertyPath(path, key), message: 'unknown field' });
162
- }
163
- }
164
- }
165
- else if (isObject(schema.additionalProperties)) {
166
- const additional = schema.additionalProperties;
167
- for (const key of Object.keys(value)) {
168
- if (!(key in properties)) {
169
- validateNode(value[key], additional, propertyPath(path, key), root, issues);
170
- }
171
- }
172
- }
173
- for (const [key, childSchema] of Object.entries(properties)) {
174
- if (value[key] !== undefined) {
175
- validateNode(value[key], childSchema, propertyPath(path, key), root, issues);
176
- }
177
- }
178
- return;
179
- }
180
- if (schema.type === 'array') {
181
- if (!Array.isArray(value)) {
182
- issues.push({ path, message: `must be an array; received ${valueType(value)}` });
183
- return;
184
- }
185
- if (schema.minItems !== undefined && value.length < schema.minItems) {
186
- issues.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
187
- }
188
- if (schema.uniqueItems) {
189
- const serialized = value.map((entry) => JSON.stringify(entry));
190
- if (new Set(serialized).size !== serialized.length) {
191
- issues.push({ path, message: 'must not contain duplicate items' });
192
- }
193
- }
194
- if (schema.items) {
195
- value.forEach((entry, index) => validateNode(entry, schema.items, `${path}[${index}]`, root, issues));
196
- }
197
- return;
198
- }
199
- if (schema.type === 'string') {
200
- if (typeof value !== 'string') {
201
- issues.push({ path, message: `must be a string; received ${valueType(value)}` });
202
- return;
203
- }
204
- if (schema.minLength !== undefined && value.length < schema.minLength) {
205
- issues.push({ path, message: `must contain at least ${schema.minLength} character(s)` });
206
- }
207
- return;
208
- }
209
- if (schema.type === 'boolean') {
210
- if (typeof value !== 'boolean') {
211
- issues.push({ path, message: `must be a boolean; received ${valueType(value)}` });
212
- }
213
- }
214
- }
215
109
  function defaultedArkRules(input) {
216
110
  return {
217
111
  ...input,
@@ -299,7 +193,7 @@ export function loadArkRulesContract(input, source = 'arkrules.json', expectedLa
299
193
  }
300
194
  const candidate = defaultedArkRules(input);
301
195
  const issues = [];
302
- validateNode(candidate, ARK_RULES_SCHEMA, '$', ARK_RULES_SCHEMA, issues);
196
+ validateSchemaNode(candidate, ARK_RULES_SCHEMA, '$', ARK_RULES_SCHEMA, issues);
303
197
  validateSemantics(candidate, issues);
304
198
  if (expectedLayer !== undefined &&
305
199
  typeof candidate.layer === 'string' &&
@@ -328,39 +222,136 @@ export function parseArkRulesJson(json, source = 'arkrules.json', expectedLayer)
328
222
  }
329
223
  return loadArkRulesContract(input, source, expectedLayer);
330
224
  }
225
+ function effectiveRuleId(part, localId) {
226
+ return part.childId ? `${part.childId}#${localId}` : localId;
227
+ }
228
+ function effectiveAppliesTo(part, appliesTo) {
229
+ if (appliesTo && appliesTo.length > 0)
230
+ return [...appliesTo];
231
+ if (part.childId && part.defaultAppliesTo && part.defaultAppliesTo.length > 0) {
232
+ return [...part.defaultAppliesTo];
233
+ }
234
+ return undefined;
235
+ }
236
+ function provenanceFor(part, localId) {
237
+ return {
238
+ sourceFile: part.sourceFile,
239
+ ruleId: effectiveRuleId(part, localId),
240
+ layer: part.layer,
241
+ ...(part.childId ? { childId: part.childId } : {}),
242
+ };
243
+ }
244
+ /**
245
+ * True when every path the glob can match stays under the slice root.
246
+ * A wider prefix, a `**` that is not the last segment, or `..` escapes.
247
+ */
248
+ function appliesToPatternInsideSlice(pattern, sliceRoot) {
249
+ const root = sliceRoot.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
250
+ const value = pattern.trim().replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase();
251
+ if (!value || !root || value.includes('..'))
252
+ return false;
253
+ if (value === root || value === `${root}/**` || value === `${root}/*`)
254
+ return true;
255
+ if (value.startsWith(`${root}/`)) {
256
+ const parts = value.slice(root.length + 1).split('/');
257
+ const globStar = parts.indexOf('**');
258
+ if (globStar !== -1 && globStar !== parts.length - 1)
259
+ return false;
260
+ return parts.every((part) => part.length > 0);
261
+ }
262
+ const bareRoot = root.replace(/^(?:src|app)\//, '');
263
+ const bareValue = value.replace(/^(?:src|app)\//, '');
264
+ if (bareRoot !== root || bareValue !== value)
265
+ return appliesToPatternInsideSlice(bareValue, bareRoot);
266
+ return false;
267
+ }
268
+ /** Explicit appliesTo on a discovered file that can match outside its slice. */
269
+ export function sliceScopeEscapes(part) {
270
+ if (!part.childId || !part.defaultAppliesTo?.length)
271
+ return [];
272
+ const sliceRoot = String(part.defaultAppliesTo[0]).replace(/\/\*\*$/, '');
273
+ const escapes = [];
274
+ const entries = [...(part.file.structure ?? []), ...(part.file.invariants ?? [])];
275
+ for (const entry of entries) {
276
+ for (const pattern of entry.appliesTo ?? []) {
277
+ if (!appliesToPatternInsideSlice(pattern, sliceRoot))
278
+ escapes.push({ id: entry.id, pattern });
279
+ }
280
+ }
281
+ return escapes;
282
+ }
283
+ /** Effective ids declared by more than one part. Central ids stay bare; slice ids are namespaced. */
284
+ export function duplicateArkRuleIds(parts) {
285
+ const seen = new Map();
286
+ const dupes = new Map();
287
+ for (const part of parts) {
288
+ const localIds = [
289
+ ...(part.file.structure ?? []).map((entry) => entry.id),
290
+ ...(part.file.invariants ?? []).map((entry) => entry.id),
291
+ ];
292
+ for (const localId of localIds) {
293
+ const id = effectiveRuleId(part, localId);
294
+ const key = `${part.layer}\0${id}`;
295
+ const existing = seen.get(key);
296
+ if (!existing) {
297
+ seen.set(key, { id, layer: part.layer, sourceFiles: [part.sourceFile] });
298
+ continue;
299
+ }
300
+ const row = dupes.get(key) ?? { id, layer: part.layer, sourceFiles: [...existing.sourceFiles] };
301
+ if (!row.sourceFiles.includes(part.sourceFile))
302
+ row.sourceFiles.push(part.sourceFile);
303
+ dupes.set(key, row);
304
+ }
305
+ }
306
+ return [...dupes.values()];
307
+ }
331
308
  /**
332
309
  * Build the Effective Contract from already-validated ArkRules files.
333
- * Callers supply `{ layer → { sourceFile, file } }` after resolving references.
310
+ * Same-layer parts merge. A child id namespaces the effective id and does not rewrite the file.
334
311
  */
335
312
  export function buildEffectiveArkRules(parts) {
336
313
  const byLayer = {};
337
314
  const structure = [];
338
315
  const invariants = [];
339
- const ordered = [...parts].sort((a, b) => a.layer.localeCompare(b.layer));
316
+ const ordered = [...parts].sort((a, b) => a.layer.localeCompare(b.layer) || a.sourceFile.localeCompare(b.sourceFile));
340
317
  for (const part of ordered) {
341
- const structureRules = (part.file.structure ?? []).map((entry) => ({
342
- ...entry,
343
- mode: normalizeMode(entry.mode),
344
- provenance: {
345
- sourceFile: part.sourceFile,
346
- ruleId: entry.id,
347
- layer: part.layer,
348
- },
349
- }));
350
- const invariantRules = (part.file.invariants ?? []).map((entry) => ({
351
- ...entry,
352
- mode: normalizeMode(entry.mode),
353
- provenance: {
318
+ const structureRules = (part.file.structure ?? []).map((entry) => {
319
+ const appliesTo = effectiveAppliesTo(part, entry.appliesTo);
320
+ return {
321
+ ...entry,
322
+ id: effectiveRuleId(part, entry.id),
323
+ ...(appliesTo ? { appliesTo } : { appliesTo: undefined }),
324
+ mode: normalizeMode(entry.mode),
325
+ provenance: provenanceFor(part, entry.id),
326
+ };
327
+ });
328
+ const invariantRules = (part.file.invariants ?? []).map((entry) => {
329
+ const appliesTo = effectiveAppliesTo(part, entry.appliesTo);
330
+ return {
331
+ ...entry,
332
+ id: effectiveRuleId(part, entry.id),
333
+ ...(appliesTo ? { appliesTo } : { appliesTo: undefined }),
334
+ mode: normalizeMode(entry.mode),
335
+ provenance: provenanceFor(part, entry.id),
336
+ };
337
+ });
338
+ const bucket = byLayer[part.layer];
339
+ if (!bucket) {
340
+ byLayer[part.layer] = {
354
341
  sourceFile: part.sourceFile,
355
- ruleId: entry.id,
356
- layer: part.layer,
357
- },
358
- }));
359
- byLayer[part.layer] = {
360
- sourceFile: part.sourceFile,
361
- structure: structureRules,
362
- invariants: invariantRules,
363
- };
342
+ sourceFiles: [part.sourceFile],
343
+ structure: structureRules,
344
+ invariants: invariantRules,
345
+ };
346
+ }
347
+ else {
348
+ if (!bucket.sourceFiles)
349
+ bucket.sourceFiles = [bucket.sourceFile];
350
+ if (!bucket.sourceFiles.includes(part.sourceFile))
351
+ bucket.sourceFiles.push(part.sourceFile);
352
+ bucket.structure.push(...structureRules);
353
+ bucket.invariants.push(...invariantRules);
354
+ }
364
355
  structure.push(...structureRules);
365
356
  invariants.push(...invariantRules);
366
357
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { findClassDeclarations, scanClassMembers } from './class-source-scan.mjs';
12
- import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
12
+ import { sourceHasPersistenceWrite, sourceHasTaggedSqlWrite, sourceImportsPersistenceDriverText, } from './persistence-write-hint.mjs';
13
13
  /** Keep in lockstep with arkRulesTypes.ARK_RULE_TIER2_SENSOR_IDS (self-contained for CLI gen). */
14
14
  const ARK_RULE_TIER2_SENSOR_IDS = ['no-anemic-model'];
15
15
  /**
@@ -35,7 +35,7 @@ const THIS_FIELD_ASSIGNMENT_RE = /\bthis\.[A-Za-z_][A-Za-z0-9_]*\s*=(?!=)/g;
35
35
  export function expectedDomainInvariantWordsPhrase() {
36
36
  return `${DOMAIN_INVARIANT_WORDS.join(', ')}, or events-array .push(`;
37
37
  }
38
- export function referencesGuardOrPublish(source) {
38
+ function referencesGuardOrPublish(source) {
39
39
  return DOMAIN_INVARIANT_WORD_RE.test(source) || DOMAIN_EVENTS_PUSH_RE.test(source);
40
40
  }
41
41
  export function isIdiomaticEventsReset(source, assignIndex, methodName) {
@@ -468,10 +468,10 @@ export function collectEmptyInvariantCatalogFindings(input) {
468
468
  },
469
469
  ];
470
470
  }
471
- export function isPersistenceDriverLayer(layer) {
471
+ function isPersistenceDriverLayer(layer) {
472
472
  return layer === 'PersistenceAdapters';
473
473
  }
474
- export function sourceImportsPersistenceDriver(content, resolvedImports) {
474
+ function sourceImportsPersistenceDriver(content, resolvedImports) {
475
475
  if (sourceImportsPersistenceDriverText(content))
476
476
  return true;
477
477
  if (!resolvedImports)
@@ -502,7 +502,9 @@ export function deriveArkRuleFileHints(_file, content, resolvedImports) {
502
502
  if (!content)
503
503
  return null;
504
504
  const hasIo = sourceImportsPersistenceDriver(content, resolvedImports);
505
- const persistenceWrite = hasIo && sourceHasPersistenceWrite(content);
505
+ // A tagged-template SQL write head is evidence on its own (the use case composed
506
+ // the statement). ORM verbs and untagged SQL still need a driver import.
507
+ const persistenceWrite = (hasIo && sourceHasPersistenceWrite(content)) || sourceHasTaggedSqlWrite(content);
506
508
  // Orchestration/adapter heuristics need a longer window; writes still fire on short probes.
507
509
  if (content.length < 40) {
508
510
  return persistenceWrite ? { persistenceWrite: true } : null;
@@ -24,7 +24,9 @@ export const LOCAL_STRICT_MERGE_MESSAGE =
24
24
  '--local cannot be combined with --strict-merge: CI stays the full fail-closed check. Use --local (or ARK_CHECK_LOCAL=1) only on the local / pre-push path.';
25
25
 
26
26
  export const LOCAL_TREE_MODE_MESSAGE =
27
- '--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote) or with --update-baseline. Use --local --base <ref> for the cheap local check.';
27
+ '--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote, --probe-invariants) or with --update-baseline. Use --local --base <ref> for the cheap local check.';
28
+
29
+ const PROBE_RUNNERS = ['vitest', 'jest', 'node'];
28
30
 
29
31
  export const UPDATE_BASELINE_SCOPE_MESSAGE =
30
32
  '--update-baseline cannot be combined with a changed-files scope (--changed, --local, or a --persona that implies --changed): the baseline is a whole-tree record, and a changed-files scan would overwrite it with only that subset and delete every other frozen key. Drop the scope flag (ARK_CHECK_LOCAL=1 is ignored for a baseline update) and re-run.';
@@ -66,6 +68,7 @@ export function applyLocalCheckMode(args, env = process.env) {
66
68
  args.plan && '--plan',
67
69
  args.report && '--report',
68
70
  args.promote && '--promote',
71
+ args.probeInvariants && '--probe-invariants',
69
72
  args.updateBaseline && '--update-baseline',
70
73
  ].filter(Boolean);
71
74
  if (args.strictMerge) {
@@ -201,6 +204,24 @@ export function parseArgs(argv, env = process.env) {
201
204
  args.promote = next && !next.startsWith('-') ? argv[++i] : true;
202
205
  }
203
206
  }
207
+ else if (arg === '--probe-invariants' || arg.startsWith('--probe-invariants=')) {
208
+ // Same shape as --promote: bare probes every declared invariant; a value
209
+ // narrows to one id (`=<id>` also carries an id that begins with '-').
210
+ const eq = arg.indexOf('=');
211
+ if (eq > 0) {
212
+ args.probeInvariants = arg.slice(eq + 1) || true;
213
+ } else {
214
+ const next = argv[i + 1];
215
+ args.probeInvariants = next && !next.startsWith('-') ? argv[++i] : true;
216
+ }
217
+ }
218
+ else if (arg === '--runner') {
219
+ const value = requireValue(arg, i++).trim().toLowerCase();
220
+ if (!PROBE_RUNNERS.includes(value)) {
221
+ throw new Error(`--runner must be one of ${PROBE_RUNNERS.join(', ')} (got ${JSON.stringify(value)}).`);
222
+ }
223
+ args.runner = value;
224
+ }
204
225
  else if (arg === '--apply') args.apply = true;
205
226
  else if (arg === '--recommend') args.recommend = true;
206
227
  else if (arg === '--write-plan') args.writePlan = true;
@@ -307,8 +328,49 @@ export function parseArgs(argv, env = process.env) {
307
328
  );
308
329
  }
309
330
  }
331
+ assertProbeInvariantsAlone(args);
310
332
  if (args.updateBaseline && (args.changed || args.local)) {
311
333
  throw new Error(UPDATE_BASELINE_SCOPE_MESSAGE);
312
334
  }
313
335
  return applyLocalCheckMode(args, env);
314
336
  }
337
+
338
+ /**
339
+ * ADR 0039 D1: the probe runs the project's own tests in a temporary copy, so
340
+ * it is never part of a check, the merge gate, status, or a changed-files scope.
341
+ * Every pairing that would put it on one of those paths is refused, and a
342
+ * report mode that answers first would make it silently do nothing.
343
+ */
344
+ function assertProbeInvariantsAlone(args) {
345
+ if (args.runner && !args.probeInvariants) {
346
+ throw new Error('--runner applies to --probe-invariants. Run `arkgate-check --probe-invariants --runner <id>`.');
347
+ }
348
+ if (!args.probeInvariants) return;
349
+ const gatePaths = [
350
+ args.local && '--local',
351
+ args.changed && '--changed',
352
+ args.strictMerge && '--strict-merge',
353
+ args.updateBaseline && '--update-baseline',
354
+ args.doctor && '--doctor',
355
+ ].filter(Boolean);
356
+ if (gatePaths.length > 0) {
357
+ throw new Error(
358
+ `--probe-invariants cannot be combined with ${gatePaths.join(', ')}: the probe runs your tests in a temporary copy and is never part of a check, the merge gate, or status. Run it on its own.`
359
+ );
360
+ }
361
+ const shadowing = [
362
+ args.promote && '--promote',
363
+ args.sensors && '--sensors',
364
+ args.pathDrift && '--path-drift',
365
+ args.coverage && '--coverage',
366
+ args.plan && '--plan',
367
+ args.report && '--report',
368
+ args.rulesInventory && '--rules-inventory',
369
+ args.recommend && '--recommend',
370
+ ].filter(Boolean);
371
+ if (shadowing.length > 0) {
372
+ throw new Error(
373
+ `--probe-invariants cannot be combined with ${shadowing.join(', ')}: one of them would answer first and the other would silently do nothing. Run them separately.`
374
+ );
375
+ }
376
+ }
@@ -16,24 +16,13 @@ import {
16
16
  buildAgentProjectionBlock,
17
17
  } from './agent-projection.mjs';
18
18
  import { getDiagnosticCatalogEntry } from './diagnostic-catalog.mjs';
19
- import { falseGreenAdoptionGap } from './field-install.mjs';
19
+ import { addPackageJsonPropertyPreservingFormat } from './field-install.mjs';
20
20
  import { renderHostSupportMatrixMarkdown } from './host-support-matrix.mjs';
21
21
  import { PREFERRED_MCP_BIN } from './hook-templates.mjs';
22
22
  import { hasCheckArchitectureScript, readPackageJson } from './gate-files.mjs';
23
23
  import { arkPackageVersion } from './skill-install.mjs';
24
24
  import { arkgateIsProjectDependency, pinnedArkgateRunner } from './package-manager.mjs';
25
25
 
26
- // Field-install helpers re-exported for callers that import from this module.
27
- export {
28
- ensureBaselineFlagInCheckCommand,
29
- syncBaselineIntoCheckSurfaces,
30
- pinArkgateDevDependency,
31
- IO_DIR_SEGMENTS,
32
- detectContractFalseGreenRisk,
33
- FALSE_GREEN_GAP_ID,
34
- falseGreenAdoptionGap,
35
- } from './field-install.mjs';
36
-
37
26
  export function checkArgsForRoot(root, { requireGates = false } = {}) {
38
27
  const baselineFlag = fs.existsSync(path.join(root, '.ark-baseline.json'))
39
28
  ? ' --baseline .ark-baseline.json'
@@ -42,7 +31,6 @@ export function checkArgsForRoot(root, { requireGates = false } = {}) {
42
31
  return `--root . --config ark.config.json ${profile}${baselineFlag}`;
43
32
  }
44
33
 
45
-
46
34
  export function packageManager(root) {
47
35
  const pm = localPackageManager(root);
48
36
  if (arkgateIsProjectDependency(root)) return pm;
@@ -114,65 +102,13 @@ export function checkArchitectureScriptSnippet(root) {
114
102
  * @param {string} scriptValue
115
103
  */
116
104
  function addPackageScriptPreservingFormat(source, scriptName, scriptValue) {
117
- const multiline = /\r?\n/.test(source);
118
- const eol = source.includes('\r\n') ? '\r\n' : '\n';
119
- const rootPropertyIndent = source.match(/\r?\n([ \t]+)"[^"\n]+"\s*:/)?.[1] ?? ' ';
120
- const indentUnit = rootPropertyIndent;
121
- const encoded = JSON.stringify(scriptValue);
122
- const key = JSON.stringify(scriptName);
123
- const scriptsMatch = /"scripts"\s*:\s*\{/.exec(source);
124
-
125
- if (scriptsMatch) {
126
- const open = source.indexOf('{', scriptsMatch.index);
127
- let depth = 0;
128
- let quoted = false;
129
- let escaped = false;
130
- let close = -1;
131
- for (let index = open; index < source.length; index += 1) {
132
- const char = source[index];
133
- if (quoted) {
134
- if (escaped) escaped = false;
135
- else if (char === '\\') escaped = true;
136
- else if (char === '"') quoted = false;
137
- continue;
138
- }
139
- if (char === '"') quoted = true;
140
- else if (char === '{') depth += 1;
141
- else if (char === '}' && --depth === 0) {
142
- close = index;
143
- break;
144
- }
145
- }
146
- if (close === -1) throw new Error('Unbalanced package.json scripts object');
147
- const body = source.slice(open + 1, close);
148
- if (!multiline) {
149
- const addition = body.trim() ? `,${key}:${encoded}` : `${key}:${encoded}`;
150
- return `${source.slice(0, close)}${addition}${source.slice(close)}`;
151
- }
152
- const beforeClose = source.slice(0, close);
153
- const trailing = beforeClose.match(/\s*$/)?.[0] ?? '';
154
- const contentEnd = close - trailing.length;
155
- const closingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
156
- const propertyIndent = `${closingIndent}${indentUnit}`;
157
- const addition = body.trim()
158
- ? `,${eol}${propertyIndent}${key}: ${encoded}`
159
- : `${propertyIndent}${key}: ${encoded}`;
160
- return `${source.slice(0, contentEnd)}${addition}${eol}${closingIndent}${source.slice(close)}`;
161
- }
162
-
163
- const rootClose = source.lastIndexOf('}');
164
- if (rootClose === -1) throw new Error('Unbalanced package.json object');
165
- const rootBody = source.slice(0, rootClose);
166
- if (!multiline) {
167
- const separator = rootBody.trim().endsWith('{') ? '' : ',';
168
- return `${rootBody}${separator}"scripts":{${key}:${encoded}}${source.slice(rootClose)}`;
169
- }
170
- const trailing = rootBody.match(/\s*$/)?.[0] ?? '';
171
- const contentEnd = rootClose - trailing.length;
172
- const rootClosingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
173
- const separator = source.slice(0, contentEnd).trimEnd().endsWith('{') ? '' : ',';
174
- const addition = `${separator}${eol}${rootPropertyIndent}"scripts": {${eol}${rootPropertyIndent}${indentUnit}${key}: ${encoded}${eol}${rootPropertyIndent}}`;
175
- return `${source.slice(0, contentEnd)}${addition}${eol}${rootClosingIndent}${source.slice(rootClose)}`;
105
+ return addPackageJsonPropertyPreservingFormat(
106
+ source,
107
+ 'scripts',
108
+ scriptName,
109
+ scriptValue,
110
+ 'package.json scripts'
111
+ );
176
112
  }
177
113
 
178
114
  /**
@@ -6,7 +6,7 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
 
8
8
  export const CI_MERGE_BOUNDARY_REL = '.ark/ci-merge-boundary.json';
9
- export const CI_MERGE_BOUNDARY_SCHEMA = '1.0';
9
+ const CI_MERGE_BOUNDARY_SCHEMA = '1.0';
10
10
 
11
11
  /**
12
12
  * @param {{
@@ -8,7 +8,7 @@
8
8
  * Pure CLI helper (bin/lib/class-source-scan.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- export const MEMBER_MODIFIERS = new Set([
11
+ const MEMBER_MODIFIERS = new Set([
12
12
  'public',
13
13
  'private',
14
14
  'protected',
@@ -92,7 +92,7 @@ function skipRegexLiteral(src, index) {
92
92
  * Index after a string / comment / regex literal starting at `index`, or `index`
93
93
  * when none starts there.
94
94
  */
95
- export function skipStringOrComment(src, index) {
95
+ function skipStringOrComment(src, index) {
96
96
  const ch = src[index];
97
97
  if (ch === '/' && src[index + 1] === '/') {
98
98
  const nl = src.indexOf('\n', index);
@@ -119,7 +119,7 @@ export function skipStringOrComment(src, index) {
119
119
  }
120
120
  return index;
121
121
  }
122
- export function skipWsAndComments(src, index) {
122
+ function skipWsAndComments(src, index) {
123
123
  let i = index;
124
124
  while (i < src.length) {
125
125
  if (/\s/.test(src[i])) {
@@ -134,7 +134,7 @@ export function skipWsAndComments(src, index) {
134
134
  }
135
135
  return i;
136
136
  }
137
- export function readIdent(src, index) {
137
+ function readIdent(src, index) {
138
138
  const ch = src[index];
139
139
  if (!ch || !/[A-Za-z_$]/.test(ch))
140
140
  return null;
@@ -148,7 +148,7 @@ export function readIdent(src, index) {
148
148
  * never closes. Strings and comments are skipped. For `<`/`>`, an arrow `=>` is not
149
149
  * a closing angle.
150
150
  */
151
- export function skipBalanced(src, openIndex, openCh, closeCh) {
151
+ function skipBalanced(src, openIndex, openCh, closeCh) {
152
152
  if (src[openIndex] !== openCh)
153
153
  return null;
154
154
  let depth = 1;