arkgate 3.7.0 → 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.
- package/CHANGELOG.md +76 -1145
- package/README.md +59 -19
- package/bin/ark-check-runtime.mjs +1598 -0
- package/bin/ark-check.mjs +32 -1565
- package/bin/ark-mcp-runtime.mjs +1976 -0
- package/bin/ark-mcp.mjs +84 -1495
- package/bin/ark-shared.mjs +34 -38
- package/bin/ark.mjs +33 -66
- package/bin/lib/adapter-contract.mjs +161 -9
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/analysis-completeness.mjs +28 -0
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/analysis-policy.mjs +27 -0
- package/bin/lib/architecture-scan.mjs +70 -357
- package/bin/lib/auto-patch.mjs +76 -8
- package/bin/lib/ci-and-commands.mjs +1 -1
- package/bin/lib/codex-home.mjs +43 -16
- package/bin/lib/design-delta.mjs +4 -0
- package/bin/lib/doctor-advisories.mjs +4 -3
- package/bin/lib/doctor-plan.mjs +40 -41
- package/bin/lib/enforcement-state.mjs +2 -0
- package/bin/lib/github-enforcement.mjs +443 -0
- package/bin/lib/hook-templates.mjs +12 -148
- package/bin/lib/html-report-advisories.mjs +1 -1
- package/bin/lib/html-report-depth.mjs +9 -0
- package/bin/lib/html-report.mjs +5 -5
- package/bin/lib/install-migrate.mjs +83 -79
- package/bin/lib/managed-upgrade.mjs +622 -0
- package/bin/lib/mcp-adoption.mjs +3 -1
- package/bin/lib/parse-health.mjs +6 -5
- package/bin/lib/port-proof.mjs +2 -2
- package/bin/lib/prepare-change.mjs +68 -38
- package/bin/lib/prepare-write.mjs +7 -1
- package/bin/lib/resident-doctor-client.mjs +55 -0
- package/bin/lib/resident-hook.mjs +247 -0
- package/bin/lib/resolved-candidate-facts.mjs +1160 -0
- package/bin/lib/scan-files.mjs +19 -6
- package/bin/lib/snippet-analysis.mjs +119 -0
- package/bin/lib/source-policy.mjs +24 -0
- package/bin/lib/typescript-host.mjs +15 -18
- package/bin/lib/unavailable-analysis.mjs +76 -0
- package/bin/lib/upgrade-command.mjs +115 -0
- package/bin/lib/weakest-link.mjs +21 -179
- package/bin/lib/write-path-capabilities.mjs +167 -16
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +3 -0
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +7 -7
- package/dist/index.d.ts +1073 -141
- package/dist/index.js +7 -7
- package/docs/agent-guide.md +103 -59
- package/docs/ai-gates.md +97 -16
- package/docs/demos/01-write-gate-self-correction.md +2 -2
- package/docs/enthusiast/README.md +10 -10
- package/docs/enthusiast/how-to-gallery-starter.md +2 -2
- package/docs/enthusiast/reference-commands.md +18 -1
- package/docs/enthusiast/tutorial-first-project.md +2 -2
- package/docs/package-surface.md +98 -12
- package/docs/typescript-support.md +109 -37
- package/package.json +32 -4
- package/schemas/ark.analysis-result.schema.json +159 -2
- package/schemas/ark.design-delta.schema.json +1 -0
- package/schemas/ark.enforcement-state.schema.json +84 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
- package/server.json +2 -2
- package/templates/skills/ark-explore.md +5 -5
- package/templates/skills/ark-fix.md +1 -1
- package/templates/skills/ark-runtime.md +15 -8
- package/templates/skills/ark-upgrade.md +122 -182
- package/bin/lib/ai-velocity.mjs +0 -293
- package/bin/lib/graph-cycles.mjs +0 -6
- package/bin/lib/safety-diagnostics.mjs +0 -284
- package/bin/lib/ts-resolve.mjs +0 -228
- package/dist/configTypes-DAPvBqK6.d.cts +0 -61
- package/dist/eslint/index.d.cts +0 -146
- package/dist/index.d.cts +0 -986
|
@@ -2,373 +2,86 @@
|
|
|
2
2
|
* Architecture check pipeline: content scan → import graph → layer edges → cycles.
|
|
3
3
|
* Extracted from ark-check entry (R3). Entry remains orchestration + presentation.
|
|
4
4
|
*/
|
|
5
|
-
import fs from 'node:fs';
|
|
6
5
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
layerForFile,
|
|
9
|
-
looksLikeIntent,
|
|
10
|
-
} from '../ark-shared.mjs';
|
|
11
|
-
import {
|
|
12
|
-
isArkPublishCandidate,
|
|
13
|
-
isPublishCall,
|
|
14
|
-
lineOf,
|
|
15
|
-
namedModuleBindings,
|
|
16
|
-
objectHasProperty,
|
|
17
|
-
publishHasSource,
|
|
18
|
-
publishSourceLiteral,
|
|
19
|
-
sourceFileExportsOnlyTypes,
|
|
20
|
-
sourceFileHasTopLevelSideEffects,
|
|
21
|
-
stringLiteralText,
|
|
22
|
-
typeOnlyExportNames,
|
|
23
|
-
} from './ast-scan.mjs';
|
|
24
|
-
import { provePortProofInject } from './port-proof.mjs';
|
|
25
6
|
import { summarizeParseHealth } from './parse-health.mjs';
|
|
26
7
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
isBlocked,
|
|
30
|
-
collectConfigWarnings,
|
|
31
|
-
} from './config-warnings.mjs';
|
|
32
|
-
import {
|
|
33
|
-
ambientCoveredByForbiddenGlobals,
|
|
34
|
-
collectCapabilityUses,
|
|
35
|
-
collectForbiddenCapabilityUses,
|
|
36
|
-
effectiveCapabilityDeny,
|
|
37
|
-
evaluateArchitectureGraph,
|
|
38
|
-
extractSemanticDependencies,
|
|
39
|
-
forbiddenGlobalForModuleSpecifier,
|
|
8
|
+
analyzeTrustedResolvedProject,
|
|
9
|
+
loadContract,
|
|
40
10
|
} from './analysis-engine.mjs';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const layerConfig = config.layers.find((layer) => layer.name === sourceLayer);
|
|
63
|
-
const forbiddenGlobals = Array.isArray(layerConfig?.forbiddenGlobals)
|
|
64
|
-
? layerConfig.forbiddenGlobals.filter((entry) => typeof entry === 'string')
|
|
65
|
-
: [];
|
|
66
|
-
for (const use of collectForbiddenCapabilityUses(ts, sourceFile, forbiddenGlobals)) {
|
|
67
|
-
violations.push({
|
|
68
|
-
ruleId: 'FORBIDDEN_GLOBAL',
|
|
69
|
-
file: normalize(path.relative(root, file)),
|
|
70
|
-
line: use.line,
|
|
71
|
-
fromLayer: sourceLayer,
|
|
72
|
-
target: use.name,
|
|
73
|
-
message: `${sourceLayer} must not use the ambient global "${use.name}".`,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// U04 — opted-in capability walls (ADR 0009). One violation, one voice: an
|
|
78
|
-
// ambient use already covered by this layer's forbiddenGlobals reports only
|
|
79
|
-
// FORBIDDEN_GLOBAL (D7 dedup); absence of the surface adds nothing.
|
|
80
|
-
const capabilityDeny = new Set(effectiveCapabilityDeny(layerConfig ?? {}));
|
|
81
|
-
if (capabilityDeny.size > 0) {
|
|
82
|
-
for (const use of collectCapabilityUses(ts, sourceFile)) {
|
|
83
|
-
if (!capabilityDeny.has(use.capability)) continue;
|
|
84
|
-
if (
|
|
85
|
-
(use.source === 'ambient-global' &&
|
|
86
|
-
ambientCoveredByForbiddenGlobals(use.symbol, forbiddenGlobals)) ||
|
|
87
|
-
(use.source === 'import-based' &&
|
|
88
|
-
forbiddenGlobalForModuleSpecifier(use.symbol, forbiddenGlobals))
|
|
89
|
-
) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
violations.push({
|
|
93
|
-
ruleId: 'CAPABILITY_VIOLATION',
|
|
94
|
-
file: normalize(path.relative(root, file)),
|
|
95
|
-
line: use.line,
|
|
96
|
-
fromLayer: sourceLayer,
|
|
97
|
-
target: use.symbol,
|
|
98
|
-
capability: use.capability,
|
|
99
|
-
message:
|
|
100
|
-
use.source === 'import-based'
|
|
101
|
-
? `${sourceLayer} denies the ${use.capability} capability; found import of "${use.symbol}".`
|
|
102
|
-
: `${sourceLayer} denies the ${use.capability} capability; found ambient "${use.symbol}".`,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const checkModuleEdge = (specifier, node, kind, typeOnly = false) => {
|
|
108
|
-
const namedBindings = namedModuleBindings(ts, node);
|
|
109
|
-
edges.push({
|
|
110
|
-
specifier,
|
|
111
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
112
|
-
kind,
|
|
113
|
-
typeOnly,
|
|
114
|
-
...(namedBindings ? { namedBindings } : {}),
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
for (const dependency of extractSemanticDependencies(ts, sourceFile)) {
|
|
119
|
-
if (!dependency.specifier) continue;
|
|
120
|
-
checkModuleEdge(
|
|
121
|
-
dependency.specifier,
|
|
122
|
-
dependency.node,
|
|
123
|
-
dependency.kind,
|
|
124
|
-
dependency.typeOnly
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const needsPolicyWalk =
|
|
129
|
-
/\bpublish\s*\(|\bintent\b/.test(source) ||
|
|
130
|
-
/['"`]\s*[A-Z][A-Za-z0-9_]*\./.test(source);
|
|
131
|
-
const visit = (node) => {
|
|
132
|
-
if (ts.isCallExpression(node)) {
|
|
133
|
-
if (isPublishCall(ts, node)) {
|
|
134
|
-
const firstArg = node.arguments[0];
|
|
135
|
-
const rawIntent = stringLiteralText(ts, firstArg);
|
|
136
|
-
for (const finding of classifyPublishFacts({
|
|
137
|
-
publishCall: true,
|
|
138
|
-
rawIntentName: rawIntent,
|
|
139
|
-
objectHasIntent: objectHasProperty(ts, firstArg, 'intent'),
|
|
140
|
-
arkPublishCandidate: isArkPublishCandidate(ts, node),
|
|
141
|
-
hasSource: publishHasSource(ts, node),
|
|
142
|
-
})) {
|
|
143
|
-
violations.push({
|
|
144
|
-
ruleId: finding.ruleId,
|
|
145
|
-
file: normalize(path.relative(root, file)),
|
|
146
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
147
|
-
...(finding.ruleId === 'PUBLISH_MISSING_SOURCE' ? { fromLayer: sourceLayer } : {}),
|
|
148
|
-
message: finding.message,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const sourceIntent = publishSourceLiteral(ts, node);
|
|
153
|
-
if (sourceIntent && looksLikeIntent(sourceIntent)) {
|
|
154
|
-
const sourceIntentLayer = layerForIntent(
|
|
155
|
-
sourceIntent,
|
|
156
|
-
config.layers,
|
|
157
|
-
manifestIntentLayers
|
|
158
|
-
);
|
|
159
|
-
if (sourceIntentLayer && sourceIntentLayer !== sourceLayer) {
|
|
160
|
-
violations.push({
|
|
161
|
-
ruleId: 'PUBLISH_SOURCE_LAYER_MISMATCH',
|
|
162
|
-
file: normalize(path.relative(root, file)),
|
|
163
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
164
|
-
fromLayer: sourceLayer,
|
|
165
|
-
toLayer: sourceIntentLayer,
|
|
166
|
-
target: sourceIntent,
|
|
167
|
-
message:
|
|
168
|
-
`Publish source "${sourceIntent}" resolves to ${sourceIntentLayer}, but the publishing file is classified as ${sourceLayer}.`,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (ts.isStringLiteralLike(node) && looksLikeIntent(node.text)) {
|
|
176
|
-
const targetLayer = layerForIntent(node.text, config.layers, manifestIntentLayers);
|
|
177
|
-
const rule = targetLayer ? isBlocked(rules, sourceLayer, targetLayer) : undefined;
|
|
178
|
-
if (rule) {
|
|
179
|
-
violations.push({
|
|
180
|
-
ruleId: 'LAYER_INTENT_REFERENCE_VIOLATION',
|
|
181
|
-
file: normalize(path.relative(root, file)),
|
|
182
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
183
|
-
fromLayer: sourceLayer,
|
|
184
|
-
toLayer: targetLayer,
|
|
185
|
-
target: node.text,
|
|
186
|
-
message:
|
|
187
|
-
rule.message ??
|
|
188
|
-
`${sourceLayer} must not reference ${targetLayer} intent ${node.text}.`,
|
|
189
|
-
});
|
|
11
|
+
import { effectiveAnalysisConfig } from './analysis-policy.mjs';
|
|
12
|
+
import { resolveCandidateFacts } from './resolved-candidate-facts.mjs';
|
|
13
|
+
|
|
14
|
+
/** Resolve canonical facts and optionally retain filesystem probes for resident invalidation. */
|
|
15
|
+
export function resolveArchitectureSnapshot({
|
|
16
|
+
root,
|
|
17
|
+
config,
|
|
18
|
+
manifest,
|
|
19
|
+
rules,
|
|
20
|
+
files,
|
|
21
|
+
ts,
|
|
22
|
+
args,
|
|
23
|
+
captureInputs = true,
|
|
24
|
+
}) {
|
|
25
|
+
const observedInputs = captureInputs ? new Map() : undefined;
|
|
26
|
+
const observeInput = observedInputs
|
|
27
|
+
? (inputPath, kind) => {
|
|
28
|
+
const absolute = path.resolve(inputPath);
|
|
29
|
+
const kinds = observedInputs.get(absolute) ?? new Set();
|
|
30
|
+
kinds.add(kind);
|
|
31
|
+
observedInputs.set(absolute, kinds);
|
|
190
32
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
33
|
+
: undefined;
|
|
34
|
+
const configPath = args?.config
|
|
35
|
+
? path.resolve(root, args.config)
|
|
36
|
+
: path.join(root, 'ark.config.json');
|
|
37
|
+
observeInput?.(configPath, 'ark-config');
|
|
38
|
+
if (args?.manifest) observeInput?.(path.resolve(root, args.manifest), 'manifest');
|
|
39
|
+
const effectiveConfig = effectiveAnalysisConfig(
|
|
40
|
+
{ ...config, rules: rules ?? config.rules },
|
|
41
|
+
manifest
|
|
42
|
+
);
|
|
43
|
+
const facts = resolveCandidateFacts({
|
|
44
|
+
root,
|
|
45
|
+
config: effectiveConfig,
|
|
46
|
+
ts,
|
|
47
|
+
...(args?.tsconfig ? { tsconfig: args.tsconfig } : {}),
|
|
48
|
+
observeInput,
|
|
49
|
+
});
|
|
50
|
+
const loadedContract = loadContract(effectiveConfig, configPath);
|
|
51
|
+
const analyzed = analyzeTrustedResolvedProject({ contract: loadedContract, facts });
|
|
52
|
+
const parseHealth = summarizeParseHealth(
|
|
53
|
+
facts.files.map((file) => ({
|
|
54
|
+
relFile: file.path,
|
|
55
|
+
entry: { parseDiagnosticCount: file.parseDiagnosticCount },
|
|
56
|
+
}))
|
|
57
|
+
);
|
|
58
|
+
const result = {
|
|
59
|
+
violations: analyzed.ir.violations,
|
|
60
|
+
warnings: analyzed.ir.warnings,
|
|
61
|
+
safety: analyzed.safety,
|
|
62
|
+
parseHealth,
|
|
63
|
+
completeness: analyzed.completeness,
|
|
64
|
+
completenessReasons: analyzed.completenessReasons,
|
|
65
|
+
valid: analyzed.valid,
|
|
66
|
+
strictValid: analyzed.strictValid,
|
|
67
|
+
mode: analyzed.mode,
|
|
68
|
+
policyHash: analyzed.policyHash,
|
|
69
|
+
resolverIdentity: analyzed.resolverIdentity,
|
|
70
|
+
factsHash: analyzed.factsHash,
|
|
71
|
+
candidateTreeHash: analyzed.candidateTreeHash,
|
|
203
72
|
};
|
|
73
|
+
const inputs = observedInputs
|
|
74
|
+
? [...observedInputs]
|
|
75
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
76
|
+
.map(([inputPath, kinds]) => ({ path: inputPath, kinds: [...kinds].sort() }))
|
|
77
|
+
: [];
|
|
78
|
+
return { facts, result, inputs };
|
|
204
79
|
}
|
|
205
80
|
|
|
206
81
|
/**
|
|
207
82
|
* Full architecture scan for governed files.
|
|
208
83
|
* @returns {{ violations: object[], warnings: object[] }}
|
|
209
84
|
*/
|
|
210
|
-
export function runArchitectureScan(
|
|
211
|
-
|
|
212
|
-
const compilerOptionsFor = createCompilerOptionsLookup(ts, root, args.tsconfig);
|
|
213
|
-
const moduleHost = createModuleResolutionHost(ts);
|
|
214
|
-
|
|
215
|
-
const warnings = collectConfigWarnings(root, config, files, rules, manifest);
|
|
216
|
-
const safety = collectSafetyDiagnostics(ts, root, config, files);
|
|
217
|
-
warnings.push(...safety.warnings);
|
|
218
|
-
const cacheKey = args.noCache ? undefined : scanCacheKey(root, args, ts.version);
|
|
219
|
-
const cachedFiles = cacheKey ? loadScanCache(root, cacheKey) : undefined;
|
|
220
|
-
const nextCacheFiles = {};
|
|
221
|
-
|
|
222
|
-
const scanned = [];
|
|
223
|
-
for (const file of files) {
|
|
224
|
-
const sourceLayer = layerForFile(root, file, config.layers);
|
|
225
|
-
if (!sourceLayer) continue;
|
|
226
|
-
const relFile = normalize(path.relative(root, file));
|
|
227
|
-
const stat = fs.statSync(file);
|
|
228
|
-
const fileKey = `${stat.mtimeMs}:${stat.size}`;
|
|
229
|
-
const cached = cachedFiles?.[relFile];
|
|
230
|
-
const entry =
|
|
231
|
-
cached &&
|
|
232
|
-
cached.fileKey === fileKey &&
|
|
233
|
-
Number.isSafeInteger(cached.parseDiagnosticCount) &&
|
|
234
|
-
cached.parseDiagnosticCount >= 0
|
|
235
|
-
? cached
|
|
236
|
-
: {
|
|
237
|
-
fileKey,
|
|
238
|
-
...scanSourceFile(
|
|
239
|
-
ts,
|
|
240
|
-
root,
|
|
241
|
-
config,
|
|
242
|
-
rules,
|
|
243
|
-
manifestIntentLayers,
|
|
244
|
-
file,
|
|
245
|
-
sourceLayer
|
|
246
|
-
),
|
|
247
|
-
};
|
|
248
|
-
nextCacheFiles[relFile] = entry;
|
|
249
|
-
scanned.push({ file, sourceLayer, relFile, entry });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const engineEdges = [];
|
|
253
|
-
// Y08 derives exact forbidden-global module duals from dependency facts
|
|
254
|
-
// already stored in every current cache entry. This stays downstream of the
|
|
255
|
-
// cache: no second AST scan, no cache-shape/version bump, and warm pre-Y08
|
|
256
|
-
// entries gain the verdict immediately.
|
|
257
|
-
const forbiddenModuleDualViolations = [];
|
|
258
|
-
const forbiddenModuleDualKeys = new Set();
|
|
259
|
-
for (const { file, sourceLayer, relFile, entry } of scanned) {
|
|
260
|
-
const layerConfig = config.layers.find((layer) => layer.name === sourceLayer);
|
|
261
|
-
const forbiddenGlobals = Array.isArray(layerConfig?.forbiddenGlobals)
|
|
262
|
-
? layerConfig.forbiddenGlobals.filter((item) => typeof item === 'string')
|
|
263
|
-
: [];
|
|
264
|
-
for (const edge of entry.edges) {
|
|
265
|
-
if (!edge.typeOnly) {
|
|
266
|
-
const forbiddenGlobal = forbiddenGlobalForModuleSpecifier(
|
|
267
|
-
edge.specifier,
|
|
268
|
-
forbiddenGlobals
|
|
269
|
-
);
|
|
270
|
-
if (forbiddenGlobal) {
|
|
271
|
-
forbiddenModuleDualKeys.add(`${relFile}\0${edge.specifier}`);
|
|
272
|
-
forbiddenModuleDualViolations.push({
|
|
273
|
-
ruleId: 'FORBIDDEN_GLOBAL',
|
|
274
|
-
file: relFile,
|
|
275
|
-
line: edge.line,
|
|
276
|
-
fromLayer: sourceLayer,
|
|
277
|
-
target: edge.specifier,
|
|
278
|
-
edgeKind: edge.kind,
|
|
279
|
-
message: `${sourceLayer} must not use module "${edge.specifier}" because it is the import form of forbidden global "${forbiddenGlobal}".`,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
const target = resolveImport(
|
|
284
|
-
ts,
|
|
285
|
-
edge.specifier,
|
|
286
|
-
file,
|
|
287
|
-
compilerOptionsFor(file),
|
|
288
|
-
moduleHost,
|
|
289
|
-
root
|
|
290
|
-
);
|
|
291
|
-
const targetLayer = target ? layerForFile(root, target, config.layers) : undefined;
|
|
292
|
-
const relTarget = target ? normalize(path.relative(root, target)) : undefined;
|
|
293
|
-
const targetCached = relTarget ? nextCacheFiles[relTarget] : undefined;
|
|
294
|
-
const staticEdge = edge.kind === 'import' || edge.kind === 'export';
|
|
295
|
-
const targetTypeOnlyExports =
|
|
296
|
-
staticEdge && Boolean(targetCached?.exportsOnlyTypes) && !edge.typeOnly;
|
|
297
|
-
const sourcePureTypeModule = Boolean(entry.exportsOnlyTypes);
|
|
298
|
-
const targetTypeNames = new Set(targetCached?.typeOnlyExportNames || []);
|
|
299
|
-
const named = edge.namedBindings;
|
|
300
|
-
const namedBindingsTypeOnly =
|
|
301
|
-
staticEdge &&
|
|
302
|
-
Array.isArray(named) &&
|
|
303
|
-
named.length > 0 &&
|
|
304
|
-
targetTypeNames.size > 0 &&
|
|
305
|
-
!targetCached?.hasTopLevelSideEffects &&
|
|
306
|
-
named.every((name) => targetTypeNames.has(name));
|
|
307
|
-
const deniedRule = targetLayer
|
|
308
|
-
? isBlocked(rules, sourceLayer, targetLayer, {
|
|
309
|
-
fromPath: relFile,
|
|
310
|
-
toPath: relTarget,
|
|
311
|
-
layers: config.layers,
|
|
312
|
-
})
|
|
313
|
-
: undefined;
|
|
314
|
-
let portProofEligible = false;
|
|
315
|
-
if (
|
|
316
|
-
deniedRule &&
|
|
317
|
-
!deniedRule.peerIsolation &&
|
|
318
|
-
!edge.typeOnly &&
|
|
319
|
-
edge.kind === 'import' &&
|
|
320
|
-
!targetTypeOnlyExports &&
|
|
321
|
-
!namedBindingsTypeOnly
|
|
322
|
-
) {
|
|
323
|
-
try {
|
|
324
|
-
const source = fs.readFileSync(file, 'utf8');
|
|
325
|
-
portProofEligible = Boolean(provePortProofInject(ts, source, { filePath: file }).eligible);
|
|
326
|
-
} catch {
|
|
327
|
-
portProofEligible = false;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
engineEdges.push({
|
|
331
|
-
from: relFile,
|
|
332
|
-
fromLayer: sourceLayer,
|
|
333
|
-
to: relTarget,
|
|
334
|
-
toLayer: targetLayer,
|
|
335
|
-
line: edge.line,
|
|
336
|
-
kind: edge.kind,
|
|
337
|
-
typeOnly: edge.typeOnly,
|
|
338
|
-
targetTypeOnlyExports,
|
|
339
|
-
sourcePureTypeModule,
|
|
340
|
-
namedBindingsTypeOnly,
|
|
341
|
-
portProofEligible,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (cacheKey) saveScanCache(root, cacheKey, nextCacheFiles);
|
|
347
|
-
|
|
348
|
-
// A cache written before Y08 can retain the overlapping capability-wall
|
|
349
|
-
// finding for this import. Filter by file+specifier (not line: D7 explicitly
|
|
350
|
-
// does not depend on cross-engine line anchors), then let FORBIDDEN_GLOBAL be
|
|
351
|
-
// the one declared-policy voice.
|
|
352
|
-
const contentViolations = scanned
|
|
353
|
-
.flatMap(({ entry }) => entry.contentViolations)
|
|
354
|
-
.filter(
|
|
355
|
-
(violation) =>
|
|
356
|
-
!(
|
|
357
|
-
violation.ruleId === 'CAPABILITY_VIOLATION' &&
|
|
358
|
-
forbiddenModuleDualKeys.has(`${violation.file}\0${violation.target}`)
|
|
359
|
-
)
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
...evaluateArchitectureGraph({
|
|
364
|
-
config,
|
|
365
|
-
rules,
|
|
366
|
-
files: scanned.map(({ relFile }) => relFile),
|
|
367
|
-
contentViolations: [...contentViolations, ...forbiddenModuleDualViolations],
|
|
368
|
-
edges: engineEdges,
|
|
369
|
-
warnings,
|
|
370
|
-
safety: safety.report,
|
|
371
|
-
}),
|
|
372
|
-
parseHealth: summarizeParseHealth(scanned),
|
|
373
|
-
};
|
|
85
|
+
export function runArchitectureScan(options) {
|
|
86
|
+
return resolveArchitectureSnapshot({ ...options, captureInputs: false }).result;
|
|
374
87
|
}
|
package/bin/lib/auto-patch.mjs
CHANGED
|
@@ -194,15 +194,35 @@ export function applyImportTypeAutoPatch(ts, source, opts = {}) {
|
|
|
194
194
|
* filePath?: string,
|
|
195
195
|
* root: string,
|
|
196
196
|
* ts: object,
|
|
197
|
-
* validate: (source: string) => { valid: boolean, violations?: any[] },
|
|
197
|
+
* validate: (source: string) => { valid: boolean, completeness?: string, violations?: any[] },
|
|
198
198
|
* resolveTargetAbs?: Function,
|
|
199
199
|
* }} opts
|
|
200
200
|
*/
|
|
201
201
|
export function validateWithAutoPatch(opts) {
|
|
202
202
|
const { source, filePath, root, ts, validate, resolveTargetAbs } = opts;
|
|
203
203
|
const result = validate(source);
|
|
204
|
+
const completeness = result.completeness === 'unavailable' ? 'unavailable' : 'partial';
|
|
205
|
+
const completenessReasons =
|
|
206
|
+
Array.isArray(result.completenessReasons) && result.completenessReasons.length > 0
|
|
207
|
+
? result.completenessReasons
|
|
208
|
+
: [
|
|
209
|
+
{
|
|
210
|
+
code:
|
|
211
|
+
completeness === 'unavailable'
|
|
212
|
+
? 'ANALYSIS_UNAVAILABLE'
|
|
213
|
+
: 'LEXICAL_EVIDENCE_INCOMPLETE',
|
|
214
|
+
message:
|
|
215
|
+
completeness === 'unavailable'
|
|
216
|
+
? 'Single-file analysis is unavailable.'
|
|
217
|
+
: 'Single-file validation cannot prove complete candidate resolver evidence.',
|
|
218
|
+
},
|
|
219
|
+
];
|
|
204
220
|
const base = {
|
|
205
|
-
|
|
221
|
+
mode: 'lexical-compatibility',
|
|
222
|
+
valid: false,
|
|
223
|
+
lexicalValid: Boolean(result.lexicalValid ?? result.valid),
|
|
224
|
+
completeness,
|
|
225
|
+
completenessReasons,
|
|
206
226
|
violations: Array.isArray(result.violations) ? result.violations : [],
|
|
207
227
|
};
|
|
208
228
|
|
|
@@ -229,8 +249,16 @@ export function validateWithAutoPatch(opts) {
|
|
|
229
249
|
};
|
|
230
250
|
});
|
|
231
251
|
|
|
232
|
-
if (base.
|
|
233
|
-
return {
|
|
252
|
+
if (base.lexicalValid) {
|
|
253
|
+
return {
|
|
254
|
+
mode: base.mode,
|
|
255
|
+
valid: false,
|
|
256
|
+
lexicalValid: true,
|
|
257
|
+
completeness: base.completeness,
|
|
258
|
+
completenessReasons: base.completenessReasons,
|
|
259
|
+
violations: [],
|
|
260
|
+
autoPatch: null,
|
|
261
|
+
};
|
|
234
262
|
}
|
|
235
263
|
|
|
236
264
|
// Write-path autoPatch: import-type mechanical-safe only (W1).
|
|
@@ -242,23 +270,63 @@ export function validateWithAutoPatch(opts) {
|
|
|
242
270
|
});
|
|
243
271
|
|
|
244
272
|
if (!attempt) {
|
|
245
|
-
return {
|
|
273
|
+
return {
|
|
274
|
+
mode: base.mode,
|
|
275
|
+
valid: false,
|
|
276
|
+
lexicalValid: false,
|
|
277
|
+
completeness: base.completeness,
|
|
278
|
+
completenessReasons: base.completenessReasons,
|
|
279
|
+
violations,
|
|
280
|
+
autoPatch: null,
|
|
281
|
+
};
|
|
246
282
|
}
|
|
247
283
|
|
|
248
284
|
const after = validate(attempt.source);
|
|
249
|
-
if (!after.valid) {
|
|
285
|
+
if (!(after.lexicalValid ?? after.valid)) {
|
|
250
286
|
// Discard — never return an unvalidated patch
|
|
251
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
mode: base.mode,
|
|
289
|
+
valid: false,
|
|
290
|
+
lexicalValid: false,
|
|
291
|
+
completeness: base.completeness,
|
|
292
|
+
completenessReasons: base.completenessReasons,
|
|
293
|
+
violations,
|
|
294
|
+
autoPatch: null,
|
|
295
|
+
};
|
|
252
296
|
}
|
|
253
297
|
|
|
298
|
+
const afterCompleteness = after.completeness === 'unavailable' ? 'unavailable' : 'partial';
|
|
299
|
+
const afterCompletenessReasons =
|
|
300
|
+
Array.isArray(after.completenessReasons) && after.completenessReasons.length > 0
|
|
301
|
+
? after.completenessReasons
|
|
302
|
+
: [
|
|
303
|
+
{
|
|
304
|
+
code:
|
|
305
|
+
afterCompleteness === 'unavailable'
|
|
306
|
+
? 'ANALYSIS_UNAVAILABLE'
|
|
307
|
+
: 'LEXICAL_EVIDENCE_INCOMPLETE',
|
|
308
|
+
message:
|
|
309
|
+
afterCompleteness === 'unavailable'
|
|
310
|
+
? 'Single-file analysis is unavailable.'
|
|
311
|
+
: 'Single-file validation cannot prove complete candidate resolver evidence.',
|
|
312
|
+
},
|
|
313
|
+
];
|
|
254
314
|
return {
|
|
315
|
+
mode: base.mode,
|
|
255
316
|
valid: false,
|
|
317
|
+
lexicalValid: false,
|
|
318
|
+
completeness: base.completeness,
|
|
319
|
+
completenessReasons: base.completenessReasons,
|
|
256
320
|
violations,
|
|
257
321
|
autoPatch: {
|
|
322
|
+
mode: 'lexical-compatibility',
|
|
258
323
|
source: attempt.source,
|
|
259
324
|
remediationKind: attempt.remediationKind,
|
|
260
325
|
confidence: attempt.confidence,
|
|
261
|
-
valid:
|
|
326
|
+
valid: false,
|
|
327
|
+
lexicalValid: true,
|
|
328
|
+
completeness: afterCompleteness,
|
|
329
|
+
completenessReasons: afterCompletenessReasons,
|
|
262
330
|
},
|
|
263
331
|
};
|
|
264
332
|
}
|
|
@@ -426,6 +426,6 @@ ${nodeSetup}
|
|
|
426
426
|
${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
|
|
427
427
|
env:
|
|
428
428
|
ARK_POLICY_BASE_REF: \${{ github.event.pull_request.base.sha || github.event.before }}
|
|
429
|
-
run: ${pm.run}
|
|
429
|
+
run: ${pm.run} --fail-on-new-smells --base-ref "\${{ github.event.pull_request.base.sha || github.event.before }}"
|
|
430
430
|
`;
|
|
431
431
|
}
|
package/bin/lib/codex-home.mjs
CHANGED
|
@@ -68,11 +68,32 @@ export function codexProjectSlug(absRoot) {
|
|
|
68
68
|
return `${base}_${hash}`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** Parse the one generated-style TOML args array from an MCP table body. */
|
|
72
|
+
export function extractCodexArgsFromBlock(block) {
|
|
73
|
+
if (!block || typeof block !== 'string') return null;
|
|
74
|
+
const matches = [
|
|
75
|
+
...block.matchAll(/^[ \t]*args[ \t]*=[ \t]*\[([^\]\r\n]*)\][ \t]*(?:#.*)?$/gm),
|
|
76
|
+
];
|
|
77
|
+
if (matches.length !== 1) return null;
|
|
78
|
+
const tokens = [...matches[0][1].matchAll(/"(?:\\.|[^"\\])*"|'[^']*'/g)];
|
|
79
|
+
const shape = matches[0][1].replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, '__ARK_STRING__');
|
|
80
|
+
if (!/^[ \t]*(?:__ARK_STRING__(?:[ \t]*,[ \t]*__ARK_STRING__)*[ \t]*,?)?[ \t]*$/.test(shape)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return tokens.map((match) =>
|
|
85
|
+
match[0].startsWith('"') ? JSON.parse(match[0]) : match[0].slice(1, -1)
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
/** Extract `--root` from one TOML mcp_servers table body. */
|
|
72
93
|
export function extractCodexRootFromBlock(block) {
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
return
|
|
94
|
+
const args = extractCodexArgsFromBlock(block);
|
|
95
|
+
const index = args?.indexOf('--root') ?? -1;
|
|
96
|
+
return index >= 0 && typeof args[index + 1] === 'string' ? args[index + 1] : null;
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
/**
|
|
@@ -82,20 +103,19 @@ export function extractCodexRootFromBlock(block) {
|
|
|
82
103
|
export function listCodexArkServerTables(tomlText) {
|
|
83
104
|
if (!tomlText || typeof tomlText !== 'string') return [];
|
|
84
105
|
const out = [];
|
|
85
|
-
const headerRe =
|
|
106
|
+
const headerRe =
|
|
107
|
+
/^[ \t]*\[[ \t]*(?:"mcp_servers"|'mcp_servers'|mcp_servers)[ \t]*\.[ \t]*(?:"(ark(?:_[a-zA-Z0-9_-]*)?)"|'(ark(?:_[a-zA-Z0-9_-]*)?)'|(ark(?:_[a-zA-Z0-9_-]*)?))[ \t]*\][ \t]*(?:#.*)?$/gm;
|
|
86
108
|
const headers = [];
|
|
87
109
|
let hm;
|
|
88
110
|
while ((hm = headerRe.exec(tomlText)) !== null) {
|
|
89
|
-
headers.push({ table: hm[1], index: hm.index });
|
|
111
|
+
headers.push({ table: hm[1] ?? hm[2] ?? hm[3], index: hm.index });
|
|
90
112
|
}
|
|
91
113
|
for (let i = 0; i < headers.length; i++) {
|
|
92
114
|
const start = headers[i].index;
|
|
93
|
-
let end =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (other >= 0) end = start + 1 + other;
|
|
98
|
-
}
|
|
115
|
+
let end = tomlText.length;
|
|
116
|
+
const rest = tomlText.slice(start + 1);
|
|
117
|
+
const other = rest.search(/\n(?=[ \t]*\[)/);
|
|
118
|
+
if (other >= 0) end = start + 1 + other;
|
|
99
119
|
const block = tomlText.slice(start, end).replace(/\s+$/, '\n');
|
|
100
120
|
out.push({
|
|
101
121
|
table: headers[i].table,
|
|
@@ -148,9 +168,14 @@ export function codexScopedTableForRoot(tomlText, absRoot) {
|
|
|
148
168
|
/** True when project TOML owns the primary Ark MCP binding for that project. */
|
|
149
169
|
export function codexProjectMcpIsValid(tomlText, projectRoot) {
|
|
150
170
|
const resolvedRoot = path.resolve(projectRoot);
|
|
171
|
+
if (listCodexArkServerTables(tomlText).filter((entry) => entry.table === 'ark').length !== 1) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
151
174
|
const primary = codexPrimaryTable(tomlText);
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
const args = extractCodexArgsFromBlock(primary?.block);
|
|
176
|
+
if (!primary?.root || !args?.some((value) => /^(ark|arkgate)-mcp$/.test(value))) return false;
|
|
177
|
+
const configIndex = args.indexOf('--config');
|
|
178
|
+
const config = configIndex >= 0 ? args[configIndex + 1] : null;
|
|
154
179
|
if (!config) return false;
|
|
155
180
|
try {
|
|
156
181
|
return (
|
|
@@ -170,7 +195,9 @@ export function extractCodexArkRootFromToml(tomlText) {
|
|
|
170
195
|
export function codexArkBlockHasPreferredBin(tomlText) {
|
|
171
196
|
const primary = codexPrimaryTable(tomlText);
|
|
172
197
|
if (!primary) return false;
|
|
173
|
-
const bins =
|
|
198
|
+
const bins = (extractCodexArgsFromBlock(primary.block) ?? []).filter((value) =>
|
|
199
|
+
/^(arkgate-mcp|ark-mcp)$/.test(value)
|
|
200
|
+
);
|
|
174
201
|
if (bins.length > 1) return false;
|
|
175
202
|
return bins.length === 1 && bins[0] === PREFERRED_CODEX_MCP_BIN;
|
|
176
203
|
}
|
|
@@ -180,7 +207,7 @@ export function codexArkBlockHasPreferredBin(tomlText) {
|
|
|
180
207
|
* Permanent different project roots are NOT broken — multi-project uses a secondary table.
|
|
181
208
|
*/
|
|
182
209
|
export function codexArkBlockNeedsRewrite(tomlText, absRoot) {
|
|
183
|
-
if (!tomlText
|
|
210
|
+
if (!codexPrimaryTable(tomlText)) return true;
|
|
184
211
|
const rootArg = extractCodexArkRootFromToml(tomlText);
|
|
185
212
|
if (!rootArg || isTempOrUpgradeRoot(rootArg)) return true;
|
|
186
213
|
try {
|
|
@@ -210,7 +237,7 @@ export function codexArkBlockNeedsRewrite(tomlText, absRoot) {
|
|
|
210
237
|
*/
|
|
211
238
|
export function assessCodexHomeMcp(tomlText, absRoot) {
|
|
212
239
|
const resolvedRoot = path.resolve(absRoot);
|
|
213
|
-
if (!tomlText
|
|
240
|
+
if (!codexPrimaryTable(tomlText)) {
|
|
214
241
|
return {
|
|
215
242
|
root: null,
|
|
216
243
|
tempPath: false,
|