arkgate 3.6.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -1145
- package/README.md +59 -19
- package/bin/ark-check-runtime.mjs +1598 -0
- package/bin/ark-check.mjs +32 -1565
- package/bin/ark-layer-match.mjs +2 -1
- 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 -304
- 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/design-smells.mjs +67 -14
- package/bin/lib/doctor-advisories.mjs +23 -7
- package/bin/lib/doctor-plan.mjs +44 -47
- 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 +59 -0
- 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 +75 -0
- 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/reshape-decisions.mjs +284 -0
- 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 +127 -52
- package/docs/ai-gates.md +100 -18
- package/docs/configuration.md +6 -0
- 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 +101 -14
- package/docs/typescript-support.md +118 -37
- package/package.json +33 -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-autopilot.md +12 -0
- package/templates/skills/ark-explore.md +12 -5
- package/templates/skills/ark-fix.md +12 -2
- package/templates/skills/ark-loop.md +14 -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 -227
- package/dist/configTypes-DAPvBqK6.d.cts +0 -61
- package/dist/eslint/index.d.cts +0 -146
- package/dist/index.d.cts +0 -986
|
@@ -2,320 +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';
|
|
6
|
+
import { summarizeParseHealth } from './parse-health.mjs';
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
import {
|
|
26
|
-
intentLayersFromManifest,
|
|
27
|
-
layerForIntent,
|
|
28
|
-
isBlocked,
|
|
29
|
-
collectConfigWarnings,
|
|
30
|
-
} from './config-warnings.mjs';
|
|
31
|
-
import {
|
|
32
|
-
ambientCoveredByForbiddenGlobals,
|
|
33
|
-
collectCapabilityUses,
|
|
34
|
-
collectForbiddenCapabilityUses,
|
|
35
|
-
effectiveCapabilityDeny,
|
|
36
|
-
evaluateArchitectureGraph,
|
|
37
|
-
extractSemanticDependencies,
|
|
8
|
+
analyzeTrustedResolvedProject,
|
|
9
|
+
loadContract,
|
|
38
10
|
} from './analysis-engine.mjs';
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { classifyPublishFacts } from './source-policy.mjs';
|
|
42
|
-
import {
|
|
43
|
-
createCompilerOptionsLookup,
|
|
44
|
-
createModuleResolutionHost,
|
|
45
|
-
loadScanCache,
|
|
46
|
-
resolveImport,
|
|
47
|
-
saveScanCache,
|
|
48
|
-
scanCacheKey,
|
|
49
|
-
} from './ts-resolve.mjs';
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Parse one governed source file into content violations + module edges.
|
|
53
|
-
*/
|
|
54
|
-
export function scanSourceFile(ts, root, config, rules, manifestIntentLayers, file, sourceLayer) {
|
|
55
|
-
const source = fs.readFileSync(file, 'utf8');
|
|
56
|
-
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
57
|
-
const violations = [];
|
|
58
|
-
const edges = [];
|
|
59
|
-
|
|
60
|
-
const layerConfig = config.layers.find((layer) => layer.name === sourceLayer);
|
|
61
|
-
const forbiddenGlobals = Array.isArray(layerConfig?.forbiddenGlobals)
|
|
62
|
-
? layerConfig.forbiddenGlobals.filter((entry) => typeof entry === 'string')
|
|
63
|
-
: [];
|
|
64
|
-
for (const use of collectForbiddenCapabilityUses(ts, sourceFile, forbiddenGlobals)) {
|
|
65
|
-
violations.push({
|
|
66
|
-
ruleId: 'FORBIDDEN_GLOBAL',
|
|
67
|
-
file: normalize(path.relative(root, file)),
|
|
68
|
-
line: use.line,
|
|
69
|
-
fromLayer: sourceLayer,
|
|
70
|
-
target: use.name,
|
|
71
|
-
message: `${sourceLayer} must not use the ambient global "${use.name}".`,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// U04 — opted-in capability walls (ADR 0009). One violation, one voice: an
|
|
76
|
-
// ambient use already covered by this layer's forbiddenGlobals reports only
|
|
77
|
-
// FORBIDDEN_GLOBAL (D7 dedup); absence of the surface adds nothing.
|
|
78
|
-
const capabilityDeny = new Set(effectiveCapabilityDeny(layerConfig ?? {}));
|
|
79
|
-
if (capabilityDeny.size > 0) {
|
|
80
|
-
for (const use of collectCapabilityUses(ts, sourceFile)) {
|
|
81
|
-
if (!capabilityDeny.has(use.capability)) continue;
|
|
82
|
-
if (
|
|
83
|
-
use.source === 'ambient-global' &&
|
|
84
|
-
ambientCoveredByForbiddenGlobals(use.symbol, forbiddenGlobals)
|
|
85
|
-
) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
violations.push({
|
|
89
|
-
ruleId: 'CAPABILITY_VIOLATION',
|
|
90
|
-
file: normalize(path.relative(root, file)),
|
|
91
|
-
line: use.line,
|
|
92
|
-
fromLayer: sourceLayer,
|
|
93
|
-
target: use.symbol,
|
|
94
|
-
capability: use.capability,
|
|
95
|
-
message:
|
|
96
|
-
use.source === 'import-based'
|
|
97
|
-
? `${sourceLayer} denies the ${use.capability} capability; found import of "${use.symbol}".`
|
|
98
|
-
: `${sourceLayer} denies the ${use.capability} capability; found ambient "${use.symbol}".`,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const checkModuleEdge = (specifier, node, kind, typeOnly = false) => {
|
|
104
|
-
const namedBindings = namedModuleBindings(ts, node);
|
|
105
|
-
edges.push({
|
|
106
|
-
specifier,
|
|
107
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
108
|
-
kind,
|
|
109
|
-
typeOnly,
|
|
110
|
-
...(namedBindings ? { namedBindings } : {}),
|
|
111
|
-
});
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
for (const dependency of extractSemanticDependencies(ts, sourceFile)) {
|
|
115
|
-
if (!dependency.specifier) continue;
|
|
116
|
-
checkModuleEdge(
|
|
117
|
-
dependency.specifier,
|
|
118
|
-
dependency.node,
|
|
119
|
-
dependency.kind,
|
|
120
|
-
dependency.typeOnly
|
|
121
|
-
);
|
|
122
|
-
}
|
|
11
|
+
import { effectiveAnalysisConfig } from './analysis-policy.mjs';
|
|
12
|
+
import { resolveCandidateFacts } from './resolved-candidate-facts.mjs';
|
|
123
13
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
143
|
-
...(finding.ruleId === 'PUBLISH_MISSING_SOURCE' ? { fromLayer: sourceLayer } : {}),
|
|
144
|
-
message: finding.message,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const sourceIntent = publishSourceLiteral(ts, node);
|
|
149
|
-
if (sourceIntent && looksLikeIntent(sourceIntent)) {
|
|
150
|
-
const sourceIntentLayer = layerForIntent(
|
|
151
|
-
sourceIntent,
|
|
152
|
-
config.layers,
|
|
153
|
-
manifestIntentLayers
|
|
154
|
-
);
|
|
155
|
-
if (sourceIntentLayer && sourceIntentLayer !== sourceLayer) {
|
|
156
|
-
violations.push({
|
|
157
|
-
ruleId: 'PUBLISH_SOURCE_LAYER_MISMATCH',
|
|
158
|
-
file: normalize(path.relative(root, file)),
|
|
159
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
160
|
-
fromLayer: sourceLayer,
|
|
161
|
-
toLayer: sourceIntentLayer,
|
|
162
|
-
target: sourceIntent,
|
|
163
|
-
message:
|
|
164
|
-
`Publish source "${sourceIntent}" resolves to ${sourceIntentLayer}, but the publishing file is classified as ${sourceLayer}.`,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (ts.isStringLiteralLike(node) && looksLikeIntent(node.text)) {
|
|
172
|
-
const targetLayer = layerForIntent(node.text, config.layers, manifestIntentLayers);
|
|
173
|
-
const rule = targetLayer ? isBlocked(rules, sourceLayer, targetLayer) : undefined;
|
|
174
|
-
if (rule) {
|
|
175
|
-
violations.push({
|
|
176
|
-
ruleId: 'LAYER_INTENT_REFERENCE_VIOLATION',
|
|
177
|
-
file: normalize(path.relative(root, file)),
|
|
178
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
179
|
-
fromLayer: sourceLayer,
|
|
180
|
-
toLayer: targetLayer,
|
|
181
|
-
target: node.text,
|
|
182
|
-
message:
|
|
183
|
-
rule.message ??
|
|
184
|
-
`${sourceLayer} must not reference ${targetLayer} intent ${node.text}.`,
|
|
185
|
-
});
|
|
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);
|
|
186
32
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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,
|
|
198
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 };
|
|
199
79
|
}
|
|
200
80
|
|
|
201
81
|
/**
|
|
202
82
|
* Full architecture scan for governed files.
|
|
203
83
|
* @returns {{ violations: object[], warnings: object[] }}
|
|
204
84
|
*/
|
|
205
|
-
export function runArchitectureScan(
|
|
206
|
-
|
|
207
|
-
const compilerOptionsFor = createCompilerOptionsLookup(ts, root, args.tsconfig);
|
|
208
|
-
const moduleHost = createModuleResolutionHost(ts);
|
|
209
|
-
|
|
210
|
-
const warnings = collectConfigWarnings(root, config, files, rules, manifest);
|
|
211
|
-
const safety = collectSafetyDiagnostics(ts, root, config, files);
|
|
212
|
-
warnings.push(...safety.warnings);
|
|
213
|
-
const cacheKey = args.noCache ? undefined : scanCacheKey(root, args);
|
|
214
|
-
const cachedFiles = cacheKey ? loadScanCache(root, cacheKey) : undefined;
|
|
215
|
-
const nextCacheFiles = {};
|
|
216
|
-
|
|
217
|
-
const scanned = [];
|
|
218
|
-
for (const file of files) {
|
|
219
|
-
const sourceLayer = layerForFile(root, file, config.layers);
|
|
220
|
-
if (!sourceLayer) continue;
|
|
221
|
-
const relFile = normalize(path.relative(root, file));
|
|
222
|
-
const stat = fs.statSync(file);
|
|
223
|
-
const fileKey = `${stat.mtimeMs}:${stat.size}`;
|
|
224
|
-
const cached = cachedFiles?.[relFile];
|
|
225
|
-
const entry =
|
|
226
|
-
cached && cached.fileKey === fileKey
|
|
227
|
-
? cached
|
|
228
|
-
: {
|
|
229
|
-
fileKey,
|
|
230
|
-
...scanSourceFile(
|
|
231
|
-
ts,
|
|
232
|
-
root,
|
|
233
|
-
config,
|
|
234
|
-
rules,
|
|
235
|
-
manifestIntentLayers,
|
|
236
|
-
file,
|
|
237
|
-
sourceLayer
|
|
238
|
-
),
|
|
239
|
-
};
|
|
240
|
-
nextCacheFiles[relFile] = entry;
|
|
241
|
-
scanned.push({ file, sourceLayer, relFile, entry });
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const engineEdges = [];
|
|
245
|
-
for (const { file, sourceLayer, relFile, entry } of scanned) {
|
|
246
|
-
for (const edge of entry.edges) {
|
|
247
|
-
const target = resolveImport(
|
|
248
|
-
ts,
|
|
249
|
-
edge.specifier,
|
|
250
|
-
file,
|
|
251
|
-
compilerOptionsFor(file),
|
|
252
|
-
moduleHost,
|
|
253
|
-
root
|
|
254
|
-
);
|
|
255
|
-
const targetLayer = target ? layerForFile(root, target, config.layers) : undefined;
|
|
256
|
-
const relTarget = target ? normalize(path.relative(root, target)) : undefined;
|
|
257
|
-
const targetCached = relTarget ? nextCacheFiles[relTarget] : undefined;
|
|
258
|
-
const staticEdge = edge.kind === 'import' || edge.kind === 'export';
|
|
259
|
-
const targetTypeOnlyExports =
|
|
260
|
-
staticEdge && Boolean(targetCached?.exportsOnlyTypes) && !edge.typeOnly;
|
|
261
|
-
const sourcePureTypeModule = Boolean(entry.exportsOnlyTypes);
|
|
262
|
-
const targetTypeNames = new Set(targetCached?.typeOnlyExportNames || []);
|
|
263
|
-
const named = edge.namedBindings;
|
|
264
|
-
const namedBindingsTypeOnly =
|
|
265
|
-
staticEdge &&
|
|
266
|
-
Array.isArray(named) &&
|
|
267
|
-
named.length > 0 &&
|
|
268
|
-
targetTypeNames.size > 0 &&
|
|
269
|
-
!targetCached?.hasTopLevelSideEffects &&
|
|
270
|
-
named.every((name) => targetTypeNames.has(name));
|
|
271
|
-
const deniedRule = targetLayer
|
|
272
|
-
? isBlocked(rules, sourceLayer, targetLayer, {
|
|
273
|
-
fromPath: relFile,
|
|
274
|
-
toPath: relTarget,
|
|
275
|
-
layers: config.layers,
|
|
276
|
-
})
|
|
277
|
-
: undefined;
|
|
278
|
-
let portProofEligible = false;
|
|
279
|
-
if (
|
|
280
|
-
deniedRule &&
|
|
281
|
-
!deniedRule.peerIsolation &&
|
|
282
|
-
!edge.typeOnly &&
|
|
283
|
-
edge.kind === 'import' &&
|
|
284
|
-
!targetTypeOnlyExports &&
|
|
285
|
-
!namedBindingsTypeOnly
|
|
286
|
-
) {
|
|
287
|
-
try {
|
|
288
|
-
const source = fs.readFileSync(file, 'utf8');
|
|
289
|
-
portProofEligible = Boolean(provePortProofInject(ts, source, { filePath: file }).eligible);
|
|
290
|
-
} catch {
|
|
291
|
-
portProofEligible = false;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
engineEdges.push({
|
|
295
|
-
from: relFile,
|
|
296
|
-
fromLayer: sourceLayer,
|
|
297
|
-
to: relTarget,
|
|
298
|
-
toLayer: targetLayer,
|
|
299
|
-
line: edge.line,
|
|
300
|
-
kind: edge.kind,
|
|
301
|
-
typeOnly: edge.typeOnly,
|
|
302
|
-
targetTypeOnlyExports,
|
|
303
|
-
sourcePureTypeModule,
|
|
304
|
-
namedBindingsTypeOnly,
|
|
305
|
-
portProofEligible,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (cacheKey) saveScanCache(root, cacheKey, nextCacheFiles);
|
|
311
|
-
|
|
312
|
-
return evaluateArchitectureGraph({
|
|
313
|
-
config,
|
|
314
|
-
rules,
|
|
315
|
-
files: scanned.map(({ relFile }) => relFile),
|
|
316
|
-
contentViolations: scanned.flatMap(({ entry }) => entry.contentViolations),
|
|
317
|
-
edges: engineEdges,
|
|
318
|
-
warnings,
|
|
319
|
-
safety: safety.report,
|
|
320
|
-
});
|
|
85
|
+
export function runArchitectureScan(options) {
|
|
86
|
+
return resolveArchitectureSnapshot({ ...options, captureInputs: false }).result;
|
|
321
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,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Generated from design-delta.source.mjs — run npm run generate:packaged-tooling.
|
|
2
|
+
import{spawnSync as R}from"node:child_process";import M from"node:crypto";import N from"node:fs";import b from"node:path";import{layerForFile as E}from"../ark-shared.mjs";import{loadGoldenPattern as C}from"./golden-pattern.mjs";import{collectGovernedFiles as O}from"./scan-files.mjs";const K="1.0",A=Object.freeze(["domain-logic-in-ui"]),x=/\.[cm]?[jt]sx?$/i,j=/(?:^|\/)(?:components?|pages|hooks|ui|views|screens)(?:\/|$)|(?:^|\/)app\/(?!api\/)/i,q=/^(can|should|calculate|compute)[A-Z_]|policy/i,W=/^(can|should)[A-Z_]|policy/i,U=/^(calculate|compute)[A-Z_]/i,I=/(?:route|routing|path|label|className|style|render|display|view|modal|dialog|tooltip|navigate|navigation|href|tab|menu|component|toast|breadcrumb|sidebar|drawer|popover|layout|theme|icon)/i,z=/^(?:use[A-Z_]|render|navigate|redirect|push|replace|open|close|show|hide|setState|set[A-Z_]|toast|alert|confirm)/,B=new Set(["includes","some","every","has"]);function m(e){return String(e||"").replace(/\\/g,"/").replace(/^\.\//,"")}function w(e){return`sha256:${M.createHash("sha256").update(e,"utf8").digest("hex")}`}function k(e,t){const i=[...t].map(n=>[m(n.path),w(n.content)]).sort(([n],[r])=>n.localeCompare(r));return w(JSON.stringify({config:e,files:i}))}function H(e,t){const i=t.toLowerCase();return i.endsWith(".tsx")?e.ScriptKind.TSX:i.endsWith(".jsx")?e.ScriptKind.JSX:i.endsWith(".js")||i.endsWith(".mjs")||i.endsWith(".cjs")?e.ScriptKind.JS:e.ScriptKind.TS}function G(e,t,i){const n=E(e,i,t?.layers??[]);return j.test(i)||/presentation|ui|view/i.test(n??"")}function J(e,t){const i=[],n=r=>{if(e.isFunctionDeclaration(r)&&r.name&&r.body)i.push({name:r.name.text,body:r.body,node:r});else if(e.isVariableDeclaration(r)&&e.isIdentifier(r.name)){const s=r.initializer;s&&(e.isArrowFunction(s)||e.isFunctionExpression(s))&&i.push({name:r.name.text,body:s.body,node:r})}else e.isMethodDeclaration(r)&&r.name&&e.isIdentifier(r.name)&&r.body&&i.push({name:r.name.text,body:r.body,node:r});e.forEachChild(r,n)};return n(t),i}function V(e,t){if(!q.test(t.name)||I.test(t.name))return null;let i=0,n=0,r=0,s=0,o=!1,a=!1;const d=new Set([e.SyntaxKind.EqualsEqualsToken,e.SyntaxKind.EqualsEqualsEqualsToken,e.SyntaxKind.ExclamationEqualsToken,e.SyntaxKind.ExclamationEqualsEqualsToken,e.SyntaxKind.LessThanToken,e.SyntaxKind.LessThanEqualsToken,e.SyntaxKind.GreaterThanToken,e.SyntaxKind.GreaterThanEqualsToken,e.SyntaxKind.InKeyword,e.SyntaxKind.InstanceOfKeyword]),u=new Set([e.SyntaxKind.AmpersandAmpersandToken,e.SyntaxKind.BarBarToken,e.SyntaxKind.QuestionQuestionToken]),f=new Set([e.SyntaxKind.PlusToken,e.SyntaxKind.MinusToken,e.SyntaxKind.AsteriskToken,e.SyntaxKind.SlashToken,e.SyntaxKind.PercentToken,e.SyntaxKind.AsteriskAsteriskToken]),c=l=>{if(e.isJsxElement(l)||e.isJsxSelfClosingElement(l)||e.isJsxFragment(l)){a=!0;return}if(e.isBinaryExpression(l)&&(d.has(l.operatorToken.kind)&&(i+=1),u.has(l.operatorToken.kind)&&(n+=1),f.has(l.operatorToken.kind)&&(r+=1)),e.isCallExpression(l)){let S="";e.isIdentifier(l.expression)?S=l.expression.text:e.isPropertyAccessExpression(l.expression)&&(S=l.expression.name.text),B.has(S)&&(s+=1),(z.test(S)||I.test(S))&&(o=!0)}e.forEachChild(l,c)};if(c(t.body),a||o)return null;const p=i+n+s,g=r;let h,y;if(U.test(t.name)&&g>0)h="calculation-rule",y=g;else if(W.test(t.name)&&p>0)h="authorization-policy-rule",y=p;else return null;return{kind:h,magnitude:y,detail:`comparisons:${i};logical:${n};predicates:${s};arithmetic:${r}`}}function Z(e,t){return e?.present&&e.golden?.newCodeHome?`Move ${t} to ${e.golden.newCodeHome} following golden pattern "${e.golden.name}", then import the pure rule from the UI.`:`Move ${t} into the project's Domain/shared pure-rules home and import it from the UI; do not weaken ark.config.json.`}function v({root:e,config:t,records:i,ts:n,goldenPattern:r}){if(!n?.createSourceFile)throw new Error("TypeScript parser is required for design-delta analysis.");const s=[];for(const o of[...i].sort((a,d)=>m(a.path).localeCompare(m(d.path)))){const a=m(o.path);if(!x.test(a)||a.endsWith(".d.ts")||!G(e,t,a))continue;const d=n.createSourceFile(a,String(o.content),n.ScriptTarget.Latest,!0,H(n,a));for(const u of J(n,d)){const f=V(n,u);if(!f)continue;const c=`domain-logic-in-ui|${a}|${u.name}|${f.kind}`,p=d.getLineAndCharacterOfPosition(u.node.getStart(d)).line+1;s.push({smellId:"domain-logic-in-ui",fingerprint:w(c),identity:c,evidence:{kind:f.kind,path:a,line:p,symbol:u.name,detail:f.detail,magnitude:f.magnitude},repairHint:Z(r,u.name)})}}return s.sort((o,a)=>o.identity.localeCompare(a.identity))}function _({mode:e,baseIdentity:t,candidateIdentity:i,touchedPaths:n,baseFindings:r,candidateFindings:s}){const o=new Set([...n].map(m)),a=new Map(r.map(c=>[c.identity,c])),d=new Set(r),u=[];let f=0;for(const c of s){let p=a.get(c.identity);if(!p){const y=[...d].filter(l=>l.smellId===c.smellId&&l.evidence.symbol===c.evidence.symbol&&l.evidence.kind===c.evidence.kind);y.length===1&&([p]=y)}p&&d.delete(p);const g=p?.evidence?.magnitude??0,h=c.evidence.magnitude;if(!o.has(c.evidence.path)){p&&(f+=1);continue}p?h>g?u.push({...c,classification:"worsened",baseMagnitude:g,candidateMagnitude:h}):f+=1:u.push({...c,classification:"new",baseMagnitude:0,candidateMagnitude:h})}return{schemaVersion:K,mode:e,complete:!0,valid:u.length===0,base:t,candidate:i,supportedSmellIds:[...A],touchedPaths:[...o].sort(),changes:u,baseFindingCount:r.length,candidateFindingCount:s.length,historicalResidualCount:f}}function D(e,t){const i=[];for(const n of O(e,t)){const r=m(b.relative(e,n));!x.test(r)||r.endsWith(".d.ts")||i.push({path:r,content:N.readFileSync(n,"utf8")})}return i}function Q(e,t,i,n){const r=new Map(i.map(s=>[m(s.path),s.content]));for(const s of n){const o=m(s.path);!x.test(o)||o.endsWith(".d.ts")||!E(e,o,t?.layers??[])||(s.delete===!0?r.delete(o):typeof s.content=="string"&&r.set(o,s.content))}return[...r].map(([s,o])=>({path:s,content:o}))}function oe({root:e,config:t,changes:i,ts:n}){const r=D(e,t),s=Q(e,t,r,i??[]),o=C(e),a=v({root:e,config:t,records:r,ts:n,goldenPattern:o}),d=v({root:e,config:t,records:s,ts:n,goldenPattern:o});return _({mode:"write-candidate",baseIdentity:{kind:"candidate-tree",value:k(t,r)},candidateIdentity:{kind:"candidate-tree",value:k(t,s)},touchedPaths:(i??[]).map(u=>u.path),baseFindings:a,candidateFindings:d})}function T(e,t,i={}){return R("git",t,{cwd:e,encoding:i.encoding??"utf8",maxBuffer:64*1024*1024,input:i.input})}function P(e,t,i){const n=T(e,t);if(n.status!==0)throw new Error(`${i}: ${(n.stderr||n.stdout||"git command failed").trim()}`);return n.stdout.trim()}function $(e,t,i){const n=T(e,t,{encoding:"buffer"});if(n.status!==0)throw new Error(`${i}: ${String(n.stderr||n.stdout||"git command failed").trim()}`);return n.stdout.toString("utf8").split("\0").map(m).filter(Boolean)}function L(e,t,i){const n=T(e,["show",`${t}:${i}`]);if(n.status!==0)throw new Error(`base file unavailable (${i}): ${(n.stderr||"").trim()}`);return n.stdout}function F(e,t){return{schemaVersion:K,mode:"git-base",complete:!1,valid:!1,base:{kind:"git-tree",value:String(e||"<missing>")},candidate:{kind:"candidate-tree",value:"<unavailable>"},supportedSmellIds:[...A],touchedPaths:[],changes:[],baseFindingCount:0,candidateFindingCount:0,historicalResidualCount:0,error:t}}function X({root:e,config:t,configPath:i="ark.config.json",baseRef:n,ts:r}){if(typeof n!="string"||!n.trim())return F(n,"--fail-on-new-smells requires --base-ref <git-ref>.");try{if(n.startsWith("-"))throw new Error('base ref must not start with "-".');const s=P(e,["rev-parse","--verify",`${n}^{commit}`],"base ref is unresolvable"),o=P(e,["rev-parse","--verify",`${s}^{tree}`],"base tree is unresolvable"),a=m(b.isAbsolute(i)?b.relative(e,i):i);if(!a||a.startsWith("../"))throw new Error("base config path must be inside the project root.");const d=JSON.parse(L(e,s,a)),f=$(e,["ls-tree","-r","--name-only","-z",s],"base tree listing failed").filter(l=>x.test(l)&&!l.endsWith(".d.ts")).filter(l=>E(e,l,d?.layers??[])).map(l=>({path:l,content:L(e,s,l)})),c=D(e,t),p=[...$(e,["diff","--name-only","-z",s,"--"],"candidate diff failed"),...$(e,["ls-files","--others","--exclude-standard","-z"],"untracked-file scan failed")],g=C(e),h=v({root:e,config:d,records:f,ts:r,goldenPattern:g}),y=v({root:e,config:t,records:c,ts:r,goldenPattern:g});return _({mode:"git-base",baseIdentity:{kind:"git-tree",value:o,commit:s},candidateIdentity:{kind:"candidate-tree",value:k(t,c)},touchedPaths:p,baseFindings:h,candidateFindings:y})}catch(s){return F(n,s instanceof Error?s.message:String(s))}}function Y(e){return e?.complete?e.valid?`Design delta passed: 0 new/worsened supported smells across ${e.touchedPaths.length} touched path(s).`:[`Design delta blocked ${e.changes.length} new/worsened supported smell(s):`,...e.changes.map(t=>`- [${t.smellId}] ${t.evidence.path}:${t.evidence.line??1} ${t.evidence.symbol??""} (${t.classification})
|
|
3
|
+
Next action: ${t.repairHint}`)].join(`
|
|
4
|
+
`):`Design delta unavailable: ${e?.error||"unknown error"}`}function le({enabled:e,...t}){const i=e?X(t):null;return{result:i,combineEdges:({activeViolationCount:n,strictConfig:r,strictWarningCount:s,policyValid:o})=>{const a=n===0&&(!r||s===0)&&o;return{edgeValid:a,observedOk:a&&(i?.valid??!0)}},exitCode:n=>i&&!i.complete?2:n,failureText:()=>i&&!i.valid?Y(i):null}}function ce(e){return e?e.complete?e.valid?[{level:"ok",text:`0 new/worsened supported smells across ${e.touchedPaths.length} touched path(s)`}]:[{level:"bad",text:`${e.changes.length} new/worsened supported smell(s) block this candidate`},...e.changes.slice(0,5).flatMap(t=>[{level:"plain",text:`[${t.smellId}] ${t.evidence.path}:${t.evidence.line??1} ${t.evidence.symbol??""}`},{level:"dim",text:`fix: ${t.repairHint}`}])]:[{level:"bad",text:`Unavailable \u2014 ${e.error||"base/candidate evidence incomplete"}`}]:[]}export{K as DESIGN_DELTA_SCHEMA_VERSION,A as DESIGN_DELTA_SUPPORTED_SMELLS,v as analyzeDesignFindings,le as createDesignDeltaCheck,ce as designDeltaDoctorLines,X as evaluateGitDesignDelta,oe as evaluateWriteDesignDelta,Y as formatDesignDeltaBlock};
|