arkgate 2.9.2 → 2.11.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.
- package/CHANGELOG.md +87 -0
- package/README.md +12 -2
- package/SECURITY.md +3 -4
- package/bin/ark-check.mjs +41 -16
- package/bin/ark-mcp.mjs +335 -111
- package/bin/ark.mjs +35 -5
- package/bin/lib/agent-gates.mjs +161 -8
- package/bin/lib/architecture-scan.mjs +23 -1
- package/bin/lib/auto-patch.mjs +264 -0
- package/bin/lib/baseline-key.mjs +17 -0
- package/bin/lib/config-warnings.mjs +22 -0
- package/bin/lib/core-layers.mjs +7 -0
- package/bin/lib/core-ratchet.mjs +3 -7
- package/bin/lib/doctor-plan.mjs +83 -5
- package/bin/lib/port-proof.mjs +309 -0
- package/bin/lib/prepare-write.mjs +130 -0
- package/bin/lib/remediation.mjs +21 -0
- package/bin/lib/safety-diagnostics.mjs +263 -0
- package/bin/lib/scan-files.mjs +51 -6
- package/bin/lib/violations.mjs +3 -3
- package/dist/index.cjs +115 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +115 -11
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +18 -5
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +18 -5
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +115 -11
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +115 -11
- package/dist/runtime/index.js.map +1 -1
- package/dist/{types-D6Q8WHes.d.cts → types-BZ17b9i5.d.cts} +5 -1
- package/dist/{types-D6Q8WHes.d.ts → types-BZ17b9i5.d.ts} +5 -1
- package/docs/agent-guide.md +15 -1
- package/docs/ai-gates.md +63 -5
- package/docs/enthusiast/how-to-agent-gates.md +8 -0
- package/docs/enthusiast/reference-commands.md +1 -1
- package/docs/package-surface.md +2 -2
- package/docs/production-hardening.md +5 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/templates/skills/ark-explain.md +1 -1
- package/templates/skills/ark-loop.md +2 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { globToRegExp } from '../ark-shared.mjs';
|
|
5
|
+
import { lineOf } from './ast-scan.mjs';
|
|
6
|
+
import { normalize } from './scan-files.mjs';
|
|
7
|
+
|
|
8
|
+
const IN_MEMORY_STORES = new Set([
|
|
9
|
+
'InMemoryAuditStore',
|
|
10
|
+
'InMemoryOutboxStore',
|
|
11
|
+
'InMemoryReadModelStore',
|
|
12
|
+
'InMemoryWorkflowStore',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const IN_MEMORY_DEFAULT_FACTORIES = new Map([
|
|
16
|
+
['createArkKernel', ['outbox', 'auditTrail', 'projections']],
|
|
17
|
+
['createAuditTrail', ['store']],
|
|
18
|
+
['createProjectionRegistry', ['store']],
|
|
19
|
+
['createWorkflowEngine', ['store']],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function matchesAny(relFile, patterns) {
|
|
23
|
+
return patterns.some((pattern) => {
|
|
24
|
+
try {
|
|
25
|
+
return globToRegExp(pattern).test(relFile);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function packageName(root) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function propertyName(ts, node) {
|
|
41
|
+
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) return node.text;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function objectHasProperty(ts, object, name) {
|
|
46
|
+
return object.properties.some((property) => {
|
|
47
|
+
if (ts.isShorthandPropertyAssignment(property)) return property.name.text === name;
|
|
48
|
+
return property.name ? propertyName(ts, property.name) === name : false;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tsSuppressionPositions(sourceFile, source) {
|
|
53
|
+
const positions = new Set();
|
|
54
|
+
for (const directive of sourceFile.commentDirectives ?? []) {
|
|
55
|
+
const start = directive.range?.pos;
|
|
56
|
+
const end = directive.range?.end;
|
|
57
|
+
if (Number.isInteger(start) && Number.isInteger(end)) {
|
|
58
|
+
const text = source.slice(start, end);
|
|
59
|
+
if (/\@ts-ignore\b/.test(text)) positions.add(start);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const noCheck = sourceFile.pragmas?.get?.('ts-nocheck');
|
|
64
|
+
const entries = Array.isArray(noCheck) ? noCheck : noCheck ? [noCheck] : [];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const start = entry.range?.pos;
|
|
67
|
+
if (Number.isInteger(start)) positions.add(start);
|
|
68
|
+
}
|
|
69
|
+
return [...positions];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function collectSafetyDiagnostics(ts, root, config, files) {
|
|
73
|
+
const safety = config.safety ?? {};
|
|
74
|
+
const dynamicAllowlist = Array.isArray(config.dynamicImportAllowlist)
|
|
75
|
+
? config.dynamicImportAllowlist
|
|
76
|
+
: [];
|
|
77
|
+
const maxTsSuppressions = Number.isInteger(safety.maxTsSuppressions)
|
|
78
|
+
? safety.maxTsSuppressions
|
|
79
|
+
: 0;
|
|
80
|
+
const maxAnyCasts = Number.isInteger(safety.maxAnyCasts) ? safety.maxAnyCasts : 0;
|
|
81
|
+
const allowInMemory = safety.allowInMemory === true;
|
|
82
|
+
const isProvider = packageName(root) === 'arkgate';
|
|
83
|
+
const report = {
|
|
84
|
+
tsSuppressions: [],
|
|
85
|
+
anyCasts: [],
|
|
86
|
+
nonLiteralDynamicImports: [],
|
|
87
|
+
inMemoryProductionStores: [],
|
|
88
|
+
disabledPeerIsolationRules: [],
|
|
89
|
+
thresholds: { maxTsSuppressions, maxAnyCasts },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (safety.allowDisabledPeerIsolation !== true) {
|
|
93
|
+
report.disabledPeerIsolationRules = (config.rules ?? [])
|
|
94
|
+
.filter(
|
|
95
|
+
(rule) =>
|
|
96
|
+
rule?.peerIsolation === false ||
|
|
97
|
+
(rule?.allowed === false &&
|
|
98
|
+
rule?.from &&
|
|
99
|
+
rule.from === rule.to &&
|
|
100
|
+
rule.peerIsolation !== true)
|
|
101
|
+
)
|
|
102
|
+
.map((rule) => ({ from: rule.from, to: rule.to }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
107
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
108
|
+
const relFile = normalize(path.relative(root, file));
|
|
109
|
+
|
|
110
|
+
for (const position of tsSuppressionPositions(sourceFile, source)) {
|
|
111
|
+
report.tsSuppressions.push({
|
|
112
|
+
file: relFile,
|
|
113
|
+
line: lineOf(sourceFile, position),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const importedFactories = new Map();
|
|
118
|
+
const arkNamespaces = new Set();
|
|
119
|
+
if (!allowInMemory && !isProvider) {
|
|
120
|
+
for (const statement of sourceFile.statements) {
|
|
121
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!/^arkgate(?:\/runtime)?$/.test(statement.moduleSpecifier.text)) continue;
|
|
125
|
+
const bindings = statement.importClause?.namedBindings;
|
|
126
|
+
if (bindings && ts.isNamespaceImport(bindings)) arkNamespaces.add(bindings.name.text);
|
|
127
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
128
|
+
for (const element of bindings.elements) {
|
|
129
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
130
|
+
const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
|
|
131
|
+
if (requirements) {
|
|
132
|
+
importedFactories.set(element.name.text, { imported, requirements });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const visit = (node) => {
|
|
139
|
+
if (
|
|
140
|
+
(ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) &&
|
|
141
|
+
node.type?.kind === ts.SyntaxKind.AnyKeyword
|
|
142
|
+
) {
|
|
143
|
+
report.anyCasts.push({
|
|
144
|
+
file: relFile,
|
|
145
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (ts.isCallExpression(node) && node.expression?.kind === ts.SyntaxKind.ImportKeyword) {
|
|
150
|
+
const argument = node.arguments[0];
|
|
151
|
+
if (!argument || !ts.isStringLiteralLike(argument)) {
|
|
152
|
+
if (!matchesAny(relFile, dynamicAllowlist)) {
|
|
153
|
+
report.nonLiteralDynamicImports.push({
|
|
154
|
+
file: relFile,
|
|
155
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!allowInMemory && !isProvider && ts.isImportDeclaration(node)) {
|
|
162
|
+
const specifier = node.moduleSpecifier;
|
|
163
|
+
const fromArk = ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
|
|
164
|
+
if (fromArk) {
|
|
165
|
+
const elements = node.importClause?.namedBindings &&
|
|
166
|
+
ts.isNamedImports(node.importClause.namedBindings)
|
|
167
|
+
? node.importClause.namedBindings.elements
|
|
168
|
+
: [];
|
|
169
|
+
for (const element of elements) {
|
|
170
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
171
|
+
if (IN_MEMORY_STORES.has(imported)) {
|
|
172
|
+
report.inMemoryProductionStores.push({
|
|
173
|
+
file: relFile,
|
|
174
|
+
line: lineOf(sourceFile, element.getStart(sourceFile)),
|
|
175
|
+
store: imported,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!allowInMemory && !isProvider && ts.isCallExpression(node)) {
|
|
183
|
+
let factory;
|
|
184
|
+
if (ts.isIdentifier(node.expression)) {
|
|
185
|
+
factory = importedFactories.get(node.expression.text);
|
|
186
|
+
} else if (
|
|
187
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
188
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
189
|
+
arkNamespaces.has(node.expression.expression.text)
|
|
190
|
+
) {
|
|
191
|
+
const imported = node.expression.name.text;
|
|
192
|
+
const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
|
|
193
|
+
if (requirements) factory = { imported, requirements };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (factory) {
|
|
197
|
+
const options = node.arguments[0];
|
|
198
|
+
const definitelyDefaults =
|
|
199
|
+
!options ||
|
|
200
|
+
(ts.isIdentifier(options) && options.text === 'undefined') ||
|
|
201
|
+
(ts.isObjectLiteralExpression(options) &&
|
|
202
|
+
factory.requirements.some((name) => !objectHasProperty(ts, options, name)));
|
|
203
|
+
if (definitelyDefaults) {
|
|
204
|
+
report.inMemoryProductionStores.push({
|
|
205
|
+
file: relFile,
|
|
206
|
+
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
207
|
+
store: `${factory.imported} defaults`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
ts.forEachChild(node, visit);
|
|
214
|
+
};
|
|
215
|
+
visit(sourceFile);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const warnings = [];
|
|
219
|
+
if (report.nonLiteralDynamicImports.length > 0) {
|
|
220
|
+
const first = report.nonLiteralDynamicImports[0];
|
|
221
|
+
warnings.push({
|
|
222
|
+
ruleId: 'DYNAMIC_IMPORT_NOT_ALLOWLISTED',
|
|
223
|
+
file: first.file,
|
|
224
|
+
line: first.line,
|
|
225
|
+
message: `${report.nonLiteralDynamicImports.length} non-literal dynamic import(s) cannot be resolved statically. Add only reviewed files to dynamicImportAllowlist.`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (report.tsSuppressions.length > maxTsSuppressions) {
|
|
229
|
+
const first = report.tsSuppressions[0];
|
|
230
|
+
warnings.push({
|
|
231
|
+
ruleId: 'TS_SUPPRESSION_THRESHOLD_EXCEEDED',
|
|
232
|
+
file: first.file,
|
|
233
|
+
line: first.line,
|
|
234
|
+
message: `${report.tsSuppressions.length} @ts-ignore/@ts-nocheck directive(s) exceed safety.maxTsSuppressions (${maxTsSuppressions}).`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (report.anyCasts.length > maxAnyCasts) {
|
|
238
|
+
const first = report.anyCasts[0];
|
|
239
|
+
warnings.push({
|
|
240
|
+
ruleId: 'ANY_CAST_THRESHOLD_EXCEEDED',
|
|
241
|
+
file: first.file,
|
|
242
|
+
line: first.line,
|
|
243
|
+
message: `${report.anyCasts.length} explicit any cast(s) exceed safety.maxAnyCasts (${maxAnyCasts}).`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (report.inMemoryProductionStores.length > 0) {
|
|
247
|
+
const first = report.inMemoryProductionStores[0];
|
|
248
|
+
warnings.push({
|
|
249
|
+
ruleId: 'IN_MEMORY_STORE_IN_PRODUCTION_SOURCE',
|
|
250
|
+
file: first.file,
|
|
251
|
+
line: first.line,
|
|
252
|
+
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.`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (report.disabledPeerIsolationRules.length > 0) {
|
|
256
|
+
warnings.push({
|
|
257
|
+
ruleId: 'PEER_ISOLATION_DISABLED',
|
|
258
|
+
message: `${report.disabledPeerIsolationRules.length} rule(s) disable or omit required peerIsolation. Restore peerIsolation: true or set safety.allowDisabledPeerIsolation only with a documented production exception.`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { report, warnings };
|
|
263
|
+
}
|
package/bin/lib/scan-files.mjs
CHANGED
|
@@ -32,24 +32,62 @@ export function isSkippedSourceDir(name) {
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const
|
|
35
|
+
function isInsideRoot(root, target) {
|
|
36
|
+
const rel = path.relative(root, target);
|
|
37
|
+
return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Walk source files while treating symlinks explicitly.
|
|
42
|
+
*
|
|
43
|
+
* When `root` is provided, every resolved file/directory must stay inside it.
|
|
44
|
+
* Internal symlink directories are followed once (TypeScript follows them too),
|
|
45
|
+
* while escaping links fail closed instead of reading arbitrary filesystem paths.
|
|
46
|
+
*/
|
|
47
|
+
export function walk(dir, files = [], options = {}) {
|
|
48
|
+
const state = options.state ?? {
|
|
49
|
+
root: options.root ? fs.realpathSync(options.root) : undefined,
|
|
50
|
+
visitedDirectories: new Set(),
|
|
51
|
+
visitedFiles: new Set(),
|
|
52
|
+
};
|
|
53
|
+
const lstat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
54
|
+
if (!lstat) return files;
|
|
55
|
+
const resolved = fs.realpathSync(dir);
|
|
56
|
+
if (state.root && !isInsideRoot(state.root, resolved)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Refusing to scan symlink outside project root: ${dir} -> ${resolved}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const stat = lstat.isSymbolicLink()
|
|
62
|
+
? fs.statSync(dir, { throwIfNoEntry: false })
|
|
63
|
+
: lstat;
|
|
37
64
|
if (!stat) return files;
|
|
38
65
|
// An `include` entry may be a single file (e.g. a root-level "middleware.ts"),
|
|
39
66
|
// not just a directory — govern it directly instead of trying to scandir it
|
|
40
67
|
// (which threw ENOTDIR). The extension filter still applies.
|
|
41
68
|
if (stat.isFile()) {
|
|
42
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
isGovernableSourceFile(path.basename(dir)) &&
|
|
71
|
+
!state.visitedFiles.has(resolved)
|
|
72
|
+
) {
|
|
73
|
+
state.visitedFiles.add(resolved);
|
|
74
|
+
files.push(dir);
|
|
75
|
+
}
|
|
43
76
|
return files;
|
|
44
77
|
}
|
|
45
78
|
if (!stat.isDirectory()) return files;
|
|
79
|
+
if (state.visitedDirectories.has(resolved)) return files;
|
|
80
|
+
state.visitedDirectories.add(resolved);
|
|
46
81
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
47
82
|
const full = path.join(dir, entry.name);
|
|
48
83
|
if (entry.isDirectory()) {
|
|
49
84
|
if (isSkippedSourceDir(entry.name)) continue;
|
|
50
|
-
walk(full, files);
|
|
85
|
+
walk(full, files, { state });
|
|
86
|
+
} else if (entry.isSymbolicLink()) {
|
|
87
|
+
if (isSkippedSourceDir(entry.name)) continue;
|
|
88
|
+
walk(full, files, { state });
|
|
51
89
|
} else if (isGovernableSourceFile(entry.name)) {
|
|
52
|
-
|
|
90
|
+
walk(full, files, { state });
|
|
53
91
|
}
|
|
54
92
|
}
|
|
55
93
|
return files;
|
|
@@ -57,7 +95,14 @@ export function walk(dir, files = []) {
|
|
|
57
95
|
|
|
58
96
|
/** Walk include roots then drop codegen / config.exclude (universal scan filter). */
|
|
59
97
|
export function collectGovernedFiles(root, config) {
|
|
60
|
-
const
|
|
98
|
+
const state = {
|
|
99
|
+
root: fs.realpathSync(root),
|
|
100
|
+
visitedDirectories: new Set(),
|
|
101
|
+
visitedFiles: new Set(),
|
|
102
|
+
};
|
|
103
|
+
const raw = (config.include ?? []).flatMap((entry) =>
|
|
104
|
+
walk(path.join(root, entry), [], { state })
|
|
105
|
+
);
|
|
61
106
|
return raw.filter((abs) => {
|
|
62
107
|
const rel = normalize(path.relative(root, abs));
|
|
63
108
|
return !isScanExcludedRelative(rel, config);
|
package/bin/lib/violations.mjs
CHANGED
|
@@ -11,8 +11,8 @@ const color = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/** Canonical: src/domain/baselineKey.ts → bin/lib/baseline-key.mjs (R4). */
|
|
14
|
-
import { baselineKey } from './baseline-key.mjs';
|
|
15
|
-
export { baselineKey };
|
|
14
|
+
import { baselineKey, baselineOccurrenceKeys } from './baseline-key.mjs';
|
|
15
|
+
export { baselineKey, baselineOccurrenceKeys };
|
|
16
16
|
|
|
17
17
|
export function readBaseline(root, baselinePath) {
|
|
18
18
|
const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
|
|
@@ -23,7 +23,7 @@ export function readBaseline(root, baselinePath) {
|
|
|
23
23
|
|
|
24
24
|
export function writeBaseline(root, baselinePath, violations) {
|
|
25
25
|
const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
|
|
26
|
-
const keys =
|
|
26
|
+
const keys = baselineOccurrenceKeys(violations).sort();
|
|
27
27
|
fs.writeFileSync(
|
|
28
28
|
fullPath,
|
|
29
29
|
`${JSON.stringify({ version: 1, note: 'Frozen ark-check violations. Only NEW violations fail --baseline runs. Regenerate with: ark-check --update-baseline', violations: keys }, null, 2)}\n`
|
package/dist/index.cjs
CHANGED
|
@@ -80,7 +80,7 @@ __export(index_exports, {
|
|
|
80
80
|
module.exports = __toCommonJS(index_exports);
|
|
81
81
|
|
|
82
82
|
// src/version.ts
|
|
83
|
-
var version = "2.
|
|
83
|
+
var version = "2.11.0";
|
|
84
84
|
|
|
85
85
|
// src/kernel/intent/IntentRegistry.ts
|
|
86
86
|
var IntentRegistry = class {
|
|
@@ -2255,11 +2255,82 @@ function extractModuleSpecifiers(source) {
|
|
|
2255
2255
|
let match;
|
|
2256
2256
|
while ((match = pattern.re.exec(source)) !== null) {
|
|
2257
2257
|
const index = match.index + match[0].indexOf(match[1]);
|
|
2258
|
-
|
|
2258
|
+
const raw = match[0];
|
|
2259
|
+
const typeOnly = pattern.kind === "import" && /\bimport\s+type\b/.test(raw) || pattern.kind === "export" && /\bexport\s+type\b/.test(raw);
|
|
2260
|
+
matches.push({ value: match[1], index, kind: pattern.kind, typeOnly });
|
|
2259
2261
|
}
|
|
2260
2262
|
}
|
|
2261
2263
|
return matches.sort((a, b) => a.index - b.index);
|
|
2262
2264
|
}
|
|
2265
|
+
function extractModuleSpecifiersAst(ts, source) {
|
|
2266
|
+
const sourceFile = ts.createSourceFile("generated.ts", source, ts.ScriptTarget.Latest, true);
|
|
2267
|
+
const matches = [];
|
|
2268
|
+
const push = (node, value, kind, typeOnly = false) => {
|
|
2269
|
+
matches.push({
|
|
2270
|
+
value,
|
|
2271
|
+
index: node.getStart(sourceFile),
|
|
2272
|
+
kind,
|
|
2273
|
+
typeOnly
|
|
2274
|
+
});
|
|
2275
|
+
};
|
|
2276
|
+
const visit = (node) => {
|
|
2277
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
|
2278
|
+
const clause = node.importClause;
|
|
2279
|
+
const namedBindings = clause?.namedBindings;
|
|
2280
|
+
const specifiersOnly = clause && !clause.name && namedBindings && ts.isNamedImports(namedBindings) && namedBindings.elements.length > 0 && namedBindings.elements.every((element) => element.isTypeOnly === true);
|
|
2281
|
+
push(
|
|
2282
|
+
node.moduleSpecifier,
|
|
2283
|
+
node.moduleSpecifier.text,
|
|
2284
|
+
"import",
|
|
2285
|
+
Boolean(clause?.isTypeOnly || specifiersOnly)
|
|
2286
|
+
);
|
|
2287
|
+
} else if (ts.isExportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
|
2288
|
+
const clause = node.exportClause;
|
|
2289
|
+
const specifiersOnly = clause && ts.isNamedExports(clause) && clause.elements.length > 0 && clause.elements.every((element) => element.isTypeOnly === true);
|
|
2290
|
+
push(
|
|
2291
|
+
node.moduleSpecifier,
|
|
2292
|
+
node.moduleSpecifier.text,
|
|
2293
|
+
"export",
|
|
2294
|
+
Boolean(node.isTypeOnly || specifiersOnly)
|
|
2295
|
+
);
|
|
2296
|
+
} else if (ts.isCallExpression(node) && node.arguments.length === 1) {
|
|
2297
|
+
const argument = node.arguments[0];
|
|
2298
|
+
const value = tsStringLiteralText(ts, argument);
|
|
2299
|
+
if (value !== void 0 && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
2300
|
+
push(argument, value, "dynamic-import");
|
|
2301
|
+
} else if (value !== void 0 && ts.isIdentifier(node.expression) && node.expression.text === "require") {
|
|
2302
|
+
push(argument, value, "require");
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
ts.forEachChild(node, visit);
|
|
2306
|
+
};
|
|
2307
|
+
visit(sourceFile);
|
|
2308
|
+
return matches.sort((a, b) => a.index - b.index);
|
|
2309
|
+
}
|
|
2310
|
+
function nonLiteralDynamicImportLines(ts, source) {
|
|
2311
|
+
const sourceFile = ts.createSourceFile("generated.ts", source, ts.ScriptTarget.Latest, true);
|
|
2312
|
+
const lines = [];
|
|
2313
|
+
const visit = (node) => {
|
|
2314
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && (!node.arguments[0] || !ts.isStringLiteralLike(node.arguments[0]))) {
|
|
2315
|
+
lines.push(sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
|
|
2316
|
+
}
|
|
2317
|
+
ts.forEachChild(node, visit);
|
|
2318
|
+
};
|
|
2319
|
+
visit(sourceFile);
|
|
2320
|
+
return lines;
|
|
2321
|
+
}
|
|
2322
|
+
function extractQuotedStringsAst(ts, source) {
|
|
2323
|
+
const sourceFile = ts.createSourceFile("generated.ts", source, ts.ScriptTarget.Latest, true);
|
|
2324
|
+
const matches = [];
|
|
2325
|
+
const visit = (node) => {
|
|
2326
|
+
if (ts.isStringLiteralLike(node)) {
|
|
2327
|
+
matches.push({ value: node.text, index: node.getStart(sourceFile) });
|
|
2328
|
+
}
|
|
2329
|
+
ts.forEachChild(node, visit);
|
|
2330
|
+
};
|
|
2331
|
+
visit(sourceFile);
|
|
2332
|
+
return matches;
|
|
2333
|
+
}
|
|
2263
2334
|
function looksLikeIntentName(s) {
|
|
2264
2335
|
return /^(Domain|Application|Adapter|Workflow|Job|Presentation|Reporting|Metadata|Security|Audit|Observability|Kernel)\.[A-Za-z0-9_.]+$/.test(s);
|
|
2265
2336
|
}
|
|
@@ -2454,6 +2525,19 @@ function createAICodeGate(options = {}) {
|
|
|
2454
2525
|
const gateContext = context;
|
|
2455
2526
|
const filePath = gateContext?.filePath;
|
|
2456
2527
|
const contextLayer = gateContext?.layer;
|
|
2528
|
+
const moduleSpecifiers = options.typescript ? extractModuleSpecifiersAst(options.typescript, source) : extractModuleSpecifiers(source);
|
|
2529
|
+
const quotedStrings = options.typescript ? extractQuotedStringsAst(options.typescript, source) : extractQuotedStrings(source);
|
|
2530
|
+
if (options.typescript && !options.allowNonLiteralDynamicImport?.(filePath)) {
|
|
2531
|
+
for (const line of nonLiteralDynamicImportLines(options.typescript, source)) {
|
|
2532
|
+
violations.push(
|
|
2533
|
+
violation(
|
|
2534
|
+
"DYNAMIC_IMPORT_NOT_ALLOWLISTED",
|
|
2535
|
+
"Non-literal dynamic import cannot be resolved statically; add the reviewed file to dynamicImportAllowlist.",
|
|
2536
|
+
{ line, filePath }
|
|
2537
|
+
)
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2457
2541
|
const exemptFromInfraHeuristics = contextLayer !== void 0 && (explicitInfraLayers.has(contextLayer) || layerHasInfrastructureRole(contextLayer));
|
|
2458
2542
|
const infraLayerEscapeHatch = contextLayer !== void 0 ? ` If "${contextLayer}" is an infrastructure layer, mark it in ark.config.json with "mayImportInfrastructure": true (or name it with an infra token like Adapters/Persistence/Repository).` : "";
|
|
2459
2543
|
for (const pat of userForbidden) {
|
|
@@ -2479,7 +2563,7 @@ function createAICodeGate(options = {}) {
|
|
|
2479
2563
|
);
|
|
2480
2564
|
}
|
|
2481
2565
|
}
|
|
2482
|
-
for (const specifier of
|
|
2566
|
+
for (const specifier of moduleSpecifiers) {
|
|
2483
2567
|
const targetHit = options.resolveImportTarget?.(specifier.value, filePath) ?? (options.resolveImportLayer ? { layer: options.resolveImportLayer(specifier.value, filePath) } : void 0);
|
|
2484
2568
|
const sourceHit = typeof filePath === "string" ? options.resolveImportTarget?.(filePath) ?? (options.resolveImportLayer ? { layer: contextLayer, relPath: void 0 } : void 0) : void 0;
|
|
2485
2569
|
const targetLayer = targetHit?.layer;
|
|
@@ -2495,6 +2579,9 @@ function createAICodeGate(options = {}) {
|
|
|
2495
2579
|
}
|
|
2496
2580
|
);
|
|
2497
2581
|
if (blocked) {
|
|
2582
|
+
if (specifier.typeOnly && !blocked.peerIsolation) {
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2498
2585
|
const peer = Boolean(blocked.peerIsolation);
|
|
2499
2586
|
violations.push(
|
|
2500
2587
|
violation(
|
|
@@ -2508,7 +2595,11 @@ function createAICodeGate(options = {}) {
|
|
|
2508
2595
|
fromLayer: contextLayer,
|
|
2509
2596
|
toLayer: targetLayer,
|
|
2510
2597
|
suggestion: peer ? "Extract shared code to a shared layer, or coordinate slices via events/ports \u2014 do not import across feature/context slices." : "Depend on a port/interface owned by an inner layer instead, or move this code to a layer allowed to make this import.",
|
|
2511
|
-
details: {
|
|
2598
|
+
details: {
|
|
2599
|
+
importKind: specifier.kind,
|
|
2600
|
+
peerIsolation: peer,
|
|
2601
|
+
...specifier.typeOnly ? { typeOnly: true } : {}
|
|
2602
|
+
}
|
|
2512
2603
|
}
|
|
2513
2604
|
)
|
|
2514
2605
|
);
|
|
@@ -2518,7 +2609,7 @@ function createAICodeGate(options = {}) {
|
|
|
2518
2609
|
continue;
|
|
2519
2610
|
}
|
|
2520
2611
|
}
|
|
2521
|
-
if (exemptFromInfraHeuristics) continue;
|
|
2612
|
+
if (exemptFromInfraHeuristics || specifier.typeOnly) continue;
|
|
2522
2613
|
if (!hasInfrastructureToken(specifier.value) && !isKnownInfrastructurePackage(specifier.value)) {
|
|
2523
2614
|
continue;
|
|
2524
2615
|
}
|
|
@@ -2563,7 +2654,7 @@ function createAICodeGate(options = {}) {
|
|
|
2563
2654
|
}
|
|
2564
2655
|
}
|
|
2565
2656
|
if (enforceAllowlist && intentNames.size > 0) {
|
|
2566
|
-
for (const literal of
|
|
2657
|
+
for (const literal of quotedStrings) {
|
|
2567
2658
|
if (looksLikeIntentName(literal.value) && !intentNames.has(literal.value)) {
|
|
2568
2659
|
violations.push(
|
|
2569
2660
|
violation(
|
|
@@ -2581,7 +2672,7 @@ function createAICodeGate(options = {}) {
|
|
|
2581
2672
|
}
|
|
2582
2673
|
}
|
|
2583
2674
|
if (options.architectureProfile && contextLayer) {
|
|
2584
|
-
for (const literal of
|
|
2675
|
+
for (const literal of quotedStrings) {
|
|
2585
2676
|
if (!looksLikeIntentName(literal.value)) continue;
|
|
2586
2677
|
const targetLayer = options.architectureProfile.resolveLayer(literal.value);
|
|
2587
2678
|
if (!targetLayer) continue;
|
|
@@ -2857,15 +2948,18 @@ function sleep(ms) {
|
|
|
2857
2948
|
});
|
|
2858
2949
|
}
|
|
2859
2950
|
async function withTimeout(operation, timeoutMs, stepName) {
|
|
2860
|
-
|
|
2951
|
+
const controller = new AbortController();
|
|
2952
|
+
if (timeoutMs === void 0) return operation(controller.signal);
|
|
2861
2953
|
let timeout;
|
|
2862
2954
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2863
2955
|
timeout = setTimeout(() => {
|
|
2864
|
-
|
|
2956
|
+
const error = new Error(`Workflow step "${stepName}" timed out after ${timeoutMs}ms.`);
|
|
2957
|
+
controller.abort(error);
|
|
2958
|
+
reject(error);
|
|
2865
2959
|
}, timeoutMs);
|
|
2866
2960
|
});
|
|
2867
2961
|
try {
|
|
2868
|
-
return await Promise.race([operation, timeoutPromise]);
|
|
2962
|
+
return await Promise.race([operation(controller.signal), timeoutPromise]);
|
|
2869
2963
|
} finally {
|
|
2870
2964
|
if (timeout) clearTimeout(timeout);
|
|
2871
2965
|
}
|
|
@@ -2900,6 +2994,15 @@ var WorkflowEngineImpl = class {
|
|
|
2900
2994
|
if (this.definitions.has(definition.name)) {
|
|
2901
2995
|
throw new Error(`Workflow "${definition.name}" is already registered.`);
|
|
2902
2996
|
}
|
|
2997
|
+
const names = /* @__PURE__ */ new Set();
|
|
2998
|
+
for (const step of definition.steps) {
|
|
2999
|
+
if (names.has(step.name)) {
|
|
3000
|
+
throw new Error(
|
|
3001
|
+
`Workflow "${definition.name}" has duplicate step name "${step.name}".`
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
names.add(step.name);
|
|
3005
|
+
}
|
|
2903
3006
|
this.definitions.set(definition.name, definition);
|
|
2904
3007
|
if (definition.startOn) {
|
|
2905
3008
|
const trigger = definition.startOn;
|
|
@@ -2940,6 +3043,7 @@ var WorkflowEngineImpl = class {
|
|
|
2940
3043
|
} catch (err) {
|
|
2941
3044
|
await this.compensate(snapshot, definition.steps, err);
|
|
2942
3045
|
snapshot.status = "failed";
|
|
3046
|
+
snapshot.currentStep = void 0;
|
|
2943
3047
|
snapshot.error = errorMessage(err);
|
|
2944
3048
|
snapshot.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2945
3049
|
await this.store.save(snapshot);
|
|
@@ -2968,7 +3072,7 @@ var WorkflowEngineImpl = class {
|
|
|
2968
3072
|
await this.store.save(snapshot);
|
|
2969
3073
|
try {
|
|
2970
3074
|
const result = await withTimeout(
|
|
2971
|
-
Promise.resolve(step.execute(snapshot.context, this.bus)),
|
|
3075
|
+
(signal) => Promise.resolve(step.execute(snapshot.context, this.bus, signal)),
|
|
2972
3076
|
step.timeoutMs,
|
|
2973
3077
|
step.name
|
|
2974
3078
|
);
|