arkgate 3.6.1 → 3.8.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 (83) hide show
  1. package/CHANGELOG.md +122 -1145
  2. package/README.md +59 -19
  3. package/bin/ark-check-runtime.mjs +1598 -0
  4. package/bin/ark-check.mjs +32 -1565
  5. package/bin/ark-layer-match.mjs +2 -1
  6. package/bin/ark-mcp-runtime.mjs +1976 -0
  7. package/bin/ark-mcp.mjs +84 -1495
  8. package/bin/ark-shared.mjs +34 -38
  9. package/bin/ark.mjs +33 -66
  10. package/bin/lib/adapter-contract.mjs +161 -9
  11. package/bin/lib/agent-gates.mjs +1 -0
  12. package/bin/lib/analysis-completeness.mjs +28 -0
  13. package/bin/lib/analysis-engine.mjs +8 -8
  14. package/bin/lib/analysis-policy.mjs +27 -0
  15. package/bin/lib/architecture-scan.mjs +70 -304
  16. package/bin/lib/auto-patch.mjs +76 -8
  17. package/bin/lib/ci-and-commands.mjs +1 -1
  18. package/bin/lib/codex-home.mjs +43 -16
  19. package/bin/lib/design-delta.mjs +4 -0
  20. package/bin/lib/design-smells.mjs +67 -14
  21. package/bin/lib/doctor-advisories.mjs +23 -7
  22. package/bin/lib/doctor-plan.mjs +44 -47
  23. package/bin/lib/enforcement-state.mjs +2 -0
  24. package/bin/lib/github-enforcement.mjs +443 -0
  25. package/bin/lib/hook-templates.mjs +12 -148
  26. package/bin/lib/html-report-advisories.mjs +59 -0
  27. package/bin/lib/html-report-depth.mjs +9 -0
  28. package/bin/lib/html-report.mjs +5 -5
  29. package/bin/lib/install-migrate.mjs +83 -79
  30. package/bin/lib/managed-upgrade.mjs +622 -0
  31. package/bin/lib/mcp-adoption.mjs +3 -1
  32. package/bin/lib/parse-health.mjs +75 -0
  33. package/bin/lib/port-proof.mjs +2 -2
  34. package/bin/lib/prepare-change.mjs +68 -38
  35. package/bin/lib/prepare-write.mjs +7 -1
  36. package/bin/lib/reshape-decisions.mjs +284 -0
  37. package/bin/lib/resident-doctor-client.mjs +55 -0
  38. package/bin/lib/resident-hook.mjs +247 -0
  39. package/bin/lib/resolved-candidate-facts.mjs +1160 -0
  40. package/bin/lib/scan-files.mjs +19 -6
  41. package/bin/lib/snippet-analysis.mjs +119 -0
  42. package/bin/lib/source-policy.mjs +24 -0
  43. package/bin/lib/typescript-host.mjs +15 -18
  44. package/bin/lib/unavailable-analysis.mjs +76 -0
  45. package/bin/lib/upgrade-command.mjs +115 -0
  46. package/bin/lib/weakest-link.mjs +21 -179
  47. package/bin/lib/write-path-capabilities.mjs +167 -16
  48. package/bin/lib/write-path-detect.mjs +3 -2
  49. package/dist/eslint/index.cjs +3 -3
  50. package/dist/eslint/index.d.ts +3 -0
  51. package/dist/eslint/index.js +3 -3
  52. package/dist/index.cjs +7 -7
  53. package/dist/index.d.ts +1073 -141
  54. package/dist/index.js +7 -7
  55. package/docs/agent-guide.md +127 -52
  56. package/docs/ai-gates.md +100 -18
  57. package/docs/configuration.md +6 -0
  58. package/docs/demos/01-write-gate-self-correction.md +2 -2
  59. package/docs/enthusiast/README.md +10 -10
  60. package/docs/enthusiast/how-to-gallery-starter.md +2 -2
  61. package/docs/enthusiast/reference-commands.md +18 -1
  62. package/docs/enthusiast/tutorial-first-project.md +2 -2
  63. package/docs/package-surface.md +101 -14
  64. package/docs/typescript-support.md +118 -37
  65. package/package.json +33 -4
  66. package/schemas/ark.analysis-result.schema.json +159 -2
  67. package/schemas/ark.design-delta.schema.json +1 -0
  68. package/schemas/ark.enforcement-state.schema.json +84 -0
  69. package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
  70. package/server.json +2 -2
  71. package/templates/skills/ark-autopilot.md +12 -0
  72. package/templates/skills/ark-explore.md +12 -5
  73. package/templates/skills/ark-fix.md +12 -2
  74. package/templates/skills/ark-loop.md +14 -1
  75. package/templates/skills/ark-runtime.md +15 -8
  76. package/templates/skills/ark-upgrade.md +122 -182
  77. package/bin/lib/ai-velocity.mjs +0 -293
  78. package/bin/lib/graph-cycles.mjs +0 -6
  79. package/bin/lib/safety-diagnostics.mjs +0 -284
  80. package/bin/lib/ts-resolve.mjs +0 -227
  81. package/dist/configTypes-DAPvBqK6.d.cts +0 -61
  82. package/dist/eslint/index.d.cts +0 -146
  83. package/dist/index.d.cts +0 -986
@@ -1,284 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import { globToRegExp } from '../ark-shared.mjs';
5
- import { extractSemanticDependencies } from './analysis-engine.mjs';
6
- import { lineOf } from './ast-scan.mjs';
7
- import { normalize } from './scan-files.mjs';
8
-
9
- const IN_MEMORY_STORES = new Set([
10
- 'InMemoryAuditStore',
11
- 'InMemoryOutboxStore',
12
- 'InMemoryReadModelStore',
13
- 'InMemoryWorkflowStore',
14
- ]);
15
-
16
- const IN_MEMORY_DEFAULT_FACTORIES = new Map([
17
- ['createArkKernel', ['outbox', 'auditTrail', 'projections']],
18
- ['createAuditTrail', ['store']],
19
- ['createProjectionRegistry', ['store']],
20
- ['createWorkflowEngine', ['store']],
21
- ]);
22
-
23
- function matchesAny(relFile, patterns) {
24
- return patterns.some((pattern) => {
25
- try {
26
- return globToRegExp(pattern).test(relFile);
27
- } catch {
28
- return false;
29
- }
30
- });
31
- }
32
-
33
- function packageName(root) {
34
- try {
35
- return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
36
- } catch {
37
- return undefined;
38
- }
39
- }
40
-
41
- function propertyName(ts, node) {
42
- if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) return node.text;
43
- return undefined;
44
- }
45
-
46
- function objectHasProperty(ts, object, name) {
47
- return object.properties.some((property) => {
48
- if (ts.isShorthandPropertyAssignment(property)) return property.name.text === name;
49
- return property.name ? propertyName(ts, property.name) === name : false;
50
- });
51
- }
52
-
53
- function tsSuppressionPositions(sourceFile, source) {
54
- const positions = new Set();
55
- for (const directive of sourceFile.commentDirectives ?? []) {
56
- const start = directive.range?.pos;
57
- const end = directive.range?.end;
58
- if (Number.isInteger(start) && Number.isInteger(end)) {
59
- const text = source.slice(start, end);
60
- if (/\@ts-ignore\b/.test(text)) positions.add(start);
61
- }
62
- }
63
-
64
- const noCheck = sourceFile.pragmas?.get?.('ts-nocheck');
65
- const entries = Array.isArray(noCheck) ? noCheck : noCheck ? [noCheck] : [];
66
- for (const entry of entries) {
67
- const start = entry.range?.pos;
68
- if (Number.isInteger(start)) positions.add(start);
69
- }
70
- return [...positions];
71
- }
72
-
73
- export function collectSafetyDiagnostics(ts, root, config, files) {
74
- const safety = config.safety ?? {};
75
- const dynamicAllowlist = Array.isArray(config.dynamicImportAllowlist)
76
- ? config.dynamicImportAllowlist
77
- : [];
78
- const maxTsSuppressions = Number.isInteger(safety.maxTsSuppressions)
79
- ? safety.maxTsSuppressions
80
- : 0;
81
- const maxAnyCasts = Number.isInteger(safety.maxAnyCasts) ? safety.maxAnyCasts : 0;
82
- const allowInMemory = safety.allowInMemory === true;
83
- const isProvider = packageName(root) === 'arkgate';
84
- const report = {
85
- tsSuppressions: [],
86
- anyCasts: [],
87
- nonLiteralDynamicImports: [],
88
- inMemoryProductionStores: [],
89
- disabledPeerIsolationRules: [],
90
- thresholds: { maxTsSuppressions, maxAnyCasts },
91
- };
92
-
93
- if (safety.allowDisabledPeerIsolation !== true) {
94
- report.disabledPeerIsolationRules = (config.rules ?? [])
95
- .filter(
96
- (rule) =>
97
- rule?.peerIsolation === false ||
98
- (rule?.allowed === false &&
99
- rule?.from &&
100
- rule.from === rule.to &&
101
- rule.peerIsolation !== true)
102
- )
103
- .map((rule) => ({ from: rule.from, to: rule.to }));
104
- }
105
-
106
- for (const file of files) {
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
- }
114
- const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
115
- const relFile = normalize(path.relative(root, file));
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
-
128
- for (const position of tsSuppressionPositions(sourceFile, source)) {
129
- report.tsSuppressions.push({
130
- file: relFile,
131
- line: lineOf(sourceFile, position),
132
- });
133
- }
134
-
135
- const importedFactories = new Map();
136
- const arkNamespaces = new Set();
137
- if (!allowInMemory && !isProvider) {
138
- for (const statement of sourceFile.statements) {
139
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) {
140
- continue;
141
- }
142
- if (!/^arkgate(?:\/runtime)?$/.test(statement.moduleSpecifier.text)) continue;
143
- const bindings = statement.importClause?.namedBindings;
144
- if (bindings && ts.isNamespaceImport(bindings)) arkNamespaces.add(bindings.name.text);
145
- if (!bindings || !ts.isNamedImports(bindings)) continue;
146
- for (const element of bindings.elements) {
147
- const imported = element.propertyName?.text ?? element.name.text;
148
- const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
149
- if (requirements) {
150
- importedFactories.set(element.name.text, { imported, requirements });
151
- }
152
- }
153
- }
154
- }
155
-
156
- const visit = (node) => {
157
- if (
158
- (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) &&
159
- node.type?.kind === ts.SyntaxKind.AnyKeyword
160
- ) {
161
- report.anyCasts.push({
162
- file: relFile,
163
- line: lineOf(sourceFile, node.getStart(sourceFile)),
164
- });
165
- }
166
-
167
- if (!allowInMemory && !isProvider && ts.isImportDeclaration(node)) {
168
- const specifier = node.moduleSpecifier;
169
- const fromArk = ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
170
- if (fromArk) {
171
- const elements = node.importClause?.namedBindings &&
172
- ts.isNamedImports(node.importClause.namedBindings)
173
- ? node.importClause.namedBindings.elements
174
- : [];
175
- for (const element of elements) {
176
- const imported = element.propertyName?.text ?? element.name.text;
177
- if (IN_MEMORY_STORES.has(imported)) {
178
- report.inMemoryProductionStores.push({
179
- file: relFile,
180
- line: lineOf(sourceFile, element.getStart(sourceFile)),
181
- store: imported,
182
- });
183
- }
184
- }
185
- }
186
- }
187
-
188
- if (!allowInMemory && !isProvider && ts.isCallExpression(node)) {
189
- let factory;
190
- if (ts.isIdentifier(node.expression)) {
191
- factory = importedFactories.get(node.expression.text);
192
- } else if (
193
- ts.isPropertyAccessExpression(node.expression) &&
194
- ts.isIdentifier(node.expression.expression) &&
195
- arkNamespaces.has(node.expression.expression.text)
196
- ) {
197
- const imported = node.expression.name.text;
198
- const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
199
- if (requirements) factory = { imported, requirements };
200
- }
201
-
202
- if (factory) {
203
- const options = node.arguments[0];
204
- const definitelyDefaults =
205
- !options ||
206
- (ts.isIdentifier(options) && options.text === 'undefined') ||
207
- (ts.isObjectLiteralExpression(options) &&
208
- factory.requirements.some((name) => !objectHasProperty(ts, options, name)));
209
- if (definitelyDefaults) {
210
- report.inMemoryProductionStores.push({
211
- file: relFile,
212
- line: lineOf(sourceFile, node.getStart(sourceFile)),
213
- store: `${factory.imported} defaults`,
214
- });
215
- }
216
- }
217
- }
218
-
219
- ts.forEachChild(node, visit);
220
- };
221
- visit(sourceFile);
222
- }
223
-
224
- const warnings = [];
225
- const nonLiteralImports = report.nonLiteralDynamicImports.filter(
226
- (entry) => entry.kind !== 'require'
227
- );
228
- if (nonLiteralImports.length > 0) {
229
- const first = nonLiteralImports[0];
230
- warnings.push({
231
- ruleId: 'DYNAMIC_IMPORT_NOT_ALLOWLISTED',
232
- file: first.file,
233
- line: first.line,
234
- message: `${nonLiteralImports.length} non-literal dynamic import(s) cannot be resolved statically. Add only reviewed files to dynamicImportAllowlist.`,
235
- });
236
- }
237
- const nonLiteralRequires = report.nonLiteralDynamicImports.filter(
238
- (entry) => entry.kind === 'require'
239
- );
240
- if (nonLiteralRequires.length > 0) {
241
- const first = nonLiteralRequires[0];
242
- warnings.push({
243
- ruleId: 'DYNAMIC_REQUIRE_NOT_ALLOWLISTED',
244
- file: first.file,
245
- line: first.line,
246
- message: `${nonLiteralRequires.length} non-literal require call(s) cannot be resolved statically. Add only reviewed files to dynamicImportAllowlist.`,
247
- });
248
- }
249
- if (report.tsSuppressions.length > maxTsSuppressions) {
250
- const first = report.tsSuppressions[0];
251
- warnings.push({
252
- ruleId: 'TS_SUPPRESSION_THRESHOLD_EXCEEDED',
253
- file: first.file,
254
- line: first.line,
255
- message: `${report.tsSuppressions.length} @ts-ignore/@ts-nocheck directive(s) exceed safety.maxTsSuppressions (${maxTsSuppressions}).`,
256
- });
257
- }
258
- if (report.anyCasts.length > maxAnyCasts) {
259
- const first = report.anyCasts[0];
260
- warnings.push({
261
- ruleId: 'ANY_CAST_THRESHOLD_EXCEEDED',
262
- file: first.file,
263
- line: first.line,
264
- message: `${report.anyCasts.length} explicit any cast(s) exceed safety.maxAnyCasts (${maxAnyCasts}).`,
265
- });
266
- }
267
- if (report.inMemoryProductionStores.length > 0) {
268
- const first = report.inMemoryProductionStores[0];
269
- warnings.push({
270
- ruleId: 'IN_MEMORY_STORE_IN_PRODUCTION_SOURCE',
271
- file: first.file,
272
- line: first.line,
273
- message: `${report.inMemoryProductionStores.length} ArkGate InMemory store risk(s) appear in governed production source. Provide durable stores or set safety.allowInMemory only for an explicitly ephemeral service.`,
274
- });
275
- }
276
- if (report.disabledPeerIsolationRules.length > 0) {
277
- warnings.push({
278
- ruleId: 'PEER_ISOLATION_DISABLED',
279
- message: `${report.disabledPeerIsolationRules.length} rule(s) disable or omit required peerIsolation. Restore peerIsolation: true or set safety.allowDisabledPeerIsolation only with a documented production exception.`,
280
- });
281
- }
282
-
283
- return { report, warnings };
284
- }
@@ -1,227 +0,0 @@
1
- /**
2
- * TypeScript module resolution + per-file scan cache for ark-check.
3
- * Extracted from ark-check entry (R3).
4
- */
5
- import crypto from 'node:crypto';
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
-
9
- export function createModuleResolutionHost(ts) {
10
- const sys = ts?.sys;
11
- const fileExists = (f) => {
12
- if (sys?.fileExists) return sys.fileExists(f);
13
- return fs.existsSync(f);
14
- };
15
- const readFile = (f) => {
16
- if (sys?.readFile) return sys.readFile(f);
17
- try {
18
- return fs.readFileSync(f, 'utf8');
19
- } catch {
20
- return undefined;
21
- }
22
- };
23
- const directoryExists = (d) => {
24
- if (sys?.directoryExists) return sys.directoryExists(d);
25
- try {
26
- return fs.statSync(d).isDirectory();
27
- } catch {
28
- return false;
29
- }
30
- };
31
- return {
32
- fileExists,
33
- readFile,
34
- directoryExists,
35
- getCurrentDirectory: () =>
36
- sys?.getCurrentDirectory ? sys.getCurrentDirectory() : process.cwd(),
37
- getDirectories: (d) => {
38
- if (sys?.getDirectories) return sys.getDirectories(d);
39
- try {
40
- return fs
41
- .readdirSync(d, { withFileTypes: true })
42
- .filter((e) => e.isDirectory())
43
- .map((e) => e.name);
44
- } catch {
45
- return [];
46
- }
47
- },
48
- realpath: sys?.realpath ? (p) => sys.realpath(p) : undefined,
49
- useCaseSensitiveFileNames: sys?.useCaseSensitiveFileNames ?? true,
50
- };
51
- }
52
-
53
- export function parseTsconfig(ts, configPath) {
54
- const host = createModuleResolutionHost(ts);
55
- const read = ts.readConfigFile(configPath, host.readFile);
56
- if (read.error) return {};
57
- // parseJsonConfigFileContent wants a ParseConfigHost-like object; our resolution host
58
- // is enough for option extraction.
59
- const parsed = ts.parseJsonConfigFileContent(
60
- read.config,
61
- {
62
- useCaseSensitiveFileNames: host.useCaseSensitiveFileNames,
63
- readDirectory: ts.sys?.readDirectory
64
- ? (...args) => ts.sys.readDirectory(...args)
65
- : () => [],
66
- fileExists: host.fileExists,
67
- readFile: host.readFile,
68
- },
69
- path.dirname(configPath)
70
- );
71
- return parsed.options;
72
- }
73
-
74
- /**
75
- * Compiler options for a given source file. With --tsconfig every file uses that one
76
- * config; otherwise each file uses the NEAREST tsconfig.json above it (like tsc does),
77
- * so monorepo packages with per-package path aliases resolve correctly under one --root.
78
- */
79
- export function createCompilerOptionsLookup(ts, root, tsconfigArg) {
80
- if (tsconfigArg) {
81
- const configPath = path.isAbsolute(tsconfigArg) ? tsconfigArg : path.join(root, tsconfigArg);
82
- const options = fs.existsSync(configPath) ? parseTsconfig(ts, configPath) : {};
83
- return () => options;
84
- }
85
- const byDir = new Map();
86
- const byConfig = new Map();
87
- return (file) => {
88
- const dir = path.dirname(file);
89
- if (byDir.has(dir)) return byDir.get(dir);
90
- const configPath = ts.findConfigFile(dir, ts.sys.fileExists, 'tsconfig.json');
91
- let options = {};
92
- if (configPath) {
93
- if (!byConfig.has(configPath)) byConfig.set(configPath, parseTsconfig(ts, configPath));
94
- options = byConfig.get(configPath);
95
- }
96
- byDir.set(dir, options);
97
- return options;
98
- };
99
- }
100
-
101
- /**
102
- * Per-file scan cache. A cache entry stores the parsed file's content-derived results:
103
- * content violations (forbidden globals, publish checks, intent references) and the list
104
- * of module-edge specifiers. Edges are NEVER cached as violations — they are re-resolved
105
- * against the live filesystem every run, because resolution depends on files and tsconfigs
106
- * outside the cached file. The whole cache is keyed by the config+manifest contents, so
107
- * any rule change invalidates everything.
108
- */
109
- export function scanCachePath(root) {
110
- return path.join(root, 'node_modules', '.cache', 'ark-check.json');
111
- }
112
-
113
- export function scanCacheKey(root, args) {
114
- const read = (p) => {
115
- try {
116
- return fs.readFileSync(p, 'utf8');
117
- } catch {
118
- return '';
119
- }
120
- };
121
- const configPath = path.isAbsolute(args.config) ? args.config : path.join(root, args.config);
122
- const manifestPath = args.manifest
123
- ? path.isAbsolute(args.manifest)
124
- ? args.manifest
125
- : path.join(root, args.manifest)
126
- : undefined;
127
- // Bump this schema tag whenever the cached scan shape or detection semantics change, so a
128
- // warm cache from an older Ark can't feed stale entries to new logic. v2: typeOnly on edges.
129
- // v3: per-file exportsOnlyTypes. v4: typeOnlyExportNames + namedBindings.
130
- // v5: hasTopLevelSideEffects. v6: non-exported impure inits + non-export class statics.
131
- // v7: scope-aware forbidden globals + import-equals dependency edges.
132
- // v8: opted-in capability walls (U04) — stale caches must not miss wall verdicts.
133
- return crypto
134
- .createHash('sha1')
135
- .update(`ark-check-cache-v8\0${read(configPath)}\0${manifestPath ? read(manifestPath) : ''}`)
136
- .digest('hex');
137
- }
138
-
139
- export function loadScanCache(root, key) {
140
- try {
141
- const data = JSON.parse(fs.readFileSync(scanCachePath(root), 'utf8'));
142
- return data.key === key && data.files && typeof data.files === 'object' ? data.files : undefined;
143
- } catch {
144
- return undefined;
145
- }
146
- }
147
-
148
- export function saveScanCache(root, key, files) {
149
- try {
150
- const target = scanCachePath(root);
151
- fs.mkdirSync(path.dirname(target), { recursive: true });
152
- fs.writeFileSync(target, JSON.stringify({ key, files }));
153
- } catch {
154
- // cache is best-effort: read-only filesystems just re-parse every run
155
- }
156
- }
157
-
158
- /**
159
- * Fallback resolver for extensionless relative imports whose on-disk target uses an
160
- * extension `ts.resolveModuleName` won't resolve without a matching tsconfig
161
- * (notably `.mts`/`.cts`). Mirrors the classic candidate list.
162
- */
163
- export function isFile(candidate) {
164
- try {
165
- return fs.statSync(candidate).isFile();
166
- } catch {
167
- return false;
168
- }
169
- }
170
-
171
- export function resolveRelativeFallback(fromFile, specifier) {
172
- const base = path.resolve(path.dirname(fromFile), specifier);
173
- const candidates = [
174
- base, // only used when the specifier already carries an extension (isFile filters dirs)
175
- `${base}.ts`,
176
- `${base}.tsx`,
177
- `${base}.mts`,
178
- `${base}.cts`,
179
- `${base}.js`,
180
- `${base}.jsx`,
181
- `${base}.mjs`,
182
- `${base}.cjs`,
183
- path.join(base, 'index.ts'),
184
- path.join(base, 'index.tsx'),
185
- path.join(base, 'index.mts'),
186
- path.join(base, 'index.cts'),
187
- ];
188
- // isFile (not existsSync) so a directory named like the specifier never shadows the
189
- // real module file — e.g. `./foo` must not resolve to a `foo/` directory before `foo.mts`.
190
- return candidates.find(isFile);
191
- }
192
-
193
- /**
194
- * Resolve any import specifier (relative, tsconfig path-alias, or package) to a source
195
- * file using TypeScript's module resolver, returning the resolved file (or undefined for
196
- * unresolved / declaration-only targets).
197
- *
198
- * ark-check governs one project rooted at --root. A resolved target is skipped when its
199
- * path RELATIVE TO ROOT either escapes the root (leading `..`) or contains a `node_modules`
200
- * segment. Using the root-relative path (not an absolute substring) means a project that
201
- * itself lives under a node_modules segment is still governed, while a broad catch-all
202
- * pattern (`**`) can't false-flag vendored deps or files outside the project. Monorepos can
203
- * run under a single --root (per-package tsconfigs are honored via the nearest-tsconfig
204
- * lookup); edges that resolve outside the root are still skipped.
205
- */
206
- export function resolveImport(ts, specifier, containingFile, options, host, root) {
207
- let file;
208
- // Explicit relative extensions are already unambiguous on disk. Avoid a full TypeScript
209
- // resolver walk for the common ESM/CJS form; retain TypeScript as the fallback for aliases,
210
- // packages, extensionless imports, and any explicit path that is not an existing file.
211
- if (specifier.startsWith('.') && path.extname(specifier)) {
212
- file = resolveRelativeFallback(containingFile, specifier);
213
- }
214
- if (!file) {
215
- const res = ts.resolveModuleName(specifier, containingFile, options, host);
216
- file = res.resolvedModule?.resolvedFileName;
217
- }
218
- if (!file && specifier.startsWith('.')) {
219
- file = resolveRelativeFallback(containingFile, specifier);
220
- }
221
- if (!file) return undefined;
222
- if (file.endsWith('.d.ts')) return undefined;
223
- const abs = path.resolve(file);
224
- const segments = path.relative(root, abs).split(path.sep);
225
- if (segments[0] === '..' || segments.includes('node_modules')) return undefined;
226
- return abs;
227
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Type vocabulary for the ark.config.json contract (U02 pilot 1).
3
- *
4
- * Pure declarations only — no runtime values. The loader/validator logic and the
5
- * published JSON Schema live in ./configContract.ts, whose generated CLI artifact
6
- * must stay self-contained: type-only imports/exports are erased on transpile, so
7
- * this split never reaches bin/lib/config-contract.mjs.
8
- */
9
- type ArkConfigSchemaVersion = '1.0';
10
- type ArkConfigCyclePolicy = 'strict' | 'soft' | 'framework-soft' | 'off';
11
- type ArkConfigLayerCapabilities = {
12
- deny?: string[];
13
- };
14
- type ArkConfigLayer = {
15
- name: string;
16
- patterns: string[];
17
- exclude?: string[];
18
- intentPrefixes?: string[];
19
- description?: string;
20
- forbiddenGlobals?: string[];
21
- /** ADR 0009 D2 — opt-in effect-capability walls; absence changes no verdict. */
22
- capabilities?: ArkConfigLayerCapabilities;
23
- /** Dual-depth sugar: `pure: true` denies all seven capabilities. */
24
- pure?: boolean;
25
- mayImportInfrastructure?: boolean;
26
- optional?: boolean;
27
- };
28
- type ArkConfigRule = {
29
- from: string;
30
- to: string;
31
- allowed: boolean;
32
- message?: string;
33
- peerIsolation?: boolean;
34
- sliceFolders?: string[];
35
- };
36
- type ArkConfigSafety = {
37
- maxTsSuppressions?: number;
38
- maxAnyCasts?: number;
39
- allowInMemory?: boolean;
40
- allowDisabledPeerIsolation?: boolean;
41
- };
42
- type ArkConfig = {
43
- $schema: string;
44
- schemaVersion: ArkConfigSchemaVersion;
45
- name?: string;
46
- include: string[];
47
- exclude?: string[];
48
- excludeGenerated?: boolean;
49
- frameworkOverlay?: string;
50
- layers: ArkConfigLayer[];
51
- rules: ArkConfigRule[];
52
- cyclePolicy?: ArkConfigCyclePolicy;
53
- dynamicImportAllowlist?: string[];
54
- safety?: ArkConfigSafety;
55
- };
56
- type ArkConfigLoadResult = {
57
- config: ArkConfig;
58
- migratedFrom: 'unversioned' | null;
59
- };
60
-
61
- export type { ArkConfig as A, ArkConfigRule as a, ArkConfigSchemaVersion as b, ArkConfigLoadResult as c, ArkConfigLayer as d };