arkgate 4.6.6 → 4.7.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 +139 -2
- package/README.md +22 -10
- package/SECURITY.md +1 -1
- package/bin/ark-check-runtime.mjs +21 -341
- package/bin/ark-mcp-runtime.mjs +71 -325
- package/bin/ark-shared.mjs +24 -158
- package/bin/lib/adapter-contract.mjs +17 -36
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/ark-run-doctor.mjs +144 -0
- package/bin/lib/ark-run-facts.mjs +472 -0
- package/bin/lib/ark-run-report.mjs +57 -0
- package/bin/lib/ark-run-sensors.mjs +309 -0
- package/bin/lib/check-args.mjs +173 -0
- package/bin/lib/check-config-detect.mjs +101 -0
- package/bin/lib/check-watch.mjs +80 -0
- package/bin/lib/config-contract.mjs +86 -11
- package/bin/lib/deep-module-coach.mjs +3 -0
- package/bin/lib/diagnostic-catalog.mjs +8 -0
- package/bin/lib/doctor-advisories.mjs +45 -8
- package/bin/lib/doctor-human.mjs +519 -0
- package/bin/lib/doctor-plan.mjs +62 -445
- package/bin/lib/extra-merge-teeth.mjs +187 -0
- package/bin/lib/github-enforcement.mjs +22 -9
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +22 -2
- package/bin/lib/html-report.mjs +40 -7
- package/bin/lib/mcp-hook-payload.mjs +328 -0
- package/bin/lib/package-manager.mjs +174 -0
- package/bin/lib/policy-delta-io.mjs +4 -0
- package/bin/lib/remediation.mjs +132 -0
- package/bin/lib/resolved-candidate-facts.mjs +67 -2
- package/bin/lib/rules-under-contract.mjs +37 -89
- package/bin/lib/snippet-analysis.mjs +43 -2
- package/bin/lib/status-command.mjs +28 -0
- package/bin/lib/status-manifest.mjs +23 -0
- package/bin/lib/team-parliament-io.mjs +4 -0
- package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
- package/dist/eslint/index.cjs +6 -2
- package/dist/eslint/index.d.ts +70 -2
- package/dist/eslint/index.js +6 -2
- package/dist/index.cjs +35 -35
- package/dist/index.d.ts +787 -272
- package/dist/index.js +35 -35
- package/docs/README.md +4 -3
- package/docs/agent-guide.md +21 -15
- package/docs/ai-gates.md +13 -0
- package/docs/configuration.md +24 -11
- package/docs/develop.md +12 -3
- package/docs/diagnostics.md +75 -0
- package/docs/enthusiast/README.md +4 -3
- package/docs/package-surface.md +17 -13
- package/docs/product-voice.md +6 -3
- package/docs/threat-model.md +1 -1
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +41 -2
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +47 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
- package/templates/agent-skills/ark-place/SKILL.md +26 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
- package/templates/skills/ark-adopt.md +23 -2
- package/templates/skills/ark-place.md +26 -2
- package/templates/skills/ark-runtime.md +66 -24
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/arkRunSensors.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/ark-run-sensors.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { extractArkRunImportedConstructorNamesFromSource, extractArkRunKernelCallsFromSource, extractArkRunManagedNewsFromSource, extractArkRunValueImportDependenciesFromSource, isArkRunKernelModuleSpecifier, isArkRunTransportBypassSpecifier, } from './ark-run-facts.mjs';
|
|
12
|
+
import { extraMergeTeethAllowed, } from './extra-merge-teeth.mjs';
|
|
13
|
+
import { deterministicNextAction } from './remediation.mjs';
|
|
14
|
+
export const ARKRUN_TIER1_SENSOR_IDS = [
|
|
15
|
+
'arkrun-missing-root',
|
|
16
|
+
'arkrun-kernel-in-domain',
|
|
17
|
+
'arkrun-direct-new',
|
|
18
|
+
'arkrun-undeclared-emit',
|
|
19
|
+
'arkrun-undeclared-handle',
|
|
20
|
+
'arkrun-undeclared-depend',
|
|
21
|
+
'arkrun-transport-bypass',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Import / `new` envelope for `arkgate/eslint`.
|
|
25
|
+
* Missing-root and undeclared-* stay CLI/MCP/preflight (project-wide or declaration facts).
|
|
26
|
+
*/
|
|
27
|
+
export const ARKRUN_EDITOR_SENSOR_IDS = [
|
|
28
|
+
'arkrun-kernel-in-domain',
|
|
29
|
+
'arkrun-direct-new',
|
|
30
|
+
'arkrun-transport-bypass',
|
|
31
|
+
];
|
|
32
|
+
const EDITOR_SENSOR_SET = new Set(ARKRUN_EDITOR_SENSOR_IDS);
|
|
33
|
+
export function isArkRunEditorSensor(sensor) {
|
|
34
|
+
return EDITOR_SENSOR_SET.has(sensor);
|
|
35
|
+
}
|
|
36
|
+
export const ARKRUN_RULE_IDS = {
|
|
37
|
+
'arkrun-missing-root': 'ARKRUN_MISSING_ROOT',
|
|
38
|
+
'arkrun-kernel-in-domain': 'ARKRUN_KERNEL_IN_DOMAIN',
|
|
39
|
+
'arkrun-direct-new': 'ARKRUN_DIRECT_NEW',
|
|
40
|
+
'arkrun-undeclared-emit': 'ARKRUN_UNDECLARED_EMIT',
|
|
41
|
+
'arkrun-undeclared-handle': 'ARKRUN_UNDECLARED_HANDLE',
|
|
42
|
+
'arkrun-undeclared-depend': 'ARKRUN_UNDECLARED_DEPEND',
|
|
43
|
+
'arkrun-transport-bypass': 'ARKRUN_TRANSPORT_BYPASS',
|
|
44
|
+
};
|
|
45
|
+
export const ARKRUN_INTERACTION_NAME_INCOMPLETE = 'ARKRUN_INTERACTION_NAME_INCOMPLETE';
|
|
46
|
+
function isDomainRoleLayer(layer, intentPrefixes = []) {
|
|
47
|
+
const name = layer.trim();
|
|
48
|
+
// Start-anchored Domain/entity/aggregate — unanchored "model" matches ReportingReadModels.
|
|
49
|
+
if (/^domain(?:model)?$/i.test(name) || /^domain(?=[A-Z_\-\s])/i.test(name))
|
|
50
|
+
return true;
|
|
51
|
+
if (/^(?:entit(?:y|ies)|aggregates?)(?:$|(?=[A-Z_\-\s]))/i.test(name))
|
|
52
|
+
return true;
|
|
53
|
+
return intentPrefixes.some((prefix) => {
|
|
54
|
+
const normalized = prefix.trim().replace(/\.+$/, '');
|
|
55
|
+
return normalized === 'Domain' || normalized.startsWith('Domain.');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function compareFindings(left, right) {
|
|
59
|
+
return (left.file.localeCompare(right.file) ||
|
|
60
|
+
left.ruleId.localeCompare(right.ruleId) ||
|
|
61
|
+
left.line - right.line ||
|
|
62
|
+
left.message.localeCompare(right.message));
|
|
63
|
+
}
|
|
64
|
+
function finding(extra, sensor, file, line, message, extras, teethAllowed) {
|
|
65
|
+
const failsStrict = extra.mode === 'enforced' && teethAllowed;
|
|
66
|
+
return {
|
|
67
|
+
ruleId: ARKRUN_RULE_IDS[sensor],
|
|
68
|
+
sensor,
|
|
69
|
+
message,
|
|
70
|
+
file,
|
|
71
|
+
line,
|
|
72
|
+
...(extras?.fromLayer ? { fromLayer: extras.fromLayer } : {}),
|
|
73
|
+
...(extras?.target ? { target: extras.target } : {}),
|
|
74
|
+
severity: failsStrict ? 'error' : 'warning',
|
|
75
|
+
failsStrict,
|
|
76
|
+
nextAction: deterministicNextAction({
|
|
77
|
+
ruleId: ARKRUN_RULE_IDS[sensor],
|
|
78
|
+
fromLayer: extras?.fromLayer,
|
|
79
|
+
target: extras?.target,
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function bagForFile(declarations, file) {
|
|
84
|
+
const uses = [];
|
|
85
|
+
const reactsTo = [];
|
|
86
|
+
const raises = [];
|
|
87
|
+
const sends = [];
|
|
88
|
+
for (const entry of declarations) {
|
|
89
|
+
if (entry.file !== file)
|
|
90
|
+
continue;
|
|
91
|
+
uses.push(...entry.uses);
|
|
92
|
+
reactsTo.push(...entry.reactsTo);
|
|
93
|
+
raises.push(...entry.raises);
|
|
94
|
+
sends.push(...entry.sends);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
uses: new Set(uses),
|
|
98
|
+
reactsTo: new Set(reactsTo),
|
|
99
|
+
raises: new Set(raises),
|
|
100
|
+
sends: new Set(sends),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function emitKinds(kind) {
|
|
104
|
+
return kind === 'publisher' || kind === 'publish' || kind === 'raise' || kind === 'send';
|
|
105
|
+
}
|
|
106
|
+
function handleKinds(kind) {
|
|
107
|
+
return kind === 'subscribe' || kind === 'register-handler';
|
|
108
|
+
}
|
|
109
|
+
function dependKinds(kind) {
|
|
110
|
+
return kind === 'resolve' || kind === 'resolve-singleton';
|
|
111
|
+
}
|
|
112
|
+
function evaluateMissingRoot(extra, hits, teethAllowed) {
|
|
113
|
+
const out = [];
|
|
114
|
+
const roots = extra.compositionRoots;
|
|
115
|
+
if (roots.length === 0) {
|
|
116
|
+
out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, 'ArkRun compositionRoots is empty; no createArkKernel factory site is declared.', undefined, teethAllowed));
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const hitsByRoot = new Map();
|
|
120
|
+
for (const hit of hits) {
|
|
121
|
+
const list = hitsByRoot.get(hit.matchedRoot) ?? [];
|
|
122
|
+
list.push(hit);
|
|
123
|
+
hitsByRoot.set(hit.matchedRoot, list);
|
|
124
|
+
}
|
|
125
|
+
for (const pattern of roots) {
|
|
126
|
+
const matched = [...(hitsByRoot.get(pattern) ?? [])].sort((left, right) => left.file.localeCompare(right.file));
|
|
127
|
+
if (matched.length === 0) {
|
|
128
|
+
out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, `ArkRun composition root ${JSON.stringify(pattern)} matched no governed files and has no createArkKernel factory.`, { target: pattern }, teethAllowed));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Factory required in the root set, not in every glob hit.
|
|
132
|
+
if (matched.some((hit) => hit.hasKernelFactory))
|
|
133
|
+
continue;
|
|
134
|
+
const first = matched[0];
|
|
135
|
+
out.push(finding(extra, 'arkrun-missing-root', first.file, 1, `ArkRun composition root ${JSON.stringify(pattern)} has no createArkKernel / createStrictArkKernel factory.`, { target: pattern }, teethAllowed));
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function evaluateKernelInDomain(extra, layers, dependencies, layerForFile, teethAllowed) {
|
|
140
|
+
const prefixes = new Map(layers.map((layer) => [layer.name, layer.intentPrefixes ?? []]));
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const dependency of dependencies) {
|
|
143
|
+
const specifier = dependency.specifier;
|
|
144
|
+
if (!specifier || !isArkRunKernelModuleSpecifier(specifier))
|
|
145
|
+
continue;
|
|
146
|
+
const fromLayer = layerForFile(dependency.from);
|
|
147
|
+
if (!fromLayer)
|
|
148
|
+
continue;
|
|
149
|
+
if (!isDomainRoleLayer(fromLayer, prefixes.get(fromLayer) ?? []))
|
|
150
|
+
continue;
|
|
151
|
+
out.push(finding(extra, 'arkrun-kernel-in-domain', dependency.from, dependency.line, `${fromLayer} must not import kernel module ${JSON.stringify(specifier)}.`, { fromLayer, target: specifier }, teethAllowed));
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function evaluateDirectNew(extra, layers, managedNews, hits, layerForFile, teethAllowed) {
|
|
156
|
+
const managed = new Set(extra.managedLayers);
|
|
157
|
+
if (managed.size === 0)
|
|
158
|
+
return [];
|
|
159
|
+
const prefixes = new Map(layers.map((layer) => [layer.name, layer.intentPrefixes ?? []]));
|
|
160
|
+
const admittedFactories = new Set(hits.filter((hit) => hit.hasKernelFactory).map((hit) => hit.file));
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const constructed of managedNews) {
|
|
163
|
+
if (admittedFactories.has(constructed.file))
|
|
164
|
+
continue;
|
|
165
|
+
const fromLayer = layerForFile(constructed.file);
|
|
166
|
+
if (!fromLayer || !managed.has(fromLayer))
|
|
167
|
+
continue;
|
|
168
|
+
if (isDomainRoleLayer(fromLayer, prefixes.get(fromLayer) ?? []))
|
|
169
|
+
continue;
|
|
170
|
+
out.push(finding(extra, 'arkrun-direct-new', constructed.file, constructed.line, `${fromLayer} must not construct ${constructed.typeName} with new outside an ArkRun composition-root factory.`, { fromLayer, target: constructed.typeName }, teethAllowed));
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
function evaluateUndeclared(extra, kernelCalls, declarations, layerForFile, teethAllowed) {
|
|
175
|
+
const findings = [];
|
|
176
|
+
const completenessReasons = [];
|
|
177
|
+
if (extra.requireDeclarations !== true) {
|
|
178
|
+
return { findings, completenessReasons };
|
|
179
|
+
}
|
|
180
|
+
const managed = new Set(extra.managedLayers);
|
|
181
|
+
if (managed.size === 0)
|
|
182
|
+
return { findings, completenessReasons };
|
|
183
|
+
for (const call of kernelCalls) {
|
|
184
|
+
if (!emitKinds(call.kind) && !handleKinds(call.kind) && !dependKinds(call.kind))
|
|
185
|
+
continue;
|
|
186
|
+
const fromLayer = layerForFile(call.file);
|
|
187
|
+
if (!fromLayer || !managed.has(fromLayer))
|
|
188
|
+
continue;
|
|
189
|
+
if (!call.nameLiteral) {
|
|
190
|
+
if (extra.mode === 'enforced') {
|
|
191
|
+
completenessReasons.push({
|
|
192
|
+
code: ARKRUN_INTERACTION_NAME_INCOMPLETE,
|
|
193
|
+
file: call.file,
|
|
194
|
+
message: `ArkRun ${call.kind} call in ${call.file} has no string-literal name; enforced extra cannot prove the declaration.`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const bag = bagForFile(declarations, call.file);
|
|
200
|
+
if (emitKinds(call.kind)) {
|
|
201
|
+
if (bag.raises.has(call.nameLiteral) || bag.sends.has(call.nameLiteral))
|
|
202
|
+
continue;
|
|
203
|
+
findings.push(finding(extra, 'arkrun-undeclared-emit', call.file, call.line, `Emit ${JSON.stringify(call.nameLiteral)} is not declared in raises or sends.`, { fromLayer, target: call.nameLiteral }, teethAllowed));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (handleKinds(call.kind)) {
|
|
207
|
+
if (bag.reactsTo.has(call.nameLiteral))
|
|
208
|
+
continue;
|
|
209
|
+
findings.push(finding(extra, 'arkrun-undeclared-handle', call.file, call.line, `Handle ${JSON.stringify(call.nameLiteral)} is not declared in reactsTo.`, { fromLayer, target: call.nameLiteral }, teethAllowed));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (bag.uses.has(call.nameLiteral))
|
|
213
|
+
continue;
|
|
214
|
+
findings.push(finding(extra, 'arkrun-undeclared-depend', call.file, call.line, `Depend ${JSON.stringify(call.nameLiteral)} is not declared in uses.`, { fromLayer, target: call.nameLiteral }, teethAllowed));
|
|
215
|
+
}
|
|
216
|
+
return { findings, completenessReasons };
|
|
217
|
+
}
|
|
218
|
+
function evaluateTransportBypass(extra, dependencies, layerForFile, teethAllowed) {
|
|
219
|
+
const managed = new Set(extra.managedLayers);
|
|
220
|
+
if (managed.size === 0)
|
|
221
|
+
return [];
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const dependency of dependencies) {
|
|
224
|
+
if (dependency.typeOnly)
|
|
225
|
+
continue;
|
|
226
|
+
const specifier = dependency.specifier;
|
|
227
|
+
if (!specifier || !isArkRunTransportBypassSpecifier(specifier))
|
|
228
|
+
continue;
|
|
229
|
+
const fromLayer = layerForFile(dependency.from);
|
|
230
|
+
if (!fromLayer || !managed.has(fromLayer))
|
|
231
|
+
continue;
|
|
232
|
+
out.push(finding(extra, 'arkrun-transport-bypass', dependency.from, dependency.line, `${fromLayer} must not import broker/queue/emitter ${JSON.stringify(specifier)}; use the ArkRun kernel transport.`, { fromLayer, target: specifier }, teethAllowed));
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Evaluate closed tier-1 ArkRun sensors. Empty extra → no findings (silent).
|
|
238
|
+
*/
|
|
239
|
+
export function evaluateArkRunSensors(input) {
|
|
240
|
+
const extra = input.arkRun;
|
|
241
|
+
if (!extra)
|
|
242
|
+
return { findings: [], completenessReasons: [] };
|
|
243
|
+
const teethAllowed = extraMergeTeethAllowed(input.classification);
|
|
244
|
+
const undeclared = evaluateUndeclared(extra, input.kernelCalls, input.declarations, input.layerForFile, teethAllowed);
|
|
245
|
+
const findings = [
|
|
246
|
+
...evaluateMissingRoot(extra, input.compositionRootHits, teethAllowed),
|
|
247
|
+
...evaluateKernelInDomain(extra, input.layers, input.dependencies, input.layerForFile, teethAllowed),
|
|
248
|
+
...evaluateDirectNew(extra, input.layers, input.managedNews, input.compositionRootHits, input.layerForFile, teethAllowed),
|
|
249
|
+
...undeclared.findings,
|
|
250
|
+
...evaluateTransportBypass(extra, input.dependencies, input.layerForFile, teethAllowed),
|
|
251
|
+
].sort(compareFindings);
|
|
252
|
+
const completenessReasons = [...undeclared.completenessReasons].sort((left, right) => {
|
|
253
|
+
const leftKey = `${left.code}\0${left.file ?? ''}\0${left.message}`;
|
|
254
|
+
const rightKey = `${right.code}\0${right.file ?? ''}\0${right.message}`;
|
|
255
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
256
|
+
});
|
|
257
|
+
return { findings, completenessReasons };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Same sensors as `evaluateArkRunSensors`, filtered to the ESLint import/`new` envelope.
|
|
261
|
+
* Does not emit missing-root or undeclared-* (those need project-wide / declaration facts).
|
|
262
|
+
*/
|
|
263
|
+
export function evaluateArkRunEditorSensors(input) {
|
|
264
|
+
const result = evaluateArkRunSensors(input);
|
|
265
|
+
return {
|
|
266
|
+
findings: result.findings.filter((item) => isArkRunEditorSensor(item.sensor)),
|
|
267
|
+
completenessReasons: [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function fileMatchesCompositionRoot(pattern, file) {
|
|
271
|
+
if (pattern === file)
|
|
272
|
+
return true;
|
|
273
|
+
const star = pattern.indexOf('*');
|
|
274
|
+
if (star < 0)
|
|
275
|
+
return false;
|
|
276
|
+
const prefix = pattern.slice(0, star).replace(/\/$/, '');
|
|
277
|
+
return prefix.length > 0 && (file === prefix || file.startsWith(`${prefix}/`));
|
|
278
|
+
}
|
|
279
|
+
function compositionRootHitsForSource(extra, file, source) {
|
|
280
|
+
const hasFactory = extractArkRunKernelCallsFromSource(file, source).some((call) => call.kind === 'factory');
|
|
281
|
+
const hits = [];
|
|
282
|
+
for (const pattern of extra.compositionRoots) {
|
|
283
|
+
if (!fileMatchesCompositionRoot(pattern, file))
|
|
284
|
+
continue;
|
|
285
|
+
hits.push({ file, matchedRoot: pattern, hasKernelFactory: hasFactory });
|
|
286
|
+
}
|
|
287
|
+
return hits;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Import / `new` envelope from one proposed source (hook Write/Edit, snippet MCP).
|
|
291
|
+
* Missing-root and undeclared-* stay project-wide CLI/MCP/preflight.
|
|
292
|
+
*/
|
|
293
|
+
export function evaluateArkRunEditorSensorsFromSource(input) {
|
|
294
|
+
const extra = input.arkRun;
|
|
295
|
+
if (!extra)
|
|
296
|
+
return { findings: [], completenessReasons: [] };
|
|
297
|
+
const admitted = new Set(extractArkRunImportedConstructorNamesFromSource(input.source));
|
|
298
|
+
return evaluateArkRunEditorSensors({
|
|
299
|
+
arkRun: extra,
|
|
300
|
+
layers: input.layers,
|
|
301
|
+
kernelCalls: [],
|
|
302
|
+
managedNews: extractArkRunManagedNewsFromSource(input.file, input.source, admitted),
|
|
303
|
+
compositionRootHits: compositionRootHitsForSource(extra, input.file, input.source),
|
|
304
|
+
declarations: [],
|
|
305
|
+
dependencies: extractArkRunValueImportDependenciesFromSource(input.file, input.source),
|
|
306
|
+
layerForFile: input.layerForFile,
|
|
307
|
+
classification: input.classification,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ark-check CLI flag parsing.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { discoverLocalBaseRef, normalizePolicyBaseRef } from './policy-delta-io.mjs';
|
|
6
|
+
|
|
7
|
+
export function resolveDesignDeltaBaseRef(root, explicit, env = process.env) {
|
|
8
|
+
const flag = typeof explicit === 'string' ? explicit.trim() : '';
|
|
9
|
+
if (flag) return flag;
|
|
10
|
+
const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF);
|
|
11
|
+
if (envRef) return envRef;
|
|
12
|
+
const githubBase = typeof env.GITHUB_BASE_REF === 'string' ? env.GITHUB_BASE_REF.trim() : '';
|
|
13
|
+
if (githubBase) return `origin/${githubBase}`;
|
|
14
|
+
return discoverLocalBaseRef(root) || undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseArgs(argv) {
|
|
18
|
+
const args = {
|
|
19
|
+
root: process.cwd(),
|
|
20
|
+
config: 'ark.config.json',
|
|
21
|
+
manifest: undefined,
|
|
22
|
+
printConfig: undefined,
|
|
23
|
+
tsconfig: undefined,
|
|
24
|
+
json: false,
|
|
25
|
+
strictConfig: false,
|
|
26
|
+
strictMerge: false,
|
|
27
|
+
requireGates: false,
|
|
28
|
+
requireWriteHook: undefined,
|
|
29
|
+
init: false,
|
|
30
|
+
installAgentGates: false,
|
|
31
|
+
compact: false,
|
|
32
|
+
tools: undefined,
|
|
33
|
+
force: false,
|
|
34
|
+
skillsOnly: false,
|
|
35
|
+
baseline: undefined,
|
|
36
|
+
policyBase: undefined,
|
|
37
|
+
policyBaseRef: undefined,
|
|
38
|
+
policyAck: undefined, failOnNewSmells: false, baseRef: undefined,
|
|
39
|
+
contractSession: false,
|
|
40
|
+
contractDiff: false,
|
|
41
|
+
changed: false,
|
|
42
|
+
against: undefined,
|
|
43
|
+
base: undefined,
|
|
44
|
+
persona: undefined,
|
|
45
|
+
author: undefined,
|
|
46
|
+
failUngoverned: false,
|
|
47
|
+
updateBaseline: false,
|
|
48
|
+
noCache: false,
|
|
49
|
+
resident: false,
|
|
50
|
+
coverage: false,
|
|
51
|
+
migrateCommands: false,
|
|
52
|
+
doctor: false,
|
|
53
|
+
plan: false,
|
|
54
|
+
recommend: false,
|
|
55
|
+
writePlan: false,
|
|
56
|
+
listPolicyPacks: false,
|
|
57
|
+
applyPolicyPack: undefined,
|
|
58
|
+
watch: false,
|
|
59
|
+
beginner: false,
|
|
60
|
+
openReport: false,
|
|
61
|
+
noOpenReport: false,
|
|
62
|
+
version: false,
|
|
63
|
+
help: false,
|
|
64
|
+
all: false,
|
|
65
|
+
followConfigRoot: false,
|
|
66
|
+
};
|
|
67
|
+
const requireValue = (flag, index) => {
|
|
68
|
+
const value = argv[index + 1];
|
|
69
|
+
if (value === undefined || value.startsWith('-')) {
|
|
70
|
+
throw new Error(`Missing value for ${flag}. Run arkgate-check --help for usage.`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
};
|
|
74
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
75
|
+
const arg = argv[i];
|
|
76
|
+
if (arg === '--json') args.json = true;
|
|
77
|
+
else if (arg === '--strict' || arg === '--strict-merge') {
|
|
78
|
+
args.strictConfig = true;
|
|
79
|
+
args.requireGates = true;
|
|
80
|
+
args.strictMerge = true;
|
|
81
|
+
}
|
|
82
|
+
else if (arg === '--strict-config') args.strictConfig = true;
|
|
83
|
+
else if (arg === '--require-gates') {
|
|
84
|
+
args.requireGates = true;
|
|
85
|
+
args.strictConfig = true;
|
|
86
|
+
}
|
|
87
|
+
else if (arg === '--require-write-hook') {
|
|
88
|
+
args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
else if (arg === '--init') args.init = true;
|
|
91
|
+
else if (arg === '--preset') args.preset = requireValue(arg, i++);
|
|
92
|
+
else if (arg === '--install-agent-gates') args.installAgentGates = true;
|
|
93
|
+
else if (arg === '--compact') args.compact = true;
|
|
94
|
+
else if (arg === '--tools') {
|
|
95
|
+
// Consume the next arg only when it isn't another flag (same rule as --baseline),
|
|
96
|
+
// so `--tools --force` can't silently eat --force as a "tool name".
|
|
97
|
+
const next = argv[i + 1];
|
|
98
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
99
|
+
i += 1;
|
|
100
|
+
args.tools = next
|
|
101
|
+
.split(',')
|
|
102
|
+
.map((tool) => tool.trim().toLowerCase())
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
} else {
|
|
105
|
+
args.tools = []; // flag without a value — rejected in runInstallAgentGates
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (arg === '--force') args.force = true;
|
|
109
|
+
else if (arg === '--follow-config-root') args.followConfigRoot = true;
|
|
110
|
+
else if (arg === '--skills-only') args.skillsOnly = true;
|
|
111
|
+
else if (arg === '--coverage') args.coverage = true;
|
|
112
|
+
else if (arg === '--doctor') args.doctor = true;
|
|
113
|
+
else if (arg === '--plan') args.plan = true;
|
|
114
|
+
else if (arg === '--rules-inventory') args.rulesInventory = true;
|
|
115
|
+
else if (arg === '--recommend') args.recommend = true;
|
|
116
|
+
else if (arg === '--write-plan') args.writePlan = true;
|
|
117
|
+
else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
|
|
118
|
+
else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++);
|
|
119
|
+
else if (arg === '--suggest-include') args.suggestInclude = true;
|
|
120
|
+
else if (arg === '--adopt-contract') args.adoptContract = true;
|
|
121
|
+
else if (arg === '--migrate-contract') args.migrateContract = true;
|
|
122
|
+
else if (arg === '--ratchet-cores') args.ratchetCores = true;
|
|
123
|
+
else if (arg === '--write') args.write = true;
|
|
124
|
+
else if (arg === '--watch') args.watch = true;
|
|
125
|
+
else if (arg === '--beginner') args.beginner = true;
|
|
126
|
+
else if (arg === '--codex-home') args.codexHome = true;
|
|
127
|
+
else if (arg === '--claude-home') args.claudeHome = true;
|
|
128
|
+
else if (arg === '--grok-home') args.grokHome = true;
|
|
129
|
+
else if (arg === '--agent-homes') {
|
|
130
|
+
args.agentHomes = true;
|
|
131
|
+
args.codexHome = true;
|
|
132
|
+
args.claudeHome = true;
|
|
133
|
+
args.grokHome = true;
|
|
134
|
+
}
|
|
135
|
+
else if (arg === '--migrate-commands') args.migrateCommands = true;
|
|
136
|
+
else if (arg === '--no-cache') args.noCache = true;
|
|
137
|
+
else if (arg === '--resident') args.resident = true;
|
|
138
|
+
else if (arg === '--report') {
|
|
139
|
+
const next = argv[i + 1];
|
|
140
|
+
args.report = next && !next.startsWith('-') ? argv[++i] : 'ark-report.html';
|
|
141
|
+
}
|
|
142
|
+
else if (arg === '--reset-origin') args.resetOrigin = true;
|
|
143
|
+
else if (arg === '--no-archive') args.noArchive = true;
|
|
144
|
+
else if (arg === '--open') args.openReport = true;
|
|
145
|
+
else if (arg === '--no-open') args.noOpenReport = true;
|
|
146
|
+
else if (arg === '--baseline' || arg === '--update-baseline') {
|
|
147
|
+
if (arg === '--update-baseline') args.updateBaseline = true;
|
|
148
|
+
// optional path value: consume the next arg only when it isn't another flag
|
|
149
|
+
const next = argv[i + 1];
|
|
150
|
+
args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
|
|
151
|
+
}
|
|
152
|
+
else if (arg === '--policy-base') args.policyBase = requireValue(arg, i++);
|
|
153
|
+
else if (arg === '--policy-base-ref') args.policyBaseRef = requireValue(arg, i++);
|
|
154
|
+
else if (arg === '--policy-ack') args.policyAck = requireValue(arg, i++); else if (arg === '--fail-on-new-smells') args.failOnNewSmells = true; else if (arg === '--base-ref') args.baseRef = requireValue(arg, i++);
|
|
155
|
+
else if (arg === '--contract-session') args.contractSession = true;
|
|
156
|
+
else if (arg === '--contract-diff') args.contractDiff = true;
|
|
157
|
+
else if (arg === '--changed') args.changed = true;
|
|
158
|
+
else if (arg === '--against') args.against = requireValue(arg, i++);
|
|
159
|
+
else if (arg === '--base') args.base = requireValue(arg, i++);
|
|
160
|
+
else if (arg === '--persona') args.persona = requireValue(arg, i++);
|
|
161
|
+
else if (arg === '--author') args.author = requireValue(arg, i++);
|
|
162
|
+
else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++));
|
|
163
|
+
else if (arg === '--config') args.config = requireValue(arg, i++);
|
|
164
|
+
else if (arg === '--manifest') args.manifest = requireValue(arg, i++);
|
|
165
|
+
else if (arg === '--print-config') args.printConfig = requireValue(arg, i++);
|
|
166
|
+
else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++);
|
|
167
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
168
|
+
else if (arg === '--all') args.all = true;
|
|
169
|
+
else if (arg === '--version' || arg === '-V') args.version = true;
|
|
170
|
+
else throw new Error(`Unknown argument: ${arg}. Run arkgate-check --help for usage.`);
|
|
171
|
+
}
|
|
172
|
+
return args;
|
|
173
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convention-based ark.config detection used by --init.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
|
|
8
|
+
DEFAULT_INTENT_PREFIXES,
|
|
9
|
+
DEFAULT_LAYER_DIRECTORIES,
|
|
10
|
+
DEFAULT_RULES,
|
|
11
|
+
} from '../ark-shared.mjs';
|
|
12
|
+
import { normalize, walk } from './scan-files.mjs';
|
|
13
|
+
import { suggestLayerForDir } from './suggestions.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Infer an ark.config.json from the directories that actually exist in the project,
|
|
17
|
+
* using the same layer→directory conventions as the eleven-layer template. A directory
|
|
18
|
+
* only counts when it contains at least one source file, so an empty scaffold dir can't
|
|
19
|
+
* produce a layer whose pattern matches nothing (which --strict-config would fail).
|
|
20
|
+
*/
|
|
21
|
+
export function detectConfig(root) {
|
|
22
|
+
const srcDir = fs.existsSync(path.join(root, 'src')) ? 'src' : '.';
|
|
23
|
+
const layers = [];
|
|
24
|
+
|
|
25
|
+
for (const entry of DEFAULT_INTENT_PREFIXES) {
|
|
26
|
+
const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter(
|
|
27
|
+
(directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0
|
|
28
|
+
);
|
|
29
|
+
if (directories.length === 0) continue;
|
|
30
|
+
layers.push({
|
|
31
|
+
name: entry.layer,
|
|
32
|
+
patterns: directories.map((directory) => `${normalize(path.join(srcDir, directory))}/**`),
|
|
33
|
+
intentPrefixes: entry.prefixes,
|
|
34
|
+
...(entry.layer === 'DomainModel'
|
|
35
|
+
? { forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS }
|
|
36
|
+
: {}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const names = new Set(layers.map((layer) => layer.name));
|
|
41
|
+
const rules = DEFAULT_RULES.filter((rule) => names.has(rule.from) && names.has(rule.to));
|
|
42
|
+
|
|
43
|
+
return { srcDir, config: { include: [srcDir], layers, rules } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Top-level directories under srcDir not covered by any detected layer pattern. */
|
|
47
|
+
export function uncoveredDirectories(root, srcDir, layers) {
|
|
48
|
+
const base = path.join(root, srcDir);
|
|
49
|
+
if (!fs.existsSync(base)) return [];
|
|
50
|
+
return fs
|
|
51
|
+
.readdirSync(base, { withFileTypes: true })
|
|
52
|
+
.filter(
|
|
53
|
+
(entry) =>
|
|
54
|
+
entry.isDirectory() &&
|
|
55
|
+
entry.name !== 'node_modules' &&
|
|
56
|
+
entry.name !== 'dist' &&
|
|
57
|
+
!entry.name.startsWith('.')
|
|
58
|
+
)
|
|
59
|
+
.map((entry) => entry.name)
|
|
60
|
+
.filter((name) => {
|
|
61
|
+
const prefix = `${normalize(path.join(srcDir, name))}/`;
|
|
62
|
+
return !layers.some((layer) =>
|
|
63
|
+
layer.patterns.some((pattern) => pattern.startsWith(prefix))
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function proposeForUncovered(root, srcDir, layers) {
|
|
69
|
+
const proposals = [];
|
|
70
|
+
for (const top of uncoveredDirectories(root, srcDir, layers)) {
|
|
71
|
+
const direct = suggestLayerForDir(top);
|
|
72
|
+
if (direct) {
|
|
73
|
+
proposals.push({ dir: `${srcDir}/${top}`, ...direct });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let children = [];
|
|
77
|
+
try {
|
|
78
|
+
children = fs
|
|
79
|
+
.readdirSync(path.join(root, srcDir, top), { withFileTypes: true })
|
|
80
|
+
.filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
|
|
81
|
+
.map((e) => e.name);
|
|
82
|
+
} catch {
|
|
83
|
+
/* not a readable directory — treat as unrecognized below */
|
|
84
|
+
}
|
|
85
|
+
if (children.length > 0) {
|
|
86
|
+
// Descend: propose per child so a mixed `lib/` yields lib/repositories → Persistence
|
|
87
|
+
// AND flags lib/db as unrecognized, instead of dropping the parts Ark can't place.
|
|
88
|
+
for (const child of children) {
|
|
89
|
+
const hit = suggestLayerForDir(child);
|
|
90
|
+
proposals.push(
|
|
91
|
+
hit
|
|
92
|
+
? { dir: `${srcDir}/${top}/${child}`, ...hit }
|
|
93
|
+
: { dir: `${srcDir}/${top}/${child}`, unrecognized: true }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
proposals.push({ dir: `${srcDir}/${top}`, unrecognized: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return proposals;
|
|
101
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ark-check --watch loop (polling fallback when fs.watch is unavailable).
|
|
3
|
+
*/
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
function watchFingerprint(target) {
|
|
9
|
+
const pending = [target];
|
|
10
|
+
const entries = [];
|
|
11
|
+
while (pending.length > 0) {
|
|
12
|
+
const current = pending.pop();
|
|
13
|
+
let stat;
|
|
14
|
+
try {
|
|
15
|
+
stat = fs.statSync(current);
|
|
16
|
+
} catch {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
|
|
20
|
+
if (!stat.isDirectory()) continue;
|
|
21
|
+
try {
|
|
22
|
+
for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
|
|
23
|
+
} catch {
|
|
24
|
+
// A concurrent delete is represented by the next fingerprint.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return entries.sort().join('|');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function watchByPolling(target, onChange) {
|
|
31
|
+
let previous = watchFingerprint(target);
|
|
32
|
+
setInterval(() => {
|
|
33
|
+
const current = watchFingerprint(target);
|
|
34
|
+
if (current === previous) return;
|
|
35
|
+
previous = current;
|
|
36
|
+
onChange();
|
|
37
|
+
}, 250);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runWatchMode(args, { cliPath, loadConfig, dim }) {
|
|
41
|
+
const argv = process.argv.slice(2).filter((token) => token !== '--watch');
|
|
42
|
+
let debounce;
|
|
43
|
+
const rerun = () => {
|
|
44
|
+
clearTimeout(debounce);
|
|
45
|
+
debounce = setTimeout(() => {
|
|
46
|
+
const result = spawnSync(process.execPath, [cliPath, ...argv], {
|
|
47
|
+
cwd: args.root,
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
env: process.env,
|
|
50
|
+
});
|
|
51
|
+
process.exitCode = result.status ?? 1;
|
|
52
|
+
}, 300);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let config;
|
|
56
|
+
try {
|
|
57
|
+
config = loadConfig(args.root, args.config);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
60
|
+
process.exitCode = 2;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const entry of config.include ?? []) {
|
|
65
|
+
const target = path.join(args.root, entry);
|
|
66
|
+
if (!fs.existsSync(target)) continue;
|
|
67
|
+
try {
|
|
68
|
+
const watcher = fs.watch(target, { recursive: true }, rerun);
|
|
69
|
+
watcher.on('error', () => {
|
|
70
|
+
watcher.close();
|
|
71
|
+
watchByPolling(target, rerun);
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
watchByPolling(target, rerun);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(dim('Watching governed paths for changes… (Ctrl+C to stop)'));
|
|
79
|
+
await new Promise(() => {});
|
|
80
|
+
}
|