arkgate 4.8.8 → 4.8.10

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 (65) hide show
  1. package/CHANGELOG.md +141 -3
  2. package/README.md +9 -6
  3. package/bin/ark-check-runtime.mjs +24 -2
  4. package/bin/ark-layer-match.mjs +25 -10
  5. package/bin/ark.mjs +18 -10
  6. package/bin/lib/agent-homes.mjs +1 -1
  7. package/bin/lib/analysis-engine.mjs +8 -8
  8. package/bin/lib/architecture-scan.mjs +91 -4
  9. package/bin/lib/ark-order-facts.mjs +11 -4
  10. package/bin/lib/ark-order-sensors.mjs +103 -3
  11. package/bin/lib/arkrule-file-hints.mjs +255 -20
  12. package/bin/lib/arkrules-sensors.mjs +364 -68
  13. package/bin/lib/baseline-key.mjs +45 -1
  14. package/bin/lib/config-contract.mjs +9 -3
  15. package/bin/lib/config-extras.mjs +1 -0
  16. package/bin/lib/contract-smells.mjs +12 -6
  17. package/bin/lib/diagnostic-catalog.mjs +1 -0
  18. package/bin/lib/doctor-human.mjs +35 -10
  19. package/bin/lib/doctor-next-actions.mjs +24 -3
  20. package/bin/lib/field-install.mjs +23 -2
  21. package/bin/lib/first-run-help.mjs +69 -5
  22. package/bin/lib/gate-files.mjs +108 -22
  23. package/bin/lib/managed-upgrade.mjs +9 -1
  24. package/bin/lib/resolved-candidate-facts.mjs +82 -1
  25. package/bin/lib/rules-inventory.mjs +7 -3
  26. package/bin/lib/upgrade-command.mjs +17 -4
  27. package/bin/lib/upstream-report.mjs +330 -0
  28. package/bin/lib/violations.mjs +51 -15
  29. package/dist/{configTypes-0eHpocR3.d.ts → configTypes-j7so8B4O.d.ts} +12 -0
  30. package/dist/{diagnosticCatalog-DxKCTBbp.d.ts → diagnosticCatalog-biferT4R.d.ts} +11 -5
  31. package/dist/eslint/index.cjs +5 -8
  32. package/dist/eslint/index.d.ts +6 -4
  33. package/dist/eslint/index.js +5 -8
  34. package/dist/index.cjs +30 -33
  35. package/dist/index.d.ts +18 -6
  36. package/dist/index.js +30 -33
  37. package/dist/nestjs/index.cjs +3 -3
  38. package/dist/nestjs/index.d.ts +3 -3
  39. package/dist/nestjs/index.js +2 -2
  40. package/dist/order/index.cjs +1 -1
  41. package/dist/order/index.d.ts +6 -2
  42. package/dist/order/index.js +1 -1
  43. package/dist/runtime/index.cjs +11 -11
  44. package/dist/runtime/index.d.ts +6 -6
  45. package/dist/runtime/index.js +11 -11
  46. package/dist/{types-BK47clMl.d.ts → types-Djbs3KjE.d.ts} +1 -1
  47. package/dist/{types-DxvmJO-D.d.ts → types-tGhZUiGX.d.ts} +1 -1
  48. package/docs/README.md +4 -3
  49. package/docs/agent-guide.md +27 -2
  50. package/docs/ai-gates.md +8 -0
  51. package/docs/arkorder.md +30 -7
  52. package/docs/brownfield-adoption.md +30 -0
  53. package/docs/configuration.md +61 -14
  54. package/docs/develop.md +4 -2
  55. package/docs/diagnostics.md +10 -0
  56. package/docs/package-surface.md +7 -5
  57. package/docs/use.md +11 -0
  58. package/package.json +1 -1
  59. package/schemas/ark.config.schema.json +12 -2
  60. package/server.json +2 -2
  61. package/templates/agent-skills/README.md +1 -1
  62. package/templates/agent-skills/ark-contract/SKILL.md +1 -1
  63. package/templates/agent-skills/ark-explore/SKILL.md +24 -3
  64. package/templates/skills/ark-contract.md +1 -1
  65. package/templates/skills/ark-explore.md +24 -3
@@ -8,6 +8,7 @@
8
8
  * Pure CLI helper (bin/lib/rules-inventory.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
+ import { DOMAIN_EVENTS_PUSH_RE, DOMAIN_INVARIANT_WORD_RE, expectedDomainInvariantWordsPhrase, isIdiomaticEventsReset, } from './arkrules-sensors.mjs';
11
12
  function lineOf(content, index) {
12
13
  return content.slice(0, index).split('\n').length;
13
14
  }
@@ -226,7 +227,7 @@ export function buildRulesInventory(input) {
226
227
  /(?:^|\/)[^/]*(?:-access)?\.error\./i.test(posix) ||
227
228
  /(?:^|\/)errors?(?:\/|$)/i.test(posix);
228
229
  if (!isErrorBag) {
229
- const mutRe = /this\.\w+\s*=/g;
230
+ const mutRe = /\bthis\.[A-Za-z_][A-Za-z0-9_]*\s*=(?!=)/g;
230
231
  let mut;
231
232
  while ((mut = mutRe.exec(content)) !== null) {
232
233
  const classStart = content.lastIndexOf('class ', mut.index);
@@ -239,15 +240,18 @@ export function buildRulesInventory(input) {
239
240
  if (/\bextends\s+(?:Error|[A-Za-z_$][A-Za-z0-9_$]*Error)\b/.test(classHeader)) {
240
241
  continue;
241
242
  }
243
+ if (isIdiomaticEventsReset(content, mut.index))
244
+ continue;
242
245
  const window = content.slice(Math.max(0, mut.index - 200), mut.index + 200);
243
- if (!/\b(ensureInvariants|assertInvariants|validate|publish|emit)\b/.test(window)) {
246
+ if (!DOMAIN_INVARIANT_WORD_RE.test(window) &&
247
+ !DOMAIN_EVENTS_PUSH_RE.test(window)) {
244
248
  seq += 1;
245
249
  candidates.push({
246
250
  id: `inv-mut-${seq}`,
247
251
  kind: 'mutation-without-guard',
248
252
  file,
249
253
  line: lineOf(content, mut.index),
250
- message: 'Domain field mutation without nearby guard/publish call.',
254
+ message: `Domain field mutation without nearby ${expectedDomainInvariantWordsPhrase()}.`,
251
255
  confidence: 'heuristic',
252
256
  governedLayer,
253
257
  suggestedArkRule: {
@@ -603,10 +603,23 @@ export function runUpgradeCommand(args, dependencies) {
603
603
  return 2;
604
604
  }
605
605
  if (applied.blocked) {
606
- if (args.json) console.log(JSON.stringify(applied, null, 2));
607
- else renderManagedUpgrade(applied, {
608
- next: 'Preview again with --accept-conflicts, then use that preview\'s exact next command.',
609
- });
606
+ const command = buildUpgradeNextCommand(
607
+ { ...args, acceptConflicts: true },
608
+ applied.planDigest
609
+ );
610
+ if (args.json) {
611
+ console.log(
612
+ managedUpgradeJson(applied, {
613
+ blocked: true,
614
+ reasonCode: applied.reasonCode ?? 'managed-consent-required',
615
+ nextCommand: command,
616
+ })
617
+ );
618
+ } else {
619
+ renderManagedUpgrade(applied, {
620
+ next: 'Preview again with --accept-conflicts, then use that preview\'s exact next command.',
621
+ });
622
+ }
610
623
  return 1;
611
624
  }
612
625
  if (applied.nothingToApply && !applied.applied) {
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Upstream GitHub issue draft for ArkGate itself (`arkgate report` / `ark report`).
3
+ * Target is this package's package.json bugs.url (pedroknigge/arkgate), never the consumer repo.
4
+ */
5
+ import { spawnSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import readline from 'node:readline/promises';
9
+
10
+ import { __packageRoot } from './gate-files.mjs';
11
+
12
+ export const UPSTREAM_OWNER_REPO = 'pedroknigge/arkgate';
13
+ export const SUBMIT_PROMPT = 'Type submit to send';
14
+
15
+ export function ownerRepoFromGithubUrl(url) {
16
+ if (typeof url !== 'string' || url.trim() === '') return null;
17
+ const match = url.trim().match(/github\.com[:/]+([^/]+)\/([^/#?\s]+)/i);
18
+ if (!match) return null;
19
+ const owner = match[1];
20
+ const repo = match[2].replace(/\.git$/i, '');
21
+ if (!owner || !repo) return null;
22
+ return `${owner}/${repo}`;
23
+ }
24
+
25
+ export function resolveUpstreamRepo(pkg) {
26
+ const fromBugs = ownerRepoFromGithubUrl(pkg?.bugs?.url);
27
+ if (fromBugs) return fromBugs;
28
+ const repository = pkg?.repository;
29
+ const repoUrl = typeof repository === 'string' ? repository : repository?.url;
30
+ const fromRepo = ownerRepoFromGithubUrl(repoUrl);
31
+ if (fromRepo) return fromRepo;
32
+ return UPSTREAM_OWNER_REPO;
33
+ }
34
+
35
+ export function posixSingleQuote(value) {
36
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
37
+ }
38
+
39
+ export function formatGhIssueCreateCommand({ repo, title, body }) {
40
+ return `gh issue create --repo ${posixSingleQuote(repo)} --title ${posixSingleQuote(title)} --body ${posixSingleQuote(body)}`;
41
+ }
42
+
43
+ export function reportUsage() {
44
+ return `arkgate report (alias ark report) — draft an upstream GitHub issue for ArkGate.
45
+
46
+ Target: pedroknigge/arkgate (this package's package.json bugs.url). Never the consumer repo.
47
+
48
+ Usage:
49
+ arkgate report [--root <project>] [--json] [--title <text>] [--finding <ref>]
50
+ arkgate report --submit --i-confirm-submit
51
+ arkgate report --submit # TTY: type submit to send
52
+
53
+ Default prints a draft (arkgate version + last-check snippet). Nothing is created.
54
+ Create only with --submit AND (--i-confirm-submit after the human said yes, or TTY
55
+ "${SUBMIT_PROMPT}"). --yes does not submit.
56
+
57
+ If gh is missing or not logged in: prints the draft and the exact
58
+ gh issue create --repo pedroknigge/arkgate command, then exits 2.
59
+ `;
60
+ }
61
+
62
+ export function lastCheckSnippet(latest) {
63
+ if (!latest || typeof latest !== 'object') return null;
64
+ const at =
65
+ typeof latest.generatedAt === 'string'
66
+ ? latest.generatedAt
67
+ : typeof latest.at === 'string'
68
+ ? latest.at
69
+ : null;
70
+ const active =
71
+ typeof latest.activeViolations === 'number'
72
+ ? latest.activeViolations
73
+ : typeof latest.violations?.active === 'number'
74
+ ? latest.violations.active
75
+ : null;
76
+ let verdict = null;
77
+ if (latest.ok === true && (active == null || active === 0)) verdict = 'pass';
78
+ else if (latest.ok === false || (typeof active === 'number' && active > 0)) verdict = 'fail';
79
+ else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
80
+ verdict = 'incomplete';
81
+ } else if (latest.ok === true) verdict = 'pass';
82
+ if (at == null && verdict == null && active == null) return null;
83
+ return { at, verdict, activeViolations: active };
84
+ }
85
+
86
+ export function readLastCheckSnapshot(root) {
87
+ const latestPath = path.join(root, '.ark', 'reports', 'latest.json');
88
+ try {
89
+ return JSON.parse(fs.readFileSync(latestPath, 'utf8'));
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ export function buildIssueDraft({ repo, arkgateVersion, lastCheck, finding, title }) {
96
+ const resolvedTitle =
97
+ typeof title === 'string' && title.trim()
98
+ ? title.trim()
99
+ : finding
100
+ ? `ArkGate finding ${finding}`
101
+ : 'ArkGate field report';
102
+ const lines = [`## ArkGate version`, String(arkgateVersion ?? 'unknown'), ''];
103
+ if (finding) {
104
+ lines.push('## Finding', String(finding), '');
105
+ }
106
+ if (lastCheck) {
107
+ lines.push('## Last check');
108
+ lines.push(`- at: ${lastCheck.at ?? 'none'}`);
109
+ lines.push(`- verdict: ${lastCheck.verdict ?? 'none'}`);
110
+ lines.push(`- activeViolations: ${lastCheck.activeViolations ?? 'unknown'}`);
111
+ lines.push('');
112
+ } else {
113
+ lines.push('## Last check', 'No last-check snapshot under .ark/reports/latest.json.', '');
114
+ }
115
+ lines.push(
116
+ '## Repro',
117
+ '```bash',
118
+ 'npx arkgate-check --doctor',
119
+ 'npx arkgate status --json',
120
+ '```',
121
+ '',
122
+ '## What happened',
123
+ '(ArkGate bug, false green, false red, missing doc, or improvable behavior in ArkGate itself — not leftover design in the consumer app.)',
124
+ '',
125
+ `Prepared with \`arkgate report\` against upstream ${repo}. Not the consumer repo.`
126
+ );
127
+ return { repo, title: resolvedTitle, body: lines.join('\n') };
128
+ }
129
+
130
+ export function defaultRunGh(argv, options = {}) {
131
+ const env = { ...(options.env ?? process.env) };
132
+ delete env.GH_REPO;
133
+ const result = spawnSync('gh', argv, {
134
+ encoding: 'utf8',
135
+ env,
136
+ stdio: ['ignore', 'pipe', 'pipe'],
137
+ });
138
+ if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {
139
+ return { missing: true, status: 127, stdout: '', stderr: result.error.message };
140
+ }
141
+ return {
142
+ missing: false,
143
+ status: result.status ?? 1,
144
+ stdout: result.stdout ?? '',
145
+ stderr: result.stderr ?? '',
146
+ };
147
+ }
148
+
149
+ function loadArkgatePackageJson(explicit) {
150
+ if (explicit && typeof explicit === 'object') return explicit;
151
+ try {
152
+ return JSON.parse(fs.readFileSync(path.join(__packageRoot, 'package.json'), 'utf8'));
153
+ } catch {
154
+ return { bugs: { url: `https://github.com/${UPSTREAM_OWNER_REPO}/issues` } };
155
+ }
156
+ }
157
+
158
+ function packageVersion(pkg, fallback) {
159
+ if (typeof fallback === 'string' && fallback.trim()) return fallback;
160
+ return typeof pkg?.version === 'string' ? pkg.version : 'unknown';
161
+ }
162
+
163
+ function renderHumanDraft(draft, arkgateVersion) {
164
+ return [
165
+ `Upstream: ${draft.repo}`,
166
+ '(never the consumer repo)',
167
+ '',
168
+ `ArkGate version: ${arkgateVersion}`,
169
+ '',
170
+ `Title: ${draft.title}`,
171
+ '',
172
+ draft.body,
173
+ ].join('\n');
174
+ }
175
+
176
+ async function defaultPromptSubmit(stdin, stdout) {
177
+ const rl = readline.createInterface({ input: stdin, output: stdout });
178
+ try {
179
+ return await rl.question(`${SUBMIT_PROMPT}\n`);
180
+ } finally {
181
+ rl.close();
182
+ }
183
+ }
184
+
185
+ export function parseReportArgv(argv = []) {
186
+ const out = {
187
+ submit: false,
188
+ iConfirmSubmit: false,
189
+ json: false,
190
+ yes: false,
191
+ help: false,
192
+ finding: undefined,
193
+ title: undefined,
194
+ root: undefined,
195
+ };
196
+ for (let i = 0; i < argv.length; i += 1) {
197
+ const arg = argv[i];
198
+ const next = () => {
199
+ const value = argv[i + 1];
200
+ if (value === undefined || String(value).startsWith('-')) {
201
+ throw new Error(`Missing value for ${arg}. Run arkgate report --help for usage.`);
202
+ }
203
+ i += 1;
204
+ return value;
205
+ };
206
+ if (arg === '--submit') out.submit = true;
207
+ else if (arg === '--i-confirm-submit') out.iConfirmSubmit = true;
208
+ else if (arg === '--json') out.json = true;
209
+ else if (arg === '--yes' || arg === '-y') out.yes = true;
210
+ else if (arg === '--help' || arg === '-h' || arg === 'help') out.help = true;
211
+ else if (arg === '--finding') out.finding = next();
212
+ else if (arg === '--title') out.title = next();
213
+ else if (arg === '--root') out.root = path.resolve(next());
214
+ else throw new Error(`Unknown argument: ${arg}. Run arkgate report --help for usage.`);
215
+ }
216
+ return out;
217
+ }
218
+
219
+ /**
220
+ * @param {object} options
221
+ * @returns {Promise<number>} process exit code
222
+ */
223
+ export async function runUpstreamReportCommand(options = {}) {
224
+ const fromArgv = Array.isArray(options.argv) ? parseReportArgv(options.argv) : {};
225
+ if (fromArgv.help) {
226
+ const writeOut = options.writeOut ?? ((text) => console.log(text));
227
+ writeOut(reportUsage());
228
+ return 0;
229
+ }
230
+ const root = path.resolve(fromArgv.root ?? options.root ?? process.cwd());
231
+ const json = Boolean(fromArgv.json || options.json);
232
+ const submit = Boolean(fromArgv.submit || options.submit);
233
+ const iConfirmSubmit = Boolean(fromArgv.iConfirmSubmit || options.iConfirmSubmit);
234
+ const yes = Boolean(fromArgv.yes || options.yes);
235
+ const finding = fromArgv.finding ?? options.finding;
236
+ const title = fromArgv.title ?? options.title;
237
+ const stdin = options.stdin ?? process.stdin;
238
+ const stdout = options.stdout ?? process.stdout;
239
+ const stdinIsTTY = options.stdinIsTTY ?? Boolean(stdin.isTTY);
240
+ const env = options.env ?? process.env;
241
+ const runGh = options.runGh ?? ((argv) => defaultRunGh(argv, { env }));
242
+ const writeOut = options.writeOut ?? ((text) => console.log(text));
243
+ const writeErr = options.writeErr ?? ((text) => console.error(text));
244
+
245
+ const pkg = loadArkgatePackageJson(options.arkgatePackageJson);
246
+ const repo = resolveUpstreamRepo(pkg);
247
+ const arkgateVersion = packageVersion(pkg, options.arkgateVersion);
248
+ const lastCheck = lastCheckSnippet(options.latestSnapshot ?? readLastCheckSnapshot(root));
249
+ const draft = buildIssueDraft({
250
+ repo,
251
+ arkgateVersion,
252
+ lastCheck,
253
+ finding,
254
+ title,
255
+ });
256
+ const ghCommand = formatGhIssueCreateCommand(draft);
257
+ const human = renderHumanDraft(draft, arkgateVersion);
258
+
259
+ const payload = {
260
+ schemaVersion: '1.0',
261
+ command: 'report',
262
+ created: false,
263
+ submitted: false,
264
+ repo: draft.repo,
265
+ title: draft.title,
266
+ body: draft.body,
267
+ arkgateVersion,
268
+ lastCheck,
269
+ ghCommand,
270
+ yesDoesNotSubmit: true,
271
+ };
272
+
273
+ const emit = (extraHuman, extraPayload) => {
274
+ if (json) writeOut(JSON.stringify({ ...payload, ...extraPayload }, null, 2));
275
+ else writeOut(extraHuman ? `${human}\n\n${extraHuman}` : human);
276
+ };
277
+
278
+ if (!submit) {
279
+ emit('Draft only. Nothing was created.\nAfter a human confirms: arkgate report --submit --i-confirm-submit\n--yes does not submit.');
280
+ return 0;
281
+ }
282
+
283
+ // --yes never confirms. Confirm is --i-confirm-submit or TTY "submit".
284
+ let confirmed = iConfirmSubmit;
285
+ if (!confirmed && stdinIsTTY) {
286
+ const typed = options.promptSubmit
287
+ ? await options.promptSubmit()
288
+ : await defaultPromptSubmit(stdin, stdout);
289
+ confirmed = /^\s*submit\s*$/i.test(String(typed ?? ''));
290
+ }
291
+
292
+ if (!confirmed) {
293
+ const refused =
294
+ 'Refused: --submit requires --i-confirm-submit or typing submit on a TTY. --yes does not submit.';
295
+ if (json) {
296
+ writeOut(JSON.stringify({ ...payload, error: 'submit-confirm-required', yesDoesNotSubmit: true }, null, 2));
297
+ } else {
298
+ writeOut(`${human}\n\nDraft only. ${refused}`);
299
+ }
300
+ writeErr(refused);
301
+ return 2;
302
+ }
303
+
304
+ const auth = runGh(['auth', 'status']);
305
+ const ghUnavailable = Boolean(auth?.missing) || auth?.status !== 0;
306
+ if (ghUnavailable) {
307
+ const missing =
308
+ 'Not filed: gh is missing or not logged in.\nExact command:\n' + ghCommand;
309
+ emit(missing, { created: false, submitted: false, error: 'gh-unavailable', ghCommand });
310
+ if (!json) writeErr('Not filed: gh is missing or not logged in.');
311
+ return 2;
312
+ }
313
+
314
+ const created = runGh(['issue', 'create', '--repo', repo, '--title', draft.title, '--body', draft.body]);
315
+ if (created?.missing || created?.status !== 0) {
316
+ const missing =
317
+ 'Not filed: gh is missing or not logged in.\nExact command:\n' + ghCommand;
318
+ emit(missing, { created: false, submitted: false, error: 'gh-unavailable', ghCommand });
319
+ if (!json) writeErr('Not filed: gh is missing or not logged in.');
320
+ return 2;
321
+ }
322
+
323
+ const url = String(created.stdout ?? '').trim();
324
+ emit(url ? `Created: ${url}` : 'Created.', {
325
+ created: true,
326
+ submitted: true,
327
+ url: url || null,
328
+ });
329
+ return 0;
330
+ }
@@ -16,8 +16,12 @@ import { baselineKeysFromDocument, baselineRecordsDocument } from './team-parlia
16
16
  import { toAdapterDiagnostic } from './adapter-contract.mjs';
17
17
  export { baselineKey, baselineOccurrenceKeys };
18
18
 
19
- const BASELINE_NOTE =
20
- 'Frozen ark-check violations (one record per edge). Only NEW keys vs the merge-base fail --against / --baseline. Regenerate with: ark-check --update-baseline';
19
+ /** Real regenerate flags: freeze-refuse needs --force; law PR needs --contract-session; stewards[] needs --author. */
20
+ export const REGENERATE_BASELINE_COMMAND =
21
+ 'ark-check --update-baseline --force --contract-session --author <steward>';
22
+
23
+ export const BASELINE_NOTE =
24
+ `Frozen ark-check violations (one record per edge). Only NEW keys vs the merge-base fail --against / --baseline. Regenerate with: ${REGENERATE_BASELINE_COMMAND} (--author when stewards[] is set)`;
21
25
 
22
26
  export function readBaseline(root, baselinePath) {
23
27
  const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
@@ -76,13 +80,34 @@ export function printViolation(violation) {
76
80
  export const CONCENTRATION_MIN_VIOLATIONS = 10;
77
81
  export const CONCENTRATION_SHARE = 0.9;
78
82
 
83
+ /** Sensor id for STRUCTURE findings. Orchestration-only is code debt, not a missing toLayer. */
84
+ export function violationSensorId(violation) {
85
+ if (typeof violation?.sensor === 'string' && violation.sensor.trim()) return violation.sensor.trim();
86
+ if (typeof violation?.arkruleId === 'string' && violation.arkruleId.trim()) return violation.arkruleId.trim();
87
+ return undefined;
88
+ }
89
+
79
90
  export function violationEdge(violation) {
80
91
  if (violation.ruleId === 'CIRCULAR_DEPENDENCY') return 'circular dependency';
81
92
  if (violation.ruleId === 'FORBIDDEN_GLOBAL') return `${violation.fromLayer ?? '?'} → ambient global`;
82
93
  if (violation.fromLayer && violation.toLayer) return `${violation.fromLayer} → ${violation.toLayer}`;
94
+ const sensor = violationSensorId(violation);
95
+ if (sensor) return sensor;
83
96
  return violation.ruleId;
84
97
  }
85
98
 
99
+ /** Layer-import arrows can mean a contract bug. Structural-sensor blobs are code debt. */
100
+ export function isContractConcentrationEdge(edge) {
101
+ return typeof edge === 'string' && edge.includes(' → ');
102
+ }
103
+
104
+ export function isContractStyleViolation(violation) {
105
+ if (violationSensorId(violation)) return false;
106
+ if (violation.ruleId === 'CIRCULAR_DEPENDENCY') return true;
107
+ if (violation.ruleId === 'FORBIDDEN_GLOBAL') return true;
108
+ return Boolean(violation.fromLayer && violation.toLayer);
109
+ }
110
+
86
111
  // The directory the offending import lands in — the signal for "where does this edge go?".
87
112
  // For a LAYER_IMPORT_VIOLATION the target is a resolved file path; cluster by its dir prefix
88
113
  // so `kernel/internal/x` and `kernel/internal/y` collapse to one "into kernel/internal/".
@@ -100,27 +125,35 @@ export function summarizeViolations(violations) {
100
125
  for (const violation of violations) {
101
126
  if (violation.typeOnly) typeOnly += 1;
102
127
  const key = violationEdge(violation);
103
- const entry = byEdge.get(key) ?? { edge: key, count: 0, typeOnly: 0, targets: new Map() };
128
+ const entry = byEdge.get(key) ?? {
129
+ edge: key,
130
+ count: 0,
131
+ typeOnly: 0,
132
+ targets: new Map(),
133
+ contractStyle: true,
134
+ };
104
135
  entry.count += 1;
105
136
  if (violation.typeOnly) entry.typeOnly += 1;
137
+ if (!isContractStyleViolation(violation)) entry.contractStyle = false;
106
138
  const subtree = violationTargetSubtree(violation);
107
139
  if (subtree) entry.targets.set(subtree, (entry.targets.get(subtree) ?? 0) + 1);
108
140
  byEdge.set(key, entry);
109
141
  }
110
- const edges = [...byEdge.values()]
111
- .map((entry) => ({
112
- edge: entry.edge,
113
- count: entry.count,
114
- typeOnly: entry.typeOnly,
115
- topTargets: [...entry.targets.entries()]
116
- .sort((a, b) => b[1] - a[1])
117
- .slice(0, 4)
118
- .map(([dir, count]) => ({ dir, count })),
119
- }))
120
- .sort((a, b) => b.count - a.count);
142
+ const ranked = [...byEdge.values()].sort((a, b) => b.count - a.count);
143
+ const edges = ranked.map((entry) => ({
144
+ edge: entry.edge,
145
+ count: entry.count,
146
+ typeOnly: entry.typeOnly,
147
+ topTargets: [...entry.targets.entries()]
148
+ .sort((a, b) => b[1] - a[1])
149
+ .slice(0, 4)
150
+ .map(([dir, count]) => ({ dir, count })),
151
+ }));
121
152
  const total = violations.length;
122
153
  const dominant = edges[0];
123
154
  const dominantShare = total > 0 && dominant ? dominant.count / total : 0;
155
+ const contractConcentration =
156
+ Boolean(ranked[0]?.contractStyle) && isContractConcentrationEdge(ranked[0]?.edge);
124
157
  return {
125
158
  total,
126
159
  // Value edges are real runtime coupling; type-only edges (erased at compile time) are
@@ -130,7 +163,10 @@ export function summarizeViolations(violations) {
130
163
  edges,
131
164
  dominant: dominant ? dominant.edge : undefined,
132
165
  dominantShare,
133
- concentrated: total >= CONCENTRATION_MIN_VIOLATIONS && dominantShare >= CONCENTRATION_SHARE,
166
+ concentrated:
167
+ total >= CONCENTRATION_MIN_VIOLATIONS &&
168
+ dominantShare >= CONCENTRATION_SHARE &&
169
+ contractConcentration,
134
170
  };
135
171
  }
136
172
 
@@ -105,12 +105,24 @@ type ArkConfigArkOrder = {
105
105
  mode: ArkConfigArkOrderMode;
106
106
  planeRoots: string[];
107
107
  managedLayers: string[];
108
+ /**
109
+ * Haken cap on one `release()` / `assertXiKeyCap` (default 7).
110
+ * Not a cap on the `xiKeys` watchlist length.
111
+ */
108
112
  maxXiKeys: number;
109
113
  /**
110
114
  * Slow product keys the team can already name (plan, cost code, protocol).
111
115
  * Optional. Empty → `ARKORDER_XI_FIELD_WRITE` stays silent.
116
+ * Repo-wide watchlist — not compared to `maxXiKeys`.
112
117
  */
113
118
  xiKeys: string[];
119
+ /**
120
+ * Optional globs that narrow ξ field-write observation inside `managedLayers`.
121
+ * Same glob engine as `layers[].patterns`. Absence or empty → every file in
122
+ * those layers. Non-empty → emit only when the layer is managed AND the file
123
+ * matches at least one glob.
124
+ */
125
+ appliesTo?: string[];
114
126
  };
115
127
  type ArkConfig = {
116
128
  $schema: string;
@@ -1,5 +1,5 @@
1
- import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-BK47clMl.js';
2
- import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-0eHpocR3.js';
1
+ import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-Djbs3KjE.js';
2
+ import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-j7so8B4O.js';
3
3
 
4
4
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
5
5
  /**
@@ -409,7 +409,7 @@ declare const ARK_ANALYSIS_RESULT_SCHEMA: {
409
409
  };
410
410
 
411
411
  /** ArkGate library version — single source of truth. */
412
- declare const version = "4.8.8";
412
+ declare const version = "4.8.10";
413
413
 
414
414
  /**
415
415
  * AI Code Gate (basic).
@@ -1924,12 +1924,18 @@ declare function evaluateArkRuleSensors(input: EvaluateArkRuleSensorsInput): Ark
1924
1924
  * Rules without appliesTo (whole-layer) never emit this signal.
1925
1925
  */
1926
1926
  declare function collectEmptyAppliesToFindings(arkRules: EffectiveArkRules, files: readonly string[]): ArkRuleSensorViolation[];
1927
+ /** Optional resolved-import facts when the collector already classified the specifier. */
1928
+ type ResolvedPersistenceImportFact = {
1929
+ specifier?: string;
1930
+ resolvedFile?: string;
1931
+ layer?: string | null;
1932
+ };
1927
1933
  /**
1928
1934
  * Pure Tooling/Domain heuristic for orchestration-only / thin-adapter fileHints.
1929
1935
  * Prefers false negatives over false positives (ADR 0013 discipline).
1930
1936
  * Returns null when neither flag is set (callers may omit the path).
1931
1937
  */
1932
- declare function deriveArkRuleFileHints(_file: string, content: string): {
1938
+ declare function deriveArkRuleFileHints(_file: string, content: string, resolvedImports?: readonly ResolvedPersistenceImportFact[]): {
1933
1939
  orchestrationHeavy?: boolean;
1934
1940
  adapterThick?: boolean;
1935
1941
  persistenceWrite?: boolean;
@@ -1937,7 +1943,7 @@ declare function deriveArkRuleFileHints(_file: string, content: string): {
1937
1943
  /**
1938
1944
  * Build fileHints map from path→content. Omits paths with no flags (sparse map).
1939
1945
  */
1940
- declare function buildArkRuleFileHints(fileContents: Readonly<Record<string, string>>): Record<string, {
1946
+ declare function buildArkRuleFileHints(fileContents: Readonly<Record<string, string>>, resolvedImportsByFile?: Readonly<Record<string, readonly ResolvedPersistenceImportFact[]>>): Record<string, {
1941
1947
  orchestrationHeavy?: boolean;
1942
1948
  adapterThick?: boolean;
1943
1949
  persistenceWrite?: boolean;