arkgate 4.8.13 → 4.8.15
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 +219 -4
- package/README.md +33 -22
- package/SECURITY.md +5 -3
- package/bin/ark-check-runtime.mjs +25 -57
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +140 -11
- package/bin/ark.mjs +7 -25
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-projection-formatters.mjs +2 -0
- package/bin/lib/agent-skills-package.mjs +241 -8
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +33 -5
- package/bin/lib/ark-order-doctor.mjs +7 -1
- package/bin/lib/ark-order-report.mjs +2 -1
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +4 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/ci-and-commands.mjs +7 -2
- package/bin/lib/config-contract.mjs +64 -1
- package/bin/lib/design-smells.mjs +21 -1
- package/bin/lib/diagnostic-catalog.mjs +5 -3
- package/bin/lib/doctor-advisories.mjs +109 -17
- package/bin/lib/doctor-human.mjs +20 -6
- package/bin/lib/doctor-next-actions.mjs +14 -0
- package/bin/lib/doctor-plan.mjs +32 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/first-run-help.mjs +19 -3
- package/bin/lib/gate-files.mjs +40 -3
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +6 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/install-migrate.mjs +23 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +57 -1
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +19 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +6 -2
- package/bin/lib/resolved-candidate-facts.mjs +225 -100
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/scan-files.mjs +39 -0
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +8 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/upgrade-whats-new.mjs +3 -3
- package/bin/lib/violations.mjs +43 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
- package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +31 -31
- package/dist/index.d.ts +126 -19
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
- package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
- package/docs/README.md +8 -6
- package/docs/agent-guide.md +35 -17
- package/docs/ai-gates.md +13 -4
- package/docs/arkorder.md +11 -4
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +63 -14
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +30 -10
- package/docs/enthusiast/README.md +6 -1
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-gallery-starter.md +2 -1
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +10 -6
- package/docs/product-voice.md +32 -6
- package/docs/threat-model.md +2 -2
- package/docs/typescript-support.md +3 -3
- package/docs/use.md +23 -11
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +7 -4
- package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
- package/templates/agent-skills/ark-architect/SKILL.md +5 -18
- package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
- package/templates/agent-skills/ark-contract/SKILL.md +9 -20
- package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
- package/templates/agent-skills/ark-explain/SKILL.md +11 -5
- package/templates/agent-skills/ark-explore/SKILL.md +39 -5
- package/templates/agent-skills/ark-fix/SKILL.md +15 -20
- package/templates/agent-skills/ark-loop/SKILL.md +14 -20
- package/templates/agent-skills/ark-order/SKILL.md +200 -0
- package/templates/agent-skills/ark-place/SKILL.md +49 -11
- package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
- package/templates/agent-skills/ark-think/SKILL.md +24 -126
- package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +91 -6
- package/templates/skills/ark-architect.md +5 -18
- package/templates/skills/ark-autopilot.md +14 -6
- package/templates/skills/ark-contract.md +9 -20
- package/templates/skills/ark-coverage.md +13 -9
- package/templates/skills/ark-explain.md +11 -5
- package/templates/skills/ark-explore.md +39 -5
- package/templates/skills/ark-fix.md +15 -20
- package/templates/skills/ark-loop.md +14 -20
- package/templates/skills/ark-order.md +200 -0
- package/templates/skills/ark-place.md +49 -11
- package/templates/skills/ark-runtime.md +21 -6
- package/templates/skills/ark-think.md +24 -126
- package/templates/skills/ark-upgrade.md +14 -3
- package/templates/tests/ark-adoption-gaps.test.ts +5 -4
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One definition of projected governed coverage: include-scoped files that
|
|
3
|
+
* match a layer glob, via computeCoverage. Shared by start, --recommend, and
|
|
4
|
+
* doctor so the same tree cannot advertise two "projected" percents.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { collectGovernedFiles } from './scan-files.mjs';
|
|
9
|
+
import { computeCoverage } from './doctor-plan.mjs';
|
|
10
|
+
import { ARCHITECTURE_PRESETS } from './presets.mjs';
|
|
11
|
+
import {
|
|
12
|
+
detectWorkspaces,
|
|
13
|
+
resolveIncludeRoots,
|
|
14
|
+
resolveStartInitPreset,
|
|
15
|
+
} from '../ark-shared.mjs';
|
|
16
|
+
|
|
17
|
+
export function measureGovernedCoverage(root, config) {
|
|
18
|
+
const files = collectGovernedFiles(root, config);
|
|
19
|
+
return computeCoverage(root, config, files, config.rules ?? []);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function summarizeGovernedCoverage(coverage) {
|
|
23
|
+
const governed = coverage?.governed ?? {};
|
|
24
|
+
return {
|
|
25
|
+
percent: typeof governed.percent === 'number' ? governed.percent : 0,
|
|
26
|
+
classifiedFiles: governed.classifiedFiles ?? 0,
|
|
27
|
+
totalFiles: governed.totalFiles ?? coverage?.totalFiles ?? 0,
|
|
28
|
+
emptyScope: coverage?.emptyScope === true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function starterConfigForPreset(root, preset) {
|
|
33
|
+
const factory = ARCHITECTURE_PRESETS[preset];
|
|
34
|
+
if (typeof factory !== 'function') return null;
|
|
35
|
+
const workspacesOrInclude =
|
|
36
|
+
preset === 'monorepo' || preset === 'ui-surface'
|
|
37
|
+
? resolveIncludeRoots(root)
|
|
38
|
+
: detectWorkspaces(root);
|
|
39
|
+
return factory(workspacesOrInclude, root);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Contract start/recommend/doctor should score: existing ark.config.json, else
|
|
44
|
+
* the starter --init would write for this tree (same preset resolution as start).
|
|
45
|
+
*/
|
|
46
|
+
export function resolveProjectedCoverageConfig(root, recommendation = {}) {
|
|
47
|
+
const configPath = path.join(root, 'ark.config.json');
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
51
|
+
if (parsed && typeof parsed === 'object') {
|
|
52
|
+
return { config: parsed, source: 'existing-config' };
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
/* fall through to starter */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const preset = resolveStartInitPreset(root, recommendation);
|
|
59
|
+
if (!preset) return { config: null, source: 'none', preset: null };
|
|
60
|
+
return { config: starterConfigForPreset(root, preset), source: 'start-starter', preset };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function measureProjectedGovernedCoverage(root, recommendation = {}) {
|
|
64
|
+
const resolved = resolveProjectedCoverageConfig(root, recommendation);
|
|
65
|
+
if (!resolved.config) {
|
|
66
|
+
return {
|
|
67
|
+
percent: 0,
|
|
68
|
+
classifiedFiles: 0,
|
|
69
|
+
totalFiles: 0,
|
|
70
|
+
emptyScope: true,
|
|
71
|
+
source: resolved.source,
|
|
72
|
+
preset: resolved.preset ?? null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...summarizeGovernedCoverage(measureGovernedCoverage(root, resolved.config)),
|
|
77
|
+
source: resolved.source,
|
|
78
|
+
preset: resolved.preset ?? recommendation.preset ?? null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const PROJECTED_COVERAGE_REASON = /projected governed coverage is \d+% \(below 90%\)/;
|
|
83
|
+
|
|
84
|
+
export function withProjectedGovernedCoverage(recommendation, root) {
|
|
85
|
+
if (!recommendation || typeof recommendation !== 'object') return recommendation;
|
|
86
|
+
const measured = measureProjectedGovernedCoverage(root, recommendation);
|
|
87
|
+
const percent = measured.percent;
|
|
88
|
+
const confirmationReasons = (recommendation.confirmationReasons ?? []).filter(
|
|
89
|
+
(reason) => !PROJECTED_COVERAGE_REASON.test(String(reason))
|
|
90
|
+
);
|
|
91
|
+
if (percent < 90) {
|
|
92
|
+
confirmationReasons.push(`projected governed coverage is ${percent}% (below 90%)`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...recommendation,
|
|
96
|
+
requiresConfirmation:
|
|
97
|
+
Boolean(recommendation.requiresConfirmation) ||
|
|
98
|
+
percent < 90 ||
|
|
99
|
+
Boolean(recommendation.thinTsSurface),
|
|
100
|
+
confirmationReasons,
|
|
101
|
+
signals: {
|
|
102
|
+
...recommendation.signals,
|
|
103
|
+
projectedGovernedCoverage: percent,
|
|
104
|
+
projectedGovernedClassifiedFiles: measured.classifiedFiles,
|
|
105
|
+
projectedGovernedTotalFiles: measured.totalFiles,
|
|
106
|
+
projectedGovernedSource: measured.source,
|
|
107
|
+
},
|
|
108
|
+
projectedCoverage: {
|
|
109
|
+
percent,
|
|
110
|
+
classifiedFiles: measured.classifiedFiles,
|
|
111
|
+
totalFiles: measured.totalFiles,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--recommend` presentation. Lives here so ark-check-runtime stays under the
|
|
3
|
+
* orchestration LOC budget and doctor does not parse this path.
|
|
4
|
+
*/
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
ADOPTION_PLAN_FILENAME,
|
|
8
|
+
buildArchitectureRecommendation,
|
|
9
|
+
formatArchitectureRecommendationHuman,
|
|
10
|
+
writeAdoptionPlan,
|
|
11
|
+
} from '../ark-shared.mjs';
|
|
12
|
+
import { withProjectedGovernedCoverage } from './projected-governed-coverage.mjs';
|
|
13
|
+
|
|
14
|
+
export function runRecommend(args) {
|
|
15
|
+
try {
|
|
16
|
+
const recommendation = withProjectedGovernedCoverage(
|
|
17
|
+
buildArchitectureRecommendation(args.root),
|
|
18
|
+
args.root
|
|
19
|
+
);
|
|
20
|
+
let planWritten;
|
|
21
|
+
if (args.writePlan) {
|
|
22
|
+
const result = writeAdoptionPlan(args.root, recommendation);
|
|
23
|
+
planWritten = result.path;
|
|
24
|
+
}
|
|
25
|
+
if (args.json) {
|
|
26
|
+
console.log(
|
|
27
|
+
JSON.stringify(
|
|
28
|
+
{
|
|
29
|
+
...recommendation,
|
|
30
|
+
...(planWritten
|
|
31
|
+
? { adoptionPlanPath: path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME }
|
|
32
|
+
: {}),
|
|
33
|
+
},
|
|
34
|
+
null,
|
|
35
|
+
2
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(formatArchitectureRecommendationHuman(recommendation));
|
|
40
|
+
if (planWritten) {
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(`Wrote ${path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (args.json) {
|
|
48
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`ark-check --recommend failed: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
process.exitCode = 2;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -216,6 +216,10 @@ export function deterministicNextAction(violation) {
|
|
|
216
216
|
return 'Add metadata.source to the publish call, then run Ark again.';
|
|
217
217
|
case 'INVARIANT_COVERAGE_OUTSIDE_ROOTS':
|
|
218
218
|
return `Move the covering test under a declared coverage root, or add its root to coverage.coverageRoots in ark.config.json, then run Ark again.`;
|
|
219
|
+
case 'INVARIANT_CATALOG_EMPTY':
|
|
220
|
+
return typeof violation.file === 'string' && violation.file.length > 0
|
|
221
|
+
? `Add 1–2 short phrases to invariants[] in ${violation.file}, then run ark-check --doctor. Starters show the shape.`
|
|
222
|
+
: 'Add 1–2 short phrases to invariants[] in arkrules/<Domain>.json, then run ark-check --doctor. Starters show the shape.';
|
|
219
223
|
case 'ARKRULE_STRUCTURE':
|
|
220
224
|
case 'ARKRULE_INVARIANT':
|
|
221
225
|
case 'INVARIANT_UNCOVERED':
|
|
@@ -249,7 +253,7 @@ export function deterministicNextAction(violation) {
|
|
|
249
253
|
? `Don't write ${violation.target} from a use-case. Take the event in, or change that choice through the valve (proposeRelease then apply), not a generic update.`
|
|
250
254
|
: "Don't write a named product choice from a use-case. Take the event in, or change that choice through the valve (proposeRelease then apply), not a generic update.";
|
|
251
255
|
case 'ARKORDER_UNVALVED_RELEASE':
|
|
252
|
-
return 'Change
|
|
256
|
+
return 'Change the choice with proposeRelease then apply. release() is only the first freeze. Never mechanical-safe.';
|
|
253
257
|
default:
|
|
254
258
|
if (typeof violation.ruleId === 'string' && violation.ruleId.startsWith('ARKRULE_')) {
|
|
255
259
|
return `Fix the ArkRule ${typeof violation.arkruleId === 'string' ? violation.arkruleId : violation.ruleId}, then preflight again.`;
|
|
@@ -544,7 +548,7 @@ export function enrichViolationWithFixClass(violation) {
|
|
|
544
548
|
: violation.ruleId === 'ARKORDER_STALE_SIGMA'
|
|
545
549
|
? 'Refresh σ. ξ does not expire.'
|
|
546
550
|
: violation.ruleId === 'ARKORDER_UNVALVED_RELEASE'
|
|
547
|
-
? 'The
|
|
551
|
+
? 'The choice is already frozen. proposeRelease then apply — do not call release() again with a different value.'
|
|
548
552
|
: 'Call createOrderPlane from arkgate/order in a listed plane root so the app actually freezes a pattern.';
|
|
549
553
|
break;
|
|
550
554
|
default:
|
|
@@ -211,32 +211,67 @@ function canonicalOverlayPath(
|
|
|
211
211
|
return [...candidates].sort()[0] ?? requested;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
function
|
|
214
|
+
function discoverScopedFiles(root, config, scopeRelatives, observeInput) {
|
|
215
|
+
const discovered = [];
|
|
216
|
+
const seen = new Set();
|
|
217
|
+
for (const raw of scopeRelatives ?? []) {
|
|
218
|
+
let relative;
|
|
219
|
+
try {
|
|
220
|
+
relative = canonicalProjectPath(
|
|
221
|
+
String(raw || '')
|
|
222
|
+
.replace(/\\/g, '/')
|
|
223
|
+
.replace(/^\.\//, '')
|
|
224
|
+
);
|
|
225
|
+
} catch {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (seen.has(relative) || !isGovernableSourceFile(path.basename(relative))) continue;
|
|
229
|
+
if (!isIncluded(relative, config.include) || isScanExcludedRelative(relative, config)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const absolute = path.join(root, ...relative.split('/'));
|
|
233
|
+
if (!fs.existsSync(absolute)) continue;
|
|
234
|
+
seen.add(relative);
|
|
235
|
+
observeResolvedInput(observeInput, absolute, 'realpath');
|
|
236
|
+
discovered.push({
|
|
237
|
+
absolute,
|
|
238
|
+
real: fs.realpathSync(absolute),
|
|
239
|
+
relative,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return discovered.sort((left, right) =>
|
|
243
|
+
left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function collectCandidateFiles(root, config, changes, observeInput, scopeRelatives) {
|
|
215
248
|
const files = new Map();
|
|
216
249
|
const directoryAliases = new Map();
|
|
217
250
|
const expandedAliasDirectories = new Set();
|
|
218
|
-
const discovered = (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
251
|
+
const discovered = Array.isArray(scopeRelatives)
|
|
252
|
+
? discoverScopedFiles(root, config, scopeRelatives, observeInput)
|
|
253
|
+
: (config.include ?? [])
|
|
254
|
+
.flatMap((entry) =>
|
|
255
|
+
collectGovernedFiles(root, { ...config, include: [entry] }, {
|
|
256
|
+
observeInput,
|
|
257
|
+
onDirectory(absolute, real) {
|
|
258
|
+
let relative = normalize(path.relative(root, absolute));
|
|
259
|
+
if (relative === '.') relative = '';
|
|
260
|
+
rememberDirectoryAlias(directoryAliases, real, relative, absolute);
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
.map((absolute) => {
|
|
265
|
+
observeResolvedInput(observeInput, absolute, 'realpath');
|
|
266
|
+
return {
|
|
267
|
+
absolute,
|
|
268
|
+
real: fs.realpathSync(absolute),
|
|
269
|
+
relative: canonicalProjectPath(normalize(path.relative(root, absolute))),
|
|
270
|
+
};
|
|
271
|
+
})
|
|
272
|
+
.sort((left, right) =>
|
|
273
|
+
left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
|
|
274
|
+
);
|
|
240
275
|
const canonicalByRealpath = new Map();
|
|
241
276
|
for (const candidate of discovered) {
|
|
242
277
|
if (!canonicalByRealpath.has(candidate.real)) canonicalByRealpath.set(candidate.real, candidate);
|
|
@@ -582,7 +617,8 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
|
|
|
582
617
|
const optionsByPath = new Map();
|
|
583
618
|
const configInputsByPath = new Map();
|
|
584
619
|
const reasons = [];
|
|
585
|
-
|
|
620
|
+
const loadConfigOptions = (configPath) => {
|
|
621
|
+
if (optionsByPath.has(configPath)) return optionsByPath.get(configPath);
|
|
586
622
|
const configContents = new Map();
|
|
587
623
|
const readConfig = (fileName) => {
|
|
588
624
|
observeResolvedInput(observeInput, fileName, 'tsconfig');
|
|
@@ -605,15 +641,16 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
|
|
|
605
641
|
});
|
|
606
642
|
optionsByPath.set(configPath, {});
|
|
607
643
|
configInputsByPath.set(configPath, configContents);
|
|
608
|
-
|
|
644
|
+
return {};
|
|
609
645
|
}
|
|
610
646
|
const parsed = ts.parseJsonConfigFileContent(
|
|
611
647
|
read.config,
|
|
612
648
|
{
|
|
613
649
|
useCaseSensitiveFileNames: ts.sys?.useCaseSensitiveFileNames ?? true,
|
|
614
|
-
readDirectory(
|
|
615
|
-
observeResolvedInput(observeInput,
|
|
616
|
-
|
|
650
|
+
readDirectory(dir) {
|
|
651
|
+
observeResolvedInput(observeInput, dir, 'tsconfig-directory');
|
|
652
|
+
// Options-only parse: `fileNames` are unused. Skip the include walk (#205).
|
|
653
|
+
return [];
|
|
617
654
|
},
|
|
618
655
|
fileExists(fileName) {
|
|
619
656
|
observeResolvedInput(observeInput, fileName, 'tsconfig-exists');
|
|
@@ -637,11 +674,19 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
|
|
|
637
674
|
}
|
|
638
675
|
optionsByPath.set(configPath, parsed.options ?? {});
|
|
639
676
|
configInputsByPath.set(configPath, configContents);
|
|
640
|
-
|
|
677
|
+
return parsed.options ?? {};
|
|
678
|
+
};
|
|
679
|
+
for (const configPath of configPaths) loadConfigOptions(configPath);
|
|
641
680
|
|
|
642
681
|
const optionsFor = (fileName) => {
|
|
643
|
-
const
|
|
644
|
-
|
|
682
|
+
const resolved = path.resolve(fileName);
|
|
683
|
+
let configPath = configByFile.get(resolved);
|
|
684
|
+
if (!configPath) {
|
|
685
|
+
configPath = explicitPath
|
|
686
|
+
?? nearestTsconfig(root, resolved, nearestConfigByDirectory, observeInput);
|
|
687
|
+
if (configPath) configByFile.set(resolved, configPath);
|
|
688
|
+
}
|
|
689
|
+
return configPath ? loadConfigOptions(configPath) ?? {} : {};
|
|
645
690
|
};
|
|
646
691
|
const configs = configPaths.map((configPath) => ({
|
|
647
692
|
path: configLabel(root, configPath, externalAnchor),
|
|
@@ -1036,6 +1081,19 @@ function unavailableFacts(config, ts, reason) {
|
|
|
1036
1081
|
});
|
|
1037
1082
|
}
|
|
1038
1083
|
|
|
1084
|
+
function loadOneCandidateFile(root, config, relative, observeInput) {
|
|
1085
|
+
const discovered = discoverScopedFiles(root, config, [relative], observeInput);
|
|
1086
|
+
const found = discovered[0];
|
|
1087
|
+
if (!found) return null;
|
|
1088
|
+
observeResolvedInput(observeInput, found.absolute, 'source');
|
|
1089
|
+
return {
|
|
1090
|
+
path: found.relative,
|
|
1091
|
+
absolute: path.resolve(found.absolute),
|
|
1092
|
+
real: path.resolve(found.real),
|
|
1093
|
+
content: fs.readFileSync(found.absolute, 'utf8'),
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1039
1097
|
/** Resolve one complete candidate tree (base or virtual overlay) into versioned neutral facts. */
|
|
1040
1098
|
export function resolveCandidateFacts({
|
|
1041
1099
|
root,
|
|
@@ -1044,6 +1102,7 @@ export function resolveCandidateFacts({
|
|
|
1044
1102
|
tsconfig,
|
|
1045
1103
|
changes = [],
|
|
1046
1104
|
observeInput,
|
|
1105
|
+
scopeFiles,
|
|
1047
1106
|
}) {
|
|
1048
1107
|
if (!ts?.createSourceFile || !ts?.resolveModuleName) {
|
|
1049
1108
|
return unavailableFacts(config, ts, 'No API-compatible TypeScript resolver is available.');
|
|
@@ -1053,10 +1112,17 @@ export function resolveCandidateFacts({
|
|
|
1053
1112
|
let candidateFiles;
|
|
1054
1113
|
let canonicalChanges;
|
|
1055
1114
|
let compiler;
|
|
1115
|
+
const scoped = Array.isArray(scopeFiles);
|
|
1056
1116
|
try {
|
|
1057
1117
|
observeResolvedInput(observeInput, root, 'realpath');
|
|
1058
1118
|
canonicalRoot = fs.realpathSync(root);
|
|
1059
|
-
const candidate = collectCandidateFiles(
|
|
1119
|
+
const candidate = collectCandidateFiles(
|
|
1120
|
+
canonicalRoot,
|
|
1121
|
+
config,
|
|
1122
|
+
changes,
|
|
1123
|
+
observeInput,
|
|
1124
|
+
scoped ? scopeFiles : undefined
|
|
1125
|
+
);
|
|
1060
1126
|
candidateFiles = candidate.files;
|
|
1061
1127
|
canonicalChanges = candidate.changes;
|
|
1062
1128
|
compiler = compilerContext(ts, canonicalRoot, tsconfig, candidateFiles, observeInput);
|
|
@@ -1102,11 +1168,20 @@ export function resolveCandidateFacts({
|
|
|
1102
1168
|
const arkOrderXiFieldWrites = [];
|
|
1103
1169
|
const arkOrderIngestWritesXi = [];
|
|
1104
1170
|
const arkOrderReleaseKeyCounts = [];
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1107
|
-
const
|
|
1171
|
+
const arkRunActive = config.arkRun != null && typeof config.arkRun === 'object';
|
|
1172
|
+
const arkOrderActive = config.arkOrder != null && typeof config.arkOrder === 'object';
|
|
1173
|
+
const arkRulesActive =
|
|
1174
|
+
config.arkRules != null &&
|
|
1175
|
+
typeof config.arkRules === 'object' &&
|
|
1176
|
+
Object.keys(config.arkRules).length > 0;
|
|
1177
|
+
const xiKeys = arkOrderActive ? [...(config.arkOrder?.xiKeys ?? [])] : [];
|
|
1178
|
+
const compositionRootPatterns = arkRunActive
|
|
1179
|
+
? [...(config.arkRun?.compositionRoots ?? [])]
|
|
1180
|
+
: [];
|
|
1181
|
+
const planeRootPatterns = arkOrderActive ? [...(config.arkOrder?.planeRoots ?? [])] : [];
|
|
1108
1182
|
|
|
1109
|
-
|
|
1183
|
+
const seedPathSet = new Set(candidateFiles.map((file) => file.path));
|
|
1184
|
+
const ingest = (candidate, fullExtract) => {
|
|
1110
1185
|
const sourceFile = ts.createSourceFile(
|
|
1111
1186
|
candidate.absolute,
|
|
1112
1187
|
candidate.content,
|
|
@@ -1158,6 +1233,7 @@ export function resolveCandidateFacts({
|
|
|
1158
1233
|
message: `${candidate.path} has ${parseDiagnosticCount} TypeScript parse diagnostic(s).`,
|
|
1159
1234
|
});
|
|
1160
1235
|
}
|
|
1236
|
+
if (!fullExtract) return;
|
|
1161
1237
|
const forbiddenUses = mayContainForbiddenCapability(ts, sourceFile, forbiddenGlobals)
|
|
1162
1238
|
? collectForbiddenCapabilityUses(ts, sourceFile, forbiddenGlobals)
|
|
1163
1239
|
: [];
|
|
@@ -1188,83 +1264,132 @@ export function resolveCandidateFacts({
|
|
|
1188
1264
|
);
|
|
1189
1265
|
// Class-shape extraction is text-conservative (false negatives over false positives).
|
|
1190
1266
|
// Only TS/TSX candidates; sensors consume the same shape via facts.classShapes.
|
|
1267
|
+
// Extra-plane and ArkRules extractors stay silent when that plane is off (#212).
|
|
1191
1268
|
if (/\.(tsx?|mts|cts)$/i.test(candidate.path)) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
arkRunKernelCalls.push(
|
|
1199
|
-
...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
|
|
1200
|
-
);
|
|
1201
|
-
} catch {
|
|
1202
|
-
// Never fail the resolver for ArkRun call extraction.
|
|
1203
|
-
}
|
|
1204
|
-
try {
|
|
1205
|
-
arkRunDeclarations.push(
|
|
1206
|
-
...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
|
|
1207
|
-
);
|
|
1208
|
-
} catch {
|
|
1209
|
-
// Never fail the resolver for ArkRun declaration extraction.
|
|
1210
|
-
}
|
|
1211
|
-
try {
|
|
1212
|
-
arkOrderPlaneCalls.push(
|
|
1213
|
-
...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
|
|
1214
|
-
);
|
|
1215
|
-
} catch {
|
|
1216
|
-
// Never fail the resolver for ArkOrder factory extraction.
|
|
1217
|
-
}
|
|
1218
|
-
try {
|
|
1219
|
-
arkOrderGenericUpdates.push(
|
|
1220
|
-
...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
|
|
1221
|
-
);
|
|
1222
|
-
} catch {
|
|
1223
|
-
// Never fail the resolver for ArkOrder generic-update extraction.
|
|
1269
|
+
if (arkRulesActive || arkRunActive) {
|
|
1270
|
+
try {
|
|
1271
|
+
classShapes.push(...extractClassShapesFromSource(candidate.path, candidate.content));
|
|
1272
|
+
} catch {
|
|
1273
|
+
// Never fail the resolver for shape extraction; sensors stay silent on this file.
|
|
1274
|
+
}
|
|
1224
1275
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1276
|
+
if (arkRunActive) {
|
|
1277
|
+
try {
|
|
1278
|
+
arkRunKernelCalls.push(
|
|
1279
|
+
...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
|
|
1280
|
+
);
|
|
1281
|
+
} catch {
|
|
1282
|
+
// Never fail the resolver for ArkRun call extraction.
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
arkRunDeclarations.push(
|
|
1286
|
+
...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
|
|
1287
|
+
);
|
|
1288
|
+
} catch {
|
|
1289
|
+
// Never fail the resolver for ArkRun declaration extraction.
|
|
1290
|
+
}
|
|
1231
1291
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1292
|
+
if (arkOrderActive) {
|
|
1293
|
+
try {
|
|
1294
|
+
arkOrderPlaneCalls.push(
|
|
1295
|
+
...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
|
|
1296
|
+
);
|
|
1297
|
+
} catch {
|
|
1298
|
+
// Never fail the resolver for ArkOrder factory extraction.
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
arkOrderGenericUpdates.push(
|
|
1302
|
+
...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
|
|
1303
|
+
);
|
|
1304
|
+
} catch {
|
|
1305
|
+
// Never fail the resolver for ArkOrder generic-update extraction.
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
arkOrderXiFieldWrites.push(
|
|
1309
|
+
...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
|
|
1310
|
+
);
|
|
1311
|
+
} catch {
|
|
1312
|
+
// Never fail the resolver for ArkOrder xi-field-write extraction.
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
arkOrderIngestWritesXi.push(
|
|
1316
|
+
...extractArkOrderIngestWritesXiFromSource(candidate.path, candidate.content)
|
|
1317
|
+
);
|
|
1318
|
+
} catch {
|
|
1319
|
+
// Never fail the resolver for ArkOrder ingest-writes-ξ extraction.
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
arkOrderReleaseKeyCounts.push(
|
|
1323
|
+
...extractArkOrderReleaseKeyCountsFromSource(candidate.path, candidate.content)
|
|
1324
|
+
);
|
|
1325
|
+
} catch {
|
|
1326
|
+
// Never fail the resolver for ArkOrder release key-count extraction.
|
|
1327
|
+
}
|
|
1238
1328
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
for (const candidate of candidateFiles) ingest(candidate, true);
|
|
1333
|
+
|
|
1334
|
+
if (scoped) {
|
|
1335
|
+
const loaded = new Set(candidateFiles.map((file) => file.path));
|
|
1336
|
+
let pending = [...candidateFiles];
|
|
1337
|
+
while (pending.length > 0) {
|
|
1338
|
+
const discovered = [];
|
|
1339
|
+
for (const source of pending) {
|
|
1340
|
+
const parsedFile = parsed.get(source.path);
|
|
1341
|
+
if (!parsedFile) continue;
|
|
1342
|
+
for (const dependency of parsedFile.dependencies) {
|
|
1343
|
+
const resolved = resolveDependency(
|
|
1344
|
+
ts,
|
|
1345
|
+
dependency,
|
|
1346
|
+
parsedFile.candidate.absolute,
|
|
1347
|
+
compiler.optionsFor(parsedFile.candidate.absolute),
|
|
1348
|
+
host
|
|
1349
|
+
);
|
|
1350
|
+
if (resolved.resolution !== 'resolved-project' || !resolved.target) continue;
|
|
1351
|
+
if (loaded.has(resolved.target)) continue;
|
|
1352
|
+
const extra = loadOneCandidateFile(
|
|
1353
|
+
canonicalRoot,
|
|
1354
|
+
config,
|
|
1355
|
+
resolved.target,
|
|
1356
|
+
observeInput
|
|
1357
|
+
);
|
|
1358
|
+
if (!extra) continue;
|
|
1359
|
+
loaded.add(extra.path);
|
|
1360
|
+
candidateFiles.push(extra);
|
|
1361
|
+
ingest(extra, false);
|
|
1362
|
+
discovered.push(extra);
|
|
1363
|
+
}
|
|
1245
1364
|
}
|
|
1365
|
+
pending = discovered;
|
|
1246
1366
|
}
|
|
1247
1367
|
}
|
|
1248
1368
|
|
|
1369
|
+
const extractCandidates = scoped
|
|
1370
|
+
? candidateFiles.filter((file) => seedPathSet.has(file.path))
|
|
1371
|
+
: candidateFiles;
|
|
1249
1372
|
const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1373
|
+
if (arkRunActive) {
|
|
1374
|
+
for (const candidate of extractCandidates) {
|
|
1375
|
+
if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
|
|
1376
|
+
try {
|
|
1377
|
+
arkRunManagedNews.push(
|
|
1378
|
+
...extractArkRunManagedNewsFromSource(
|
|
1379
|
+
candidate.path,
|
|
1380
|
+
candidate.content,
|
|
1381
|
+
admittedTypeNames
|
|
1382
|
+
)
|
|
1383
|
+
);
|
|
1384
|
+
} catch {
|
|
1385
|
+
// Never fail the resolver for managed-new extraction.
|
|
1386
|
+
}
|
|
1262
1387
|
}
|
|
1263
1388
|
}
|
|
1264
1389
|
|
|
1265
1390
|
if (planeRootPatterns.length > 0) {
|
|
1266
1391
|
const factoryFiles = new Set(arkOrderPlaneCalls.map((call) => call.file));
|
|
1267
|
-
for (const candidate of
|
|
1392
|
+
for (const candidate of extractCandidates) {
|
|
1268
1393
|
for (const pattern of planeRootPatterns) {
|
|
1269
1394
|
try {
|
|
1270
1395
|
if (!globToRegExp(pattern).test(candidate.path)) continue;
|
|
@@ -1284,7 +1409,7 @@ export function resolveCandidateFacts({
|
|
|
1284
1409
|
const factoryFiles = new Set(
|
|
1285
1410
|
arkRunKernelCalls.filter((call) => call.kind === 'factory').map((call) => call.file)
|
|
1286
1411
|
);
|
|
1287
|
-
for (const candidate of
|
|
1412
|
+
for (const candidate of extractCandidates) {
|
|
1288
1413
|
for (const pattern of compositionRootPatterns) {
|
|
1289
1414
|
try {
|
|
1290
1415
|
if (!globToRegExp(pattern).test(candidate.path)) continue;
|