arkgate 2.13.0 → 3.0.0

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 (72) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +37 -22
  3. package/bin/ark-check.mjs +62 -4
  4. package/bin/ark-mcp.mjs +108 -1
  5. package/bin/ark-shared.mjs +204 -149
  6. package/bin/ark.mjs +90 -25
  7. package/bin/lib/adapter-contract.mjs +93 -0
  8. package/bin/lib/agent-gates.mjs +1 -0
  9. package/bin/lib/analysis-engine.mjs +1171 -0
  10. package/bin/lib/architecture-scan.mjs +84 -135
  11. package/bin/lib/ci-and-commands.mjs +31 -0
  12. package/bin/lib/config-warnings.mjs +7 -205
  13. package/bin/lib/field-install.mjs +67 -10
  14. package/bin/lib/gate-files.mjs +42 -3
  15. package/bin/lib/graph-cycles.mjs +4 -54
  16. package/bin/lib/hook-templates.mjs +33 -1
  17. package/bin/lib/host-support-matrix.mjs +7 -1
  18. package/bin/lib/install-migrate.mjs +54 -16
  19. package/bin/lib/presets.mjs +42 -2
  20. package/bin/lib/safety-diagnostics.mjs +18 -17
  21. package/bin/lib/scan-files.mjs +12 -1
  22. package/bin/lib/skill-install.mjs +8 -1
  23. package/bin/lib/source-policy.mjs +36 -0
  24. package/bin/lib/start-preview.mjs +271 -0
  25. package/bin/lib/ts-resolve.mjs +11 -2
  26. package/bin/lib/write-path-capabilities.mjs +4 -0
  27. package/compat/nestjs.cjs +2 -0
  28. package/compat/nestjs.d.ts +2 -0
  29. package/compat/nestjs.js +1 -0
  30. package/compat/runtime.cjs +2 -0
  31. package/compat/runtime.d.ts +2 -0
  32. package/compat/runtime.js +1 -0
  33. package/dist/configContract-BxSIwVRo.d.cts +259 -0
  34. package/dist/configContract-BxSIwVRo.d.ts +259 -0
  35. package/dist/eslint/index.cjs +125 -48
  36. package/dist/eslint/index.d.cts +7 -1
  37. package/dist/eslint/index.d.ts +7 -1
  38. package/dist/eslint/index.js +125 -48
  39. package/dist/index.cjs +1248 -3302
  40. package/dist/index.d.cts +359 -483
  41. package/dist/index.d.ts +359 -483
  42. package/dist/index.js +1231 -3248
  43. package/docs/agent-guide.md +28 -16
  44. package/docs/ai-gates.md +30 -7
  45. package/docs/migrate-from-ark-runtime-kernel.md +2 -3
  46. package/docs/package-surface.md +8 -13
  47. package/docs/production-hardening.md +17 -4
  48. package/docs/typescript-support.md +27 -0
  49. package/package.json +33 -11
  50. package/schemas/ark.analysis-result.schema.json +91 -0
  51. package/server.json +2 -2
  52. package/templates/skills/ark-architect.md +3 -2
  53. package/dist/configContract-iBLxx5Tz.d.cts +0 -53
  54. package/dist/configContract-iBLxx5Tz.d.ts +0 -53
  55. package/dist/eslint/index.cjs.map +0 -1
  56. package/dist/eslint/index.js.map +0 -1
  57. package/dist/index.cjs.map +0 -1
  58. package/dist/index.js.map +0 -1
  59. package/dist/nestjs/index.cjs +0 -2606
  60. package/dist/nestjs/index.cjs.map +0 -1
  61. package/dist/nestjs/index.d.cts +0 -23
  62. package/dist/nestjs/index.d.ts +0 -23
  63. package/dist/nestjs/index.js +0 -2582
  64. package/dist/nestjs/index.js.map +0 -1
  65. package/dist/runtime/index.cjs +0 -4014
  66. package/dist/runtime/index.cjs.map +0 -1
  67. package/dist/runtime/index.d.cts +0 -3
  68. package/dist/runtime/index.d.ts +0 -3
  69. package/dist/runtime/index.js +0 -3925
  70. package/dist/runtime/index.js.map +0 -1
  71. package/dist/types-BxBwnBpC.d.cts +0 -1041
  72. package/dist/types-Wcs_l1_J.d.ts +0 -1041
@@ -21,6 +21,68 @@ function arkPackageVersion() {
21
21
  }
22
22
  }
23
23
 
24
+ function matchingBrace(text, openIndex) {
25
+ let depth = 0;
26
+ let quoted = false;
27
+ let escaped = false;
28
+ for (let index = openIndex; index < text.length; index += 1) {
29
+ const char = text[index];
30
+ if (quoted) {
31
+ if (escaped) escaped = false;
32
+ else if (char === '\\') escaped = true;
33
+ else if (char === '"') quoted = false;
34
+ continue;
35
+ }
36
+ if (char === '"') quoted = true;
37
+ else if (char === '{') depth += 1;
38
+ else if (char === '}' && --depth === 0) return index;
39
+ }
40
+ return -1;
41
+ }
42
+
43
+ function addDevDependencyPreservingFormat(source, version) {
44
+ const multiline = /\r?\n/.test(source);
45
+ const eol = source.includes('\r\n') ? '\r\n' : '\n';
46
+ const rootPropertyIndent = source.match(/\r?\n([ \t]+)"[^"\n]+"\s*:/)?.[1] ?? ' ';
47
+ const indentUnit = rootPropertyIndent;
48
+ const encoded = JSON.stringify(version);
49
+ const devMatch = /"devDependencies"\s*:\s*\{/.exec(source);
50
+
51
+ if (devMatch) {
52
+ const open = source.indexOf('{', devMatch.index);
53
+ const close = matchingBrace(source, open);
54
+ if (close === -1) throw new Error('Unbalanced devDependencies object');
55
+ const body = source.slice(open + 1, close);
56
+ if (!multiline) {
57
+ const addition = body.trim() ? `,"arkgate":${encoded}` : `"arkgate":${encoded}`;
58
+ return `${source.slice(0, close)}${addition}${source.slice(close)}`;
59
+ }
60
+ const beforeClose = source.slice(0, close);
61
+ const trailing = beforeClose.match(/\s*$/)?.[0] ?? '';
62
+ const contentEnd = close - trailing.length;
63
+ const closingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
64
+ const propertyIndent = `${closingIndent}${indentUnit}`;
65
+ const addition = body.trim()
66
+ ? `,${eol}${propertyIndent}"arkgate": ${encoded}`
67
+ : `${propertyIndent}"arkgate": ${encoded}`;
68
+ return `${source.slice(0, contentEnd)}${addition}${eol}${closingIndent}${source.slice(close)}`;
69
+ }
70
+
71
+ const rootClose = source.lastIndexOf('}');
72
+ if (rootClose === -1) throw new Error('Unbalanced package.json object');
73
+ const rootBody = source.slice(0, rootClose);
74
+ if (!multiline) {
75
+ const separator = rootBody.trim().endsWith('{') ? '' : ',';
76
+ return `${rootBody}${separator}"devDependencies":{"arkgate":${encoded}}${source.slice(rootClose)}`;
77
+ }
78
+ const trailing = rootBody.match(/\s*$/)?.[0] ?? '';
79
+ const contentEnd = rootClose - trailing.length;
80
+ const rootClosingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
81
+ const separator = source.slice(0, contentEnd).trimEnd().endsWith('{') ? '' : ',';
82
+ const addition = `${separator}${eol}${rootPropertyIndent}"devDependencies": {${eol}${rootPropertyIndent}${indentUnit}"arkgate": ${encoded}${eol}${rootPropertyIndent}}`;
83
+ return `${source.slice(0, contentEnd)}${addition}${eol}${rootClosingIndent}${source.slice(rootClose)}`;
84
+ }
85
+
24
86
  /**
25
87
  * Ensure a check command string includes `--baseline <file>`.
26
88
  * Only touches strings that already invoke ark-check / arkgate-check.
@@ -150,16 +212,15 @@ export function pinArkgateDevDependency(root, opts = {}) {
150
212
  return { changed: false, reason: 'no-package-json' };
151
213
  }
152
214
  let pkg;
215
+ let source;
153
216
  try {
154
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
217
+ source = fs.readFileSync(pkgPath, 'utf8');
218
+ pkg = JSON.parse(source);
155
219
  } catch {
156
220
  return { changed: false, reason: 'unreadable-package-json' };
157
221
  }
158
222
  const deps = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {};
159
- const dev =
160
- pkg.devDependencies && typeof pkg.devDependencies === 'object'
161
- ? { ...pkg.devDependencies }
162
- : {};
223
+ const dev = pkg.devDependencies && typeof pkg.devDependencies === 'object' ? pkg.devDependencies : {};
163
224
  if (typeof deps.arkgate === 'string' || typeof dev.arkgate === 'string') {
164
225
  return {
165
226
  changed: false,
@@ -174,12 +235,8 @@ export function pinArkgateDevDependency(root, opts = {}) {
174
235
  : shipped
175
236
  ? `^${shipped}`
176
237
  : 'latest';
177
- dev.arkgate = version;
178
238
  if (opts.write !== false) {
179
- fs.writeFileSync(
180
- pkgPath,
181
- `${JSON.stringify({ ...pkg, devDependencies: dev }, null, 2)}\n`
182
- );
239
+ fs.writeFileSync(pkgPath, addDevDependencyPreservingFormat(source, version));
183
240
  }
184
241
  return { changed: true, reason: 'added', version };
185
242
  }
@@ -119,6 +119,40 @@ export const REQUIRED_GATE_FILES = [
119
119
  '.mcp.json',
120
120
  ];
121
121
  const REQUIRED_GATE_WORKFLOW = '.github/workflows/*.yml running ark-check';
122
+ const COMPACT_ROUTER = /<!--\s*arkgate:compact-router host=([a-z]+)\s*-->/;
123
+
124
+ const COMPACT_HOST_FILES = {
125
+ claude: ['.claude/settings.json'],
126
+ grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json'],
127
+ cursor: ['.cursor/mcp.json'],
128
+ codex: ['.codex/hooks.json'],
129
+ windsurf: ['.windsurf/rules/ark.md'],
130
+ cline: ['.clinerules/ark.md'],
131
+ copilot: ['.github/copilot-instructions.md'],
132
+ kiro: ['.kiro/steering/ark.md'],
133
+ roo: ['.roo/rules/ark.md'],
134
+ continue: ['.continue/rules/ark.md'],
135
+ gemini: ['GEMINI.md'],
136
+ };
137
+
138
+ export function compactRouterHost(root) {
139
+ try {
140
+ const agents = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
141
+ return agents.match(COMPACT_ROUTER)?.[1] ?? null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ export function isCompactRouterAgentsContent(text) {
148
+ return typeof text === 'string' && COMPACT_ROUTER.test(text);
149
+ }
150
+
151
+ function hasCompactHostRegistration(root, host) {
152
+ if (host === 'none') return fs.existsSync(path.join(root, '.mcp.json'));
153
+ const files = COMPACT_HOST_FILES[host];
154
+ return Boolean(files) && files.every((relativePath) => fs.existsSync(path.join(root, relativePath)));
155
+ }
122
156
 
123
157
  export function hasArkWorkflow(root) {
124
158
  const workflowsDir = path.join(root, '.github', 'workflows');
@@ -141,9 +175,14 @@ export function hasArkWorkflow(root) {
141
175
  }
142
176
 
143
177
  export function missingGates(root) {
144
- const missing = REQUIRED_GATE_FILES.filter(
145
- (relativePath) => !fs.existsSync(path.join(root, relativePath))
146
- );
178
+ const compactHost = compactRouterHost(root);
179
+ const required = compactHost
180
+ ? REQUIRED_GATE_FILES.filter((relativePath) => relativePath !== '.mcp.json')
181
+ : REQUIRED_GATE_FILES;
182
+ const missing = required.filter((relativePath) => !fs.existsSync(path.join(root, relativePath)));
183
+ if (compactHost && !hasCompactHostRegistration(root, compactHost)) {
184
+ missing.push(`compact host registration (${compactHost})`);
185
+ }
147
186
  if (!hasArkWorkflow(root)) missing.push(REQUIRED_GATE_WORKFLOW);
148
187
  return missing;
149
188
  }
@@ -1,56 +1,6 @@
1
- /**
2
- * Import-graph cycle detection (Tarjan) for ark-check.
3
- * Extracted from ark-check entry (R3).
4
- */
5
- export function detectCycles(graph) {
6
- let index = 0;
7
- const indices = new Map();
8
- const low = new Map();
9
- const onStack = new Set();
10
- const stack = [];
11
- const components = [];
12
-
13
- // ponytail: recursive Tarjan; make it iterative only if a real repo blows the stack.
14
- const strongconnect = (v) => {
15
- indices.set(v, index);
16
- low.set(v, index);
17
- index += 1;
18
- stack.push(v);
19
- onStack.add(v);
20
- for (const w of [...(graph.get(v) ?? [])].sort()) {
21
- if (!graph.has(w)) continue;
22
- if (!indices.has(w)) {
23
- strongconnect(w);
24
- low.set(v, Math.min(low.get(v), low.get(w)));
25
- } else if (onStack.has(w)) {
26
- low.set(v, Math.min(low.get(v), indices.get(w)));
27
- }
28
- }
29
- if (low.get(v) === indices.get(v)) {
30
- const comp = [];
31
- let w;
32
- do {
33
- w = stack.pop();
34
- onStack.delete(w);
35
- comp.push(w);
36
- } while (w !== v);
37
- if (comp.length > 1) components.push(comp.sort());
38
- }
39
- };
1
+ /** Compatibility adapter; canonical Tarjan evaluation lives in the bundled Kernel engine. */
2
+ import { detectArchitectureCycles } from './analysis-engine.mjs';
40
3
 
41
- for (const v of [...graph.keys()].sort()) {
42
- if (!indices.has(v)) strongconnect(v);
43
- }
44
-
45
- return components
46
- .sort((a, b) => a[0].localeCompare(b[0]))
47
- .map((members) => ({
48
- ruleId: 'CIRCULAR_DEPENDENCY',
49
- file: members[0],
50
- line: 1,
51
- target: members.join(' → '),
52
- message: `Circular dependency among ${members.length} files: ${members.join(' → ')} → ${members[0]}.`,
53
- // Graph is value/runtime edges only (type-only imports omitted).
54
- cycleKind: 'value',
55
- }));
4
+ export function detectCycles(graph) {
5
+ return detectArchitectureCycles(graph);
56
6
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Host hook / MCP project templates for agent-gate install (Claude, Grok).
2
+ * Host hook / MCP project templates for agent-gate install (Claude, Grok, Codex).
3
3
  * Kept out of agent-gates.mjs so install orchestration stays scannable (explore gap #5).
4
4
  */
5
5
  import { execCommandParts, execRunner } from '../ark-shared.mjs';
@@ -41,6 +41,38 @@ export function claudeSettings(root) {
41
41
  }, null, 2)}\n`;
42
42
  }
43
43
 
44
+ export function codexHooks(root) {
45
+ const runner = execRunner(root);
46
+ const codexRoot = '${CODEX_PROJECT_DIR:-${PWD:-.}}';
47
+ return `${JSON.stringify({
48
+ hooks: {
49
+ SessionStart: [
50
+ {
51
+ hooks: [
52
+ {
53
+ type: 'command',
54
+ timeout: 30,
55
+ command: `${runner} ${PREFERRED_MCP_BIN} --session-context --root "${codexRoot}" --config ark.config.json`,
56
+ },
57
+ ],
58
+ },
59
+ ],
60
+ PreToolUse: [
61
+ {
62
+ matcher: 'ApplyPatch|apply_patch|Write|Edit|MultiEdit',
63
+ hooks: [
64
+ {
65
+ type: 'command',
66
+ timeout: 30,
67
+ command: `${runner} ${PREFERRED_MCP_BIN} --hook --hook-repair --root "${codexRoot}" --config ark.config.json`,
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ },
73
+ }, null, 2)}\n`;
74
+ }
75
+
44
76
  // Grok Build project config: MCP registration (commit-friendly relative paths — unlike
45
77
  // Codex's global config.toml, Grok loads .grok/config.toml from the project).
46
78
  export function grokProjectConfig(root) {
@@ -36,7 +36,13 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
36
36
  true
37
37
  ),
38
38
  cursor: hostProfile('Cursor', null, null, false, false),
39
- codex: hostProfile('OpenAI Codex', null, null, false, false),
39
+ codex: hostProfile(
40
+ 'OpenAI Codex',
41
+ '.codex/hooks.json',
42
+ 'Best-effort PreToolUse `apply_patch`; Code Mode hosts may bypass the event',
43
+ false,
44
+ false
45
+ ),
40
46
  });
41
47
 
42
48
  export const HOST_SUPPORT_HOSTS = Object.freeze(Object.keys(HOST_SUPPORT_MATRIX));
@@ -20,12 +20,14 @@ import {
20
20
  import {
21
21
  PREFERRED_MCP_BIN,
22
22
  claudeSettings,
23
+ codexHooks,
23
24
  grokHooks,
24
25
  grokProjectConfig,
25
26
  } from './hook-templates.mjs';
26
27
  import {
27
28
  hasCheckArchitectureScript,
28
29
  ensureTypecheckScript,
30
+ compactRouterHost,
29
31
  writeTemplate,
30
32
  } from './gate-files.mjs';
31
33
  import {
@@ -36,6 +38,7 @@ import {
36
38
  detectCiNode,
37
39
  cursorRule,
38
40
  instructionRule,
41
+ compactAgentInstructions,
39
42
  codexTomlSnippet,
40
43
  arkCheckCommand,
41
44
  checkArchitectureScriptSnippet,
@@ -215,12 +218,21 @@ export function runInstallAgentGates(args) {
215
218
  }
216
219
  const pm = packageManager(root);
217
220
  const hasCheckScript = hasCheckArchitectureScript(root);
218
- const { tools, source } = resolveTools(args);
221
+ const { tools, source } = args.compact && args.tools == null
222
+ ? { tools: new Set(), source: 'compact-none' }
223
+ : resolveTools(args);
224
+ if (args.compact && tools.size > 1) {
225
+ console.error('--compact accepts exactly one selected host. Pass --tools <host>.');
226
+ process.exitCode = 2;
227
+ return;
228
+ }
219
229
  const toolSource =
220
230
  source === 'explicit'
221
231
  ? 'from --tools'
222
232
  : source === 'detected'
223
233
  ? 'auto-detected from config dirs'
234
+ : source === 'compact-none'
235
+ ? 'no active host detected'
224
236
  : 'default set — no agent config dirs found';
225
237
  console.log(`Agent gates for: ${[...tools].sort().join(', ')} (${toolSource})`);
226
238
  const templates = [];
@@ -231,15 +243,21 @@ export function runInstallAgentGates(args) {
231
243
  // Do not mutate package.json under --skills-only (typecheck bootstrap is gates/CI).
232
244
  if (!args.skillsOnly) {
233
245
  // Bootstrap typecheck before CI template so generated workflow includes the step.
234
- const typecheckBootstrap = ensureTypecheckScript(root, { write: true });
235
- if (typecheckBootstrap.changed && !args.json) {
246
+ const typecheckBootstrap = ensureTypecheckScript(root, { write: !args.compact });
247
+ if (typecheckBootstrap.changed && !args.compact && !args.json) {
236
248
  console.log(
237
249
  `Added package.json script "typecheck": "${typecheckBootstrap.script}" (tsconfig present; local/CI parity).`
238
250
  );
239
251
  }
240
252
  // Base gates: tool-agnostic contract + CI backstop, always written.
241
- templates.push(['AGENTS.md', agentInstructions(root)]);
242
- templates.push(['.mcp.json', mcpJson(root)]);
253
+ const compactHost = args.compact ? [...tools][0] ?? null : null;
254
+ templates.push([
255
+ 'AGENTS.md',
256
+ args.compact ? compactAgentInstructions(root, compactHost) : agentInstructions(root),
257
+ ]);
258
+ if (!args.compact || !compactHost || compactHost === 'claude') {
259
+ templates.push(['.mcp.json', mcpJson(root)]);
260
+ }
243
261
  templates.push([
244
262
  '.github/workflows/ark-check.yml',
245
263
  (() => {
@@ -252,13 +270,14 @@ export function runInstallAgentGates(args) {
252
270
  ]);
253
271
  if (tools.has('cursor')) {
254
272
  templates.push(['.cursor/mcp.json', mcpJson(root)]);
255
- templates.push(['.cursor/rules/ark.mdc', cursorRule(root)]);
273
+ if (!args.compact) templates.push(['.cursor/rules/ark.mdc', cursorRule(root)]);
256
274
  }
257
275
  if (tools.has('claude')) {
258
276
  templates.push(['.claude/settings.json', claudeSettings(root)]);
259
277
  }
260
278
  if (tools.has('codex')) {
261
- templates.push(['docs/ark-codex-config.toml', codexTomlSnippet(root)]);
279
+ templates.push(['.codex/hooks.json', codexHooks(root)]);
280
+ if (!args.compact) templates.push(['docs/ark-codex-config.toml', codexTomlSnippet(root)]);
262
281
  }
263
282
  if (tools.has('grok')) {
264
283
  templates.push(['.grok/config.toml', grokProjectConfig(root)]);
@@ -296,18 +315,37 @@ export function runInstallAgentGates(args) {
296
315
  const version = arkPackageVersion();
297
316
  const skills = skillTemplates().map(([name, content]) => [name, stampSkill(content, version)]);
298
317
  const skillPaths = new Set();
299
- for (const tool of tools) {
300
- const target = SKILL_TOOL_TARGETS[tool];
301
- if (!target) continue;
302
- for (const [name, content] of skills) {
303
- const relativePath = target(name);
304
- skillPaths.add(relativePath);
305
- templates.push([relativePath, content]);
318
+ if (!args.compact) {
319
+ for (const tool of tools) {
320
+ const target = SKILL_TOOL_TARGETS[tool];
321
+ if (!target) continue;
322
+ for (const [name, content] of skills) {
323
+ const relativePath = target(name);
324
+ skillPaths.add(relativePath);
325
+ templates.push([relativePath, content]);
326
+ }
327
+ }
328
+ }
329
+
330
+ // A compact router can be moved back from an explicit host removal. Delete the
331
+ // generic MCP file only when it exactly matches Ark's generated artifact.
332
+ const priorCompactHost = compactRouterHost(root);
333
+ if (args.compact && priorCompactHost === 'none' && [...tools][0]) {
334
+ const genericMcp = path.join(root, '.mcp.json');
335
+ try {
336
+ if (fs.readFileSync(genericMcp, 'utf8') === mcpJson(root)) fs.rmSync(genericMcp);
337
+ } catch {
338
+ // Missing or customized user MCP configuration is deliberately retained.
306
339
  }
307
340
  }
308
341
 
309
342
  const results = templates.map(([relativePath, content]) =>
310
- writeTemplate(root, relativePath, content, args.force)
343
+ writeTemplate(
344
+ root,
345
+ relativePath,
346
+ content,
347
+ args.force || (args.compact && relativePath === 'AGENTS.md' && priorCompactHost !== null)
348
+ )
311
349
  );
312
350
 
313
351
  console.log('Ark agent gate templates:');
@@ -397,7 +435,7 @@ export function runInstallAgentGates(args) {
397
435
  // developer's real config. A genuinely redirected CODEX_HOME or explicit
398
436
  // --codex-home still wires as requested.
399
437
  let codexMcp = null;
400
- const wantCodexWire = tools.has('codex') || args.codexHome;
438
+ const wantCodexWire = !args.compact && (tools.has('codex') || args.codexHome);
401
439
  const skipHomeWire =
402
440
  wantCodexWire &&
403
441
  isTempOrUpgradeRoot(root) &&
@@ -5,6 +5,7 @@ import {
5
5
  applyFrameworkLayoutOverlays,
6
6
  createElevenLayerConfig,
7
7
  DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
8
+ discoverRepoUnits,
8
9
  DEFAULT_INTENT_PREFIXES,
9
10
  resolveIncludeRoots,
10
11
  } from '../ark-shared.mjs';
@@ -203,12 +204,41 @@ export const ARCHITECTURE_PRESETS = {
203
204
  monorepo: (includeDirs, root) => {
204
205
  let include =
205
206
  includeDirs && includeDirs.length > 0 ? [...includeDirs] : [];
207
+ let units = [];
206
208
  if (root) {
207
209
  const resolved = resolveIncludeRoots(root);
208
210
  if (resolved.length > 0) include = resolved;
211
+ units = discoverRepoUnits(root);
212
+ const nonProductRoots = new Set(
213
+ units
214
+ .filter((unit) => ['docs', 'example', 'test'].includes(unit.role))
215
+ .map((unit) => unit.root)
216
+ );
217
+ include = include.filter((entry) => !nonProductRoots.has(entry));
209
218
  }
210
219
  // Turborepo: apps/ + packages/; Nx enterprise: apps/ + libs/ (+ packages/).
211
220
  if (include.length === 0) include = ['packages', 'apps', 'libs'];
221
+ // Established workspaces often have flat package source roots rather than a
222
+ // directory named domain/application. The package manifest supplies a real,
223
+ // reviewable role signal: a published library is a domain surface; an app or
224
+ // CLI package coordinates work. This is deliberately limited to package roots
225
+ // Ark already discovered, never a catch-all **/src/** fallback.
226
+ const packagePatterns = (roles) => {
227
+ if (!root) return [];
228
+ return units
229
+ .filter((unit) => roles.includes(unit.role))
230
+ .flatMap((unit) => unit.sourceRoots.map((sourceRoot) => {
231
+ const base = unit.root === '.'
232
+ ? sourceRoot
233
+ : sourceRoot === '.'
234
+ ? unit.root
235
+ : `${unit.root}/${sourceRoot}`;
236
+ return base === '.' ? null : `${base}/**`;
237
+ }))
238
+ .filter(Boolean);
239
+ };
240
+ const librarySourcePatterns = packagePatterns(['library']);
241
+ const applicationSourcePatterns = packagePatterns(['application', 'cli']);
212
242
  return presetWithOverlays(
213
243
  {
214
244
  include,
@@ -219,14 +249,24 @@ export const ARCHITECTURE_PRESETS = {
219
249
  'Pure business rules and entities, in any package. No I/O, no framework, no ambient globals.',
220
250
  // Domain by intentional folders only — NOT bare **/types.ts (that mis-classifies
221
251
  // application bags like frontend/src/core/**/types.ts as Domain and creates false edges).
222
- patterns: ['**/domain/**', '**/entities/**', '**/cinematic/types.ts'],
252
+ patterns: [
253
+ '**/domain/**',
254
+ '**/entities/**',
255
+ '**/cinematic/types.ts',
256
+ ...librarySourcePatterns,
257
+ ],
223
258
  forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
224
259
  optional: true,
225
260
  },
226
261
  {
227
262
  name: 'ApplicationOrchestration',
228
263
  description: 'Use cases and services that coordinate the domain through ports.',
229
- patterns: ['**/application/**', '**/use-cases/**', '**/services/**'],
264
+ patterns: [
265
+ '**/application/**',
266
+ '**/use-cases/**',
267
+ '**/services/**',
268
+ ...applicationSourcePatterns,
269
+ ],
230
270
  optional: true,
231
271
  },
232
272
  {
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { globToRegExp } from '../ark-shared.mjs';
5
+ import { extractSemanticDependencies } from './analysis-engine.mjs';
5
6
  import { lineOf } from './ast-scan.mjs';
6
7
  import { normalize } from './scan-files.mjs';
7
8
 
@@ -104,9 +105,26 @@ export function collectSafetyDiagnostics(ts, root, config, files) {
104
105
 
105
106
  for (const file of files) {
106
107
  const source = fs.readFileSync(file, 'utf8');
108
+ // Most source files cannot contain a safety finding. Avoid parsing a second AST when the
109
+ // lexical markers for every supported diagnostic are absent; a match only opts into the
110
+ // existing exact AST analysis, so this cannot suppress a finding.
111
+ if (!/@ts-(?:ignore|nocheck)\b|\bany\b|\b(?:import|require)\s*\(|\barkgate(?:\/runtime)?\b/.test(source)) {
112
+ continue;
113
+ }
107
114
  const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
108
115
  const relFile = normalize(path.relative(root, file));
109
116
 
117
+ if (!matchesAny(relFile, dynamicAllowlist)) {
118
+ for (const dependency of extractSemanticDependencies(ts, sourceFile)) {
119
+ if (!dependency.unresolved) continue;
120
+ report.nonLiteralDynamicImports.push({
121
+ file: relFile,
122
+ line: dependency.line,
123
+ kind: dependency.kind === 'require' ? 'require' : 'import',
124
+ });
125
+ }
126
+ }
127
+
110
128
  for (const position of tsSuppressionPositions(sourceFile, source)) {
111
129
  report.tsSuppressions.push({
112
130
  file: relFile,
@@ -146,23 +164,6 @@ export function collectSafetyDiagnostics(ts, root, config, files) {
146
164
  });
147
165
  }
148
166
 
149
- if (ts.isCallExpression(node)) {
150
- const dynamicImport = node.expression?.kind === ts.SyntaxKind.ImportKeyword;
151
- const directRequire =
152
- ts.isIdentifier(node.expression) && node.expression.text === 'require';
153
- const argument = node.arguments[0];
154
- const unresolved =
155
- (dynamicImport || directRequire) &&
156
- (!argument || !ts.isStringLiteralLike(argument));
157
- if (unresolved && !matchesAny(relFile, dynamicAllowlist)) {
158
- report.nonLiteralDynamicImports.push({
159
- file: relFile,
160
- line: lineOf(sourceFile, node.getStart(sourceFile)),
161
- kind: directRequire ? 'require' : 'import',
162
- });
163
- }
164
- }
165
-
166
167
  if (!allowInMemory && !isProvider && ts.isImportDeclaration(node)) {
167
168
  const specifier = node.moduleSpecifier;
168
169
  const fromArk = ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
@@ -12,7 +12,7 @@ export const SOURCE_FILE_NAME = /\.[cm]?[tj]sx?$/;
12
12
  * to production code (*.spec.ts). Counting them as ungoverned forces false
13
13
  * CONFIG_UNCLASSIFIED_FILES under --strict-config on every starter. */
14
14
  export const TEST_FILE_NAME =
15
- /\.(spec|test)\.(tsx?|jsx?|mts|cts)$/i;
15
+ /(^test(?:[-_.]).*|\.(spec|test)(?:-d)?\.)(tsx?|jsx?|mjsx?|cjsx?|mts|cts)$/i;
16
16
 
17
17
  export function isGovernableSourceFile(name) {
18
18
  return SOURCE_FILE_NAME.test(name) && !name.endsWith('.d.ts') && !TEST_FILE_NAME.test(name);
@@ -23,6 +23,17 @@ export function isSkippedSourceDir(name) {
23
23
  name === 'node_modules' ||
24
24
  name === 'dist' ||
25
25
  name === 'coverage' ||
26
+ name === 'bench' ||
27
+ name === 'benches' ||
28
+ name === 'benchmark' ||
29
+ name === 'benchmarks' ||
30
+ name === 'docs' ||
31
+ name === 'documentation' ||
32
+ name === 'example' ||
33
+ name === 'examples' ||
34
+ name === 'fixture' ||
35
+ name === 'fixtures' ||
36
+ name === 'playground' ||
26
37
  name === '__tests__' ||
27
38
  name === '__mocks__' ||
28
39
  name === 'e2e' ||
@@ -4,7 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { codexPromptsDir } from './codex-home.mjs';
7
- import { __packageRoot, readJson } from './gate-files.mjs';
7
+ import { __packageRoot, isCompactRouterAgentsContent, readJson } from './gate-files.mjs';
8
8
 
9
9
  export function normalizeToolsList(tools) {
10
10
  if (tools == null) return [];
@@ -284,6 +284,13 @@ export function detectCodexHomeGap(root) {
284
284
 
285
285
  export function detectSkillGaps(root) {
286
286
  if (!fs.existsSync(path.join(root, 'AGENTS.md'))) return [];
287
+ try {
288
+ if (isCompactRouterAgentsContent(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'))) {
289
+ return [];
290
+ }
291
+ } catch {
292
+ // Continue with the ordinary missing-skill detection below.
293
+ }
287
294
  // The Ark source tree keeps the skill templates at templates/skills/ — it's the
288
295
  // producer, not a consumer, so it must not nag itself to "install" its own skills.
289
296
  if (fs.existsSync(path.join(root, 'templates', 'skills'))) return [];
@@ -0,0 +1,36 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/sourcePolicy.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/source-policy.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export const SOURCE_POLICY_MESSAGES = {
12
+ RAW_EVENT_PUBLISH: 'Publish through a registered intent creator; raw event objects or intent strings bypass Ark contracts and tooling.',
13
+ PUBLISH_MISSING_SOURCE: 'Strict Ark publish calls must include metadata.source.',
14
+ };
15
+ export function looksLikeArkIntent(value) {
16
+ return /^(Domain|Application|Adapter|Workflow|Job|Presentation|Reporting|Metadata|Security|Audit|Observability|Kernel)\.[A-Za-z0-9_.]+$/.test(value);
17
+ }
18
+ export function classifyPublishFacts(facts) {
19
+ if (!facts.publishCall)
20
+ return [];
21
+ const findings = [];
22
+ if ((facts.rawIntentName !== undefined && looksLikeArkIntent(facts.rawIntentName)) ||
23
+ facts.objectHasIntent) {
24
+ findings.push({
25
+ ruleId: 'RAW_EVENT_PUBLISH',
26
+ message: SOURCE_POLICY_MESSAGES.RAW_EVENT_PUBLISH,
27
+ });
28
+ }
29
+ if (facts.arkPublishCandidate && !facts.hasSource) {
30
+ findings.push({
31
+ ruleId: 'PUBLISH_MISSING_SOURCE',
32
+ message: SOURCE_POLICY_MESSAGES.PUBLISH_MISSING_SOURCE,
33
+ });
34
+ }
35
+ return findings;
36
+ }