arkgate 2.12.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 (88) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +90 -51
  3. package/bin/ark-check.mjs +156 -39
  4. package/bin/ark-mcp.mjs +119 -6
  5. package/bin/ark-shared.mjs +216 -129
  6. package/bin/ark.mjs +134 -34
  7. package/bin/lib/adapter-contract.mjs +93 -0
  8. package/bin/lib/agent-gates.mjs +13 -0
  9. package/bin/lib/analysis-engine.mjs +1171 -0
  10. package/bin/lib/architecture-scan.mjs +84 -127
  11. package/bin/lib/ci-and-commands.mjs +40 -3
  12. package/bin/lib/codex-home.mjs +7 -0
  13. package/bin/lib/config-contract.mjs +331 -0
  14. package/bin/lib/config-warnings.mjs +7 -205
  15. package/bin/lib/doctor-plan.mjs +43 -16
  16. package/bin/lib/enforcement-profiles.mjs +97 -0
  17. package/bin/lib/field-install.mjs +67 -10
  18. package/bin/lib/gate-files.mjs +42 -3
  19. package/bin/lib/graph-cycles.mjs +4 -54
  20. package/bin/lib/hook-templates.mjs +33 -1
  21. package/bin/lib/host-support-matrix.mjs +83 -0
  22. package/bin/lib/install-migrate.mjs +99 -30
  23. package/bin/lib/mcp-adoption.mjs +35 -3
  24. package/bin/lib/open-html.mjs +75 -0
  25. package/bin/lib/presets.mjs +45 -4
  26. package/bin/lib/safety-diagnostics.mjs +36 -15
  27. package/bin/lib/scan-files.mjs +12 -1
  28. package/bin/lib/skill-install.mjs +72 -1
  29. package/bin/lib/source-policy.mjs +36 -0
  30. package/bin/lib/start-preview.mjs +271 -0
  31. package/bin/lib/ts-resolve.mjs +13 -3
  32. package/bin/lib/weakest-link.mjs +417 -0
  33. package/bin/lib/write-path-capabilities.mjs +186 -0
  34. package/bin/lib/write-path-detect.mjs +62 -99
  35. package/compat/nestjs.cjs +2 -0
  36. package/compat/nestjs.d.ts +2 -0
  37. package/compat/nestjs.js +1 -0
  38. package/compat/runtime.cjs +2 -0
  39. package/compat/runtime.d.ts +2 -0
  40. package/compat/runtime.js +1 -0
  41. package/dist/configContract-BxSIwVRo.d.cts +259 -0
  42. package/dist/configContract-BxSIwVRo.d.ts +259 -0
  43. package/dist/eslint/index.cjs +500 -61
  44. package/dist/eslint/index.d.cts +36 -20
  45. package/dist/eslint/index.d.ts +36 -20
  46. package/dist/eslint/index.js +500 -61
  47. package/dist/index.cjs +1349 -2741
  48. package/dist/index.d.cts +449 -483
  49. package/dist/index.d.ts +449 -483
  50. package/dist/index.js +1325 -2687
  51. package/docs/agent-guide.md +58 -34
  52. package/docs/ai-gates.md +79 -21
  53. package/docs/configuration.md +97 -0
  54. package/docs/enthusiast/README.md +3 -3
  55. package/docs/enthusiast/how-to-agent-gates.md +7 -3
  56. package/docs/migrate-from-ark-runtime-kernel.md +5 -3
  57. package/docs/package-surface.md +19 -19
  58. package/docs/production-hardening.md +31 -5
  59. package/docs/threat-model.md +65 -0
  60. package/docs/typescript-support.md +30 -3
  61. package/package.json +46 -11
  62. package/schemas/ark.analysis-result.schema.json +91 -0
  63. package/schemas/ark.config.schema.json +750 -0
  64. package/server.json +2 -2
  65. package/templates/hooks/pre-commit-ark +37 -0
  66. package/templates/skills/ark-architect.md +3 -2
  67. package/templates/skills/ark-coverage.md +2 -2
  68. package/templates/skills/ark-runtime.md +8 -5
  69. package/templates/skills/ark-upgrade.md +36 -16
  70. package/tests/fixtures/ts-consumer/ark.config.json +2 -0
  71. package/dist/eslint/index.cjs.map +0 -1
  72. package/dist/eslint/index.js.map +0 -1
  73. package/dist/index.cjs.map +0 -1
  74. package/dist/index.js.map +0 -1
  75. package/dist/nestjs/index.cjs +0 -2498
  76. package/dist/nestjs/index.cjs.map +0 -1
  77. package/dist/nestjs/index.d.cts +0 -22
  78. package/dist/nestjs/index.d.ts +0 -22
  79. package/dist/nestjs/index.js +0 -2474
  80. package/dist/nestjs/index.js.map +0 -1
  81. package/dist/runtime/index.cjs +0 -3352
  82. package/dist/runtime/index.cjs.map +0 -1
  83. package/dist/runtime/index.d.cts +0 -2
  84. package/dist/runtime/index.d.ts +0 -2
  85. package/dist/runtime/index.js +0 -3270
  86. package/dist/runtime/index.js.map +0 -1
  87. package/dist/types-BZ17b9i5.d.cts +0 -1068
  88. package/dist/types-BZ17b9i5.d.ts +0 -1068
@@ -0,0 +1,271 @@
1
+ import crypto from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { arkCommand, buildArchitectureRecommendation } from '../ark-shared.mjs';
7
+ import { compactAgentInstructions, instructionRule, mcpJson } from './ci-and-commands.mjs';
8
+ import { claudeSettings, codexHooks, grokHooks, grokProjectConfig } from './hook-templates.mjs';
9
+
10
+ const COMPACT_HOST_TEMPLATES = {
11
+ claude: (root) => [
12
+ ['.claude/settings.json', claudeSettings(root)],
13
+ ['.mcp.json', mcpJson(root)],
14
+ ],
15
+ grok: (root) => [
16
+ ['.grok/config.toml', grokProjectConfig(root)],
17
+ ['.grok/hooks/ark-write-gate.json', grokHooks(root)],
18
+ ],
19
+ cursor: (root) => [['.cursor/mcp.json', mcpJson(root)]],
20
+ codex: (root) => [['.codex/hooks.json', codexHooks(root)]],
21
+ windsurf: (root) => [['.windsurf/rules/ark.md', instructionRule(root)]],
22
+ cline: (root) => [['.clinerules/ark.md', instructionRule(root)]],
23
+ copilot: (root) => [['.github/copilot-instructions.md', instructionRule(root)]],
24
+ kiro: (root) => [['.kiro/steering/ark.md', instructionRule(root)]],
25
+ roo: (root) => [['.roo/rules/ark.md', instructionRule(root)]],
26
+ continue: (root) => [['.continue/rules/ark.md', instructionRule(root)]],
27
+ gemini: (root) => [['GEMINI.md', instructionRule(root)]],
28
+ };
29
+
30
+ function treeFiles(root) {
31
+ const files = new Map();
32
+ const visit = (dir) => {
33
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
34
+ if (['.git', 'node_modules', 'dist', 'coverage'].includes(entry.name)) continue;
35
+ const absolute = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) visit(absolute);
37
+ else if (entry.isFile()) {
38
+ files.set(path.relative(root, absolute).split(path.sep).join('/'), fs.readFileSync(absolute));
39
+ }
40
+ }
41
+ };
42
+ visit(root);
43
+ return files;
44
+ }
45
+
46
+ function normalizedContent(content, shadowRoot, root) {
47
+ if (!content) return content;
48
+ const text = content.toString('utf8');
49
+ return text.includes('\u0000') ? content : Buffer.from(text.split(shadowRoot).join(root));
50
+ }
51
+
52
+ function digest(content) {
53
+ return `sha256:${crypto.createHash('sha256').update(content ?? Buffer.alloc(0)).digest('hex')}`;
54
+ }
55
+
56
+ function change(pathname, before, after) {
57
+ return {
58
+ path: pathname,
59
+ action: !before ? 'create' : !after ? 'delete' : 'edit',
60
+ beforeHash: before ? digest(before) : null,
61
+ afterHash: after ? digest(after) : null,
62
+ beforeBase64: before ? before.toString('base64') : null,
63
+ afterBase64: after ? after.toString('base64') : null,
64
+ };
65
+ }
66
+
67
+ function setupBudget(changes) {
68
+ const generatedChanges = changes.filter((item) => item.path !== 'package.json');
69
+ const bytes = generatedChanges.reduce(
70
+ (total, item) => total + (item.afterBase64 ? Buffer.from(item.afterBase64, 'base64').length : 0),
71
+ 0
72
+ );
73
+ return {
74
+ files: generatedChanges.length,
75
+ bytes,
76
+ maxFiles: 5,
77
+ maxBytes: 25 * 1024,
78
+ ok: generatedChanges.length <= 5 && bytes < 25 * 1024,
79
+ };
80
+ }
81
+
82
+ function commands(root, args, helpers) {
83
+ if (args.removeHost) {
84
+ return [`ark start --root ${root} --tools ${args.removeHost} --apply`];
85
+ }
86
+ const result = [];
87
+ if (args.installExplicit && args.install && fs.existsSync(path.join(root, 'package.json'))) {
88
+ const [command, commandArgs] = helpers.packageInstallArgv(root, `^${helpers.cliVersion()}`);
89
+ result.push(`${command} ${commandArgs.join(' ')}`);
90
+ }
91
+ result.push(arkCommand(root, 'ark-check', '--init'));
92
+ const host = args.tools ? ` --tools ${args.tools}` : '';
93
+ result.push(arkCommand(root, 'ark-check', `--install-agent-gates --compact${host}`));
94
+ result.push(arkCommand(root, 'ark-check', '--plan --json'));
95
+ result.push(arkCommand(root, 'ark-check', '--coverage --json'));
96
+ return result;
97
+ }
98
+
99
+ export function renderStartPreview(preview) {
100
+ console.log('Ark start preview — no files were changed.');
101
+ if (preview.analysis) {
102
+ console.log(`Your project looks like: ${preview.analysis.label} (${preview.analysis.archetype}, confidence ${preview.analysis.confidence}).`);
103
+ }
104
+ console.log(`Projected governed coverage: ${preview.projectedCoverage.percent ?? 'unknown'}% (${preview.projectedCoverage.classifiedFiles}/${preview.projectedCoverage.totalFiles} files)`);
105
+ console.log(`Compact setup budget: ${preview.setupBudget.files}/${preview.setupBudget.maxFiles} files, ${preview.setupBudget.bytes}/${preview.setupBudget.maxBytes} bytes${preview.setupBudget.ok ? '' : ' (exceeded)'}.`);
106
+ console.log('Files to create/edit/delete:');
107
+ if (preview.changes.length === 0) console.log(' (none)');
108
+ for (const change of preview.changes) {
109
+ console.log(` ${change.action.padEnd(6)} ${change.path} ${change.afterHash ?? '(deleted)'}`);
110
+ }
111
+ console.log('Commands in the approved setup plan:');
112
+ for (const command of preview.commands) console.log(` ${command}`);
113
+ console.log('Host guarantees:');
114
+ for (const guarantee of preview.hostGuarantees) console.log(` ${guarantee}`);
115
+ if (preview.unresolvedDecisions.length > 0) {
116
+ console.log('Unresolved decisions:');
117
+ for (const decision of preview.unresolvedDecisions) console.log(` ${decision}`);
118
+ }
119
+ console.log('Review complete file contents with --json. Apply this plan with: ark start --apply');
120
+ }
121
+
122
+ export function applyStartPreview(root, preview) {
123
+ if (!preview.setupBudget?.ok) {
124
+ throw new Error('Refusing to apply a start plan that exceeds the compact setup budget.');
125
+ }
126
+ for (const change of preview.changes) {
127
+ const target = path.join(root, change.path);
128
+ const current = fs.existsSync(target) ? fs.readFileSync(target) : null;
129
+ const currentHash = current ? digest(current) : null;
130
+ if (currentHash !== change.beforeHash) {
131
+ throw new Error(`Refusing to apply stale preview: ${change.path} changed after planning.`);
132
+ }
133
+ if (change.action === 'delete') {
134
+ fs.rmSync(target, { force: true });
135
+ continue;
136
+ }
137
+ fs.mkdirSync(path.dirname(target), { recursive: true });
138
+ fs.writeFileSync(target, Buffer.from(change.afterBase64, 'base64'));
139
+ }
140
+ }
141
+
142
+ function planHostRemoval(args, helpers) {
143
+ const host = args.removeHost;
144
+ const templateFactory = COMPACT_HOST_TEMPLATES[host];
145
+ if (!templateFactory) throw new Error(`Unknown compact host: ${host}.`);
146
+ const changes = [];
147
+ const unresolvedDecisions = [];
148
+ for (const [relativePath, expected] of templateFactory(args.root)) {
149
+ const target = path.join(args.root, relativePath);
150
+ if (!fs.existsSync(target)) continue;
151
+ const before = fs.readFileSync(target);
152
+ if (before.toString('utf8') !== expected) {
153
+ unresolvedDecisions.push(`${relativePath} was customized and was left untouched.`);
154
+ continue;
155
+ }
156
+ changes.push(change(relativePath, before, null));
157
+ }
158
+
159
+ const agentsPath = path.join(args.root, 'AGENTS.md');
160
+ if (fs.existsSync(agentsPath)) {
161
+ const before = fs.readFileSync(agentsPath);
162
+ if (before.toString('utf8') === compactAgentInstructions(args.root, host)) {
163
+ changes.push(change('AGENTS.md', before, Buffer.from(compactAgentInstructions(args.root))));
164
+ } else {
165
+ unresolvedDecisions.push('AGENTS.md is not the expected compact router and was left untouched.');
166
+ }
167
+ }
168
+
169
+ const genericMcp = path.join(args.root, '.mcp.json');
170
+ if (!fs.existsSync(genericMcp)) {
171
+ changes.push(change('.mcp.json', null, Buffer.from(mcpJson(args.root))));
172
+ }
173
+
174
+ return {
175
+ version: 1,
176
+ root: args.root,
177
+ readOnly: true,
178
+ mode: 'remove-host',
179
+ host,
180
+ analysis: null,
181
+ projectedCoverage: { percent: null, classifiedFiles: 0, totalFiles: 0 },
182
+ changes,
183
+ setupBudget: setupBudget(changes),
184
+ commands: commands(args.root, args, helpers),
185
+ hostGuarantees: ['host removal is limited to Ark-owned compact artifacts', 'restore with the displayed --tools command'],
186
+ unresolvedDecisions,
187
+ };
188
+ }
189
+
190
+ export async function planStart(args, helpers) {
191
+ if (args.removeHost) return planHostRemoval(args, helpers);
192
+ const root = args.root;
193
+ const before = treeFiles(root);
194
+ let recommendation = null;
195
+ try {
196
+ const rec = buildArchitectureRecommendation(root);
197
+ recommendation = {
198
+ archetype: rec.archetype,
199
+ label: rec.label,
200
+ confidence: rec.confidence,
201
+ mature: rec.mature,
202
+ };
203
+ } catch {
204
+ recommendation = null;
205
+ }
206
+ const shadowRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ark-start-preview-'));
207
+ try {
208
+ fs.cpSync(root, shadowRoot, {
209
+ recursive: true,
210
+ filter: (source) => {
211
+ const relative = path.relative(root, source).split(path.sep).join('/');
212
+ return !relative.split('/').some((part) => ['.git', 'node_modules', 'dist', 'coverage'].includes(part));
213
+ },
214
+ });
215
+ const childArgs = ['start', '--root', shadowRoot, '--internal-apply', '--skip-package-manager'];
216
+ if (args.yes) childArgs.push('--yes');
217
+ if (args.force) childArgs.push('--force');
218
+ if (!args.strict) childArgs.push('--no-strict');
219
+ if (args.installExplicit && args.install) childArgs.push('--install');
220
+ if (args.installExplicit && !args.install) childArgs.push('--no-install');
221
+ if (args.tools) childArgs.push('--tools', args.tools);
222
+ if (args.requireWriteHook) childArgs.push('--require-write-hook', args.requireWriteHook);
223
+ const planned = spawnSync(process.execPath, [helpers.cliPath, ...childArgs], {
224
+ cwd: shadowRoot,
225
+ encoding: 'utf8',
226
+ stdio: 'pipe',
227
+ });
228
+ if ((planned.status ?? 1) !== 0) {
229
+ throw new Error(`Unable to produce a safe start plan (exit ${planned.status ?? 1}): ${planned.stderr || planned.stdout}`.trim());
230
+ }
231
+ const coverageRun = spawnSync(process.execPath, [helpers.arkCheck, '--root', shadowRoot, '--config', 'ark.config.json', '--coverage', '--json'], { cwd: shadowRoot, encoding: 'utf8' });
232
+ let coverage = {};
233
+ try {
234
+ coverage = JSON.parse(coverageRun.stdout || '{}').coverage || {};
235
+ } catch {
236
+ coverage = {};
237
+ }
238
+ const afterRaw = treeFiles(shadowRoot);
239
+ const after = new Map([...afterRaw].map(([file, content]) => [file, normalizedContent(content, shadowRoot, root)]));
240
+ const changes = [];
241
+ for (const file of [...new Set([...before.keys(), ...after.keys()])].sort()) {
242
+ const oldContent = before.get(file);
243
+ const newContent = after.get(file);
244
+ if ((oldContent && newContent && oldContent.equals(newContent)) || (!oldContent && !newContent)) continue;
245
+ changes.push(change(file, oldContent, newContent));
246
+ }
247
+ const percent = coverage.governed?.percent ?? null;
248
+ return {
249
+ version: 1,
250
+ root,
251
+ readOnly: true,
252
+ analysis: recommendation,
253
+ projectedCoverage: {
254
+ percent,
255
+ classifiedFiles: coverage.governed?.classifiedFiles ?? 0,
256
+ totalFiles: coverage.governed?.totalFiles ?? coverage.totalFiles ?? 0,
257
+ },
258
+ changes,
259
+ setupBudget: setupBudget(changes),
260
+ commands: commands(root, args, helpers),
261
+ hostGuarantees: [
262
+ args.requireWriteHook ? `Hard-write hook verified for ${args.requireWriteHook}` : 'shared CI merge gate will be installed',
263
+ 'preview phase performs no writes in the target project',
264
+ 'apply writes the exact bytes identified by each afterHash',
265
+ ],
266
+ unresolvedDecisions: percent !== null && percent < 90 ? [`Projected governed coverage is ${percent}%; review unclassified files before treating the contract as complete.`] : [],
267
+ };
268
+ } finally {
269
+ fs.rmSync(shadowRoot, { recursive: true, force: true });
270
+ }
271
+ }
@@ -128,9 +128,10 @@ export function scanCacheKey(root, args) {
128
128
  // warm cache from an older Ark can't feed stale entries to new logic. v2: typeOnly on edges.
129
129
  // v3: per-file exportsOnlyTypes. v4: typeOnlyExportNames + namedBindings.
130
130
  // v5: hasTopLevelSideEffects. v6: non-exported impure inits + non-export class statics.
131
+ // v7: scope-aware forbidden globals + import-equals dependency edges.
131
132
  return crypto
132
133
  .createHash('sha1')
133
- .update(`ark-check-cache-v6\0${read(configPath)}\0${manifestPath ? read(manifestPath) : ''}`)
134
+ .update(`ark-check-cache-v7\0${read(configPath)}\0${manifestPath ? read(manifestPath) : ''}`)
134
135
  .digest('hex');
135
136
  }
136
137
 
@@ -202,8 +203,17 @@ export function resolveRelativeFallback(fromFile, specifier) {
202
203
  * lookup); edges that resolve outside the root are still skipped.
203
204
  */
204
205
  export function resolveImport(ts, specifier, containingFile, options, host, root) {
205
- const res = ts.resolveModuleName(specifier, containingFile, options, host);
206
- let file = res.resolvedModule?.resolvedFileName;
206
+ let file;
207
+ // Explicit relative extensions are already unambiguous on disk. Avoid a full TypeScript
208
+ // resolver walk for the common ESM/CJS form; retain TypeScript as the fallback for aliases,
209
+ // packages, extensionless imports, and any explicit path that is not an existing file.
210
+ if (specifier.startsWith('.') && path.extname(specifier)) {
211
+ file = resolveRelativeFallback(containingFile, specifier);
212
+ }
213
+ if (!file) {
214
+ const res = ts.resolveModuleName(specifier, containingFile, options, host);
215
+ file = res.resolvedModule?.resolvedFileName;
216
+ }
207
217
  if (!file && specifier.startsWith('.')) {
208
218
  file = resolveRelativeFallback(containingFile, specifier);
209
219
  }