arkgate 4.8.15 → 4.8.16
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 +39 -0
- package/README.md +7 -5
- package/bin/ark-shared.mjs +2 -0
- package/bin/ark.mjs +17 -8
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/architecture-scan.mjs +17 -0
- package/bin/lib/baseline-key.mjs +2 -0
- package/bin/lib/config-contract.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +2 -0
- package/bin/lib/doctor-advisories.mjs +26 -0
- package/bin/lib/doctor-green-cite.mjs +139 -0
- package/bin/lib/doctor-human.mjs +98 -63
- package/bin/lib/doctor-next-actions.mjs +9 -0
- package/bin/lib/doctor-plan.mjs +6 -3
- package/bin/lib/field-install.mjs +47 -7
- package/bin/lib/first-run-help.mjs +1 -0
- package/bin/lib/improvement-compass-doctor.mjs +3 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/invariant-coverage.mjs +121 -0
- package/bin/lib/invariant-tests-path.mjs +212 -0
- package/bin/lib/package-manager.mjs +8 -0
- package/bin/lib/prototype-shortcuts.mjs +224 -0
- package/bin/lib/remediation.mjs +11 -0
- package/bin/lib/start-preview.mjs +20 -1
- package/dist/{configTypes-Dt3DpVbd.d.ts → configTypes-VD0qcubY.d.ts} +2 -1
- package/dist/{diagnosticCatalog-BEg85XlE.d.ts → diagnosticCatalog-KWvGLI1U.d.ts} +24 -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 +5 -5
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +10 -10
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +10 -10
- package/dist/{types-TBiv0WHL.d.ts → types-BSzRy2X1.d.ts} +1 -1
- package/dist/{types-CN9tVMPz.d.ts → types-D5GT5ZT8.d.ts} +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +2 -0
- package/docs/configuration.md +11 -3
- package/docs/develop.md +3 -1
- package/docs/diagnostics.md +25 -1
- package/docs/package-surface.md +1 -1
- package/docs/use.md +5 -3
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +15 -0
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -1
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
- package/templates/agent-skills/ark-explain/SKILL.md +2 -1
- package/templates/agent-skills/ark-explore/SKILL.md +19 -0
- package/templates/agent-skills/ark-place/SKILL.md +4 -0
- package/templates/skills/ark-adopt.md +15 -0
- package/templates/skills/ark-autopilot.md +6 -1
- package/templates/skills/ark-coverage.md +1 -1
- package/templates/skills/ark-explain.md +2 -1
- package/templates/skills/ark-explore.md +19 -0
- package/templates/skills/ark-place.md +4 -0
|
@@ -83,6 +83,37 @@ function addDevDependencyPreservingFormat(source, version) {
|
|
|
83
83
|
return `${source.slice(0, contentEnd)}${addition}${eol}${rootClosingIndent}${source.slice(rootClose)}`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function replaceArkgateDependencyPreservingFormat(source, version) {
|
|
87
|
+
const encoded = JSON.stringify(version);
|
|
88
|
+
const next = source.replace(/("arkgate"\s*:\s*)(?:"(?:\\.|[^"\\])*")/, `$1${encoded}`);
|
|
89
|
+
if (next === source) {
|
|
90
|
+
throw new Error('arkgate pin not found in package.json');
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when a caret/tilde/exact pin is an older semver than the CLI. Non-semver stays. */
|
|
96
|
+
function pinCoreIsBehind(declaredPin, cliVersion) {
|
|
97
|
+
const pinCore = String(declaredPin).replace(/^[\^~>=<\s]+/, '').split(/\s+/)[0];
|
|
98
|
+
const pinParts = pinCore.split('.').map((p) => Number.parseInt(p, 10));
|
|
99
|
+
const cliParts = String(cliVersion).split('.').map((p) => Number.parseInt(p, 10));
|
|
100
|
+
if (
|
|
101
|
+
pinParts.length < 1 ||
|
|
102
|
+
cliParts.length < 1 ||
|
|
103
|
+
!pinParts.every((n) => Number.isFinite(n)) ||
|
|
104
|
+
!cliParts.every((n) => Number.isFinite(n))
|
|
105
|
+
) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
for (let i = 0; i < 3; i += 1) {
|
|
109
|
+
const p = pinParts[i] ?? 0;
|
|
110
|
+
const c = cliParts[i] ?? 0;
|
|
111
|
+
if (p < c) return true;
|
|
112
|
+
if (p > c) return false;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
const ARK_CHECK_BIN_RE = /\b(?:ark-check|arkgate-check)(?:\.mjs|\.js)?\b/;
|
|
87
118
|
const ARK_CHECK_RUNNER_RE =
|
|
88
119
|
/(?:^|[\s"'`;|&])(?:npx|pnpm|yarn|npm|bunx?|node)(?:\s|$)/;
|
|
@@ -379,13 +410,6 @@ export function pinArkgateDevDependency(root, opts = {}) {
|
|
|
379
410
|
}
|
|
380
411
|
const deps = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {};
|
|
381
412
|
const dev = pkg.devDependencies && typeof pkg.devDependencies === 'object' ? pkg.devDependencies : {};
|
|
382
|
-
if (typeof deps.arkgate === 'string' || typeof dev.arkgate === 'string') {
|
|
383
|
-
return {
|
|
384
|
-
changed: false,
|
|
385
|
-
reason: 'already-present',
|
|
386
|
-
version: deps.arkgate || dev.arkgate,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
413
|
const shipped = arkPackageVersion();
|
|
390
414
|
const version =
|
|
391
415
|
typeof opts.version === 'string' && opts.version
|
|
@@ -393,6 +417,22 @@ export function pinArkgateDevDependency(root, opts = {}) {
|
|
|
393
417
|
: shipped
|
|
394
418
|
? `^${shipped}`
|
|
395
419
|
: 'latest';
|
|
420
|
+
if (typeof deps.arkgate === 'string' || typeof dev.arkgate === 'string') {
|
|
421
|
+
const current = deps.arkgate || dev.arkgate;
|
|
422
|
+
const shouldBump =
|
|
423
|
+
opts.force === true || (Boolean(shipped) && pinCoreIsBehind(current, shipped));
|
|
424
|
+
if (!shouldBump || current === version) {
|
|
425
|
+
return {
|
|
426
|
+
changed: false,
|
|
427
|
+
reason: 'already-present',
|
|
428
|
+
version: current,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (opts.write !== false) {
|
|
432
|
+
fs.writeFileSync(pkgPath, replaceArkgateDependencyPreservingFormat(source, version));
|
|
433
|
+
}
|
|
434
|
+
return { changed: true, reason: 'bumped', version };
|
|
435
|
+
}
|
|
396
436
|
if (opts.write !== false) {
|
|
397
437
|
fs.writeFileSync(pkgPath, addDevDependencyPreservingFormat(source, version));
|
|
398
438
|
}
|
|
@@ -13,6 +13,7 @@ ${NORTH_STAR_ONE_LINE}
|
|
|
13
13
|
arkgate start --apply write host + CI setup
|
|
14
14
|
(refuses weak coverage/shape; lock with --archetype/--preset/--force)
|
|
15
15
|
arkgate-check --doctor status — one next step
|
|
16
|
+
(if missing: npx --package=arkgate arkgate-check --doctor)
|
|
16
17
|
|
|
17
18
|
Stuck? Run status (--doctor). Do #1.
|
|
18
19
|
`;
|
|
@@ -45,7 +45,9 @@ export function buildDoctorImprovementCompass(input = {}) {
|
|
|
45
45
|
if (
|
|
46
46
|
id.startsWith('ARKRULE_') ||
|
|
47
47
|
id === 'INVARIANT_UNCOVERED' ||
|
|
48
|
-
id === 'INVARIANT_CATALOG_EMPTY'
|
|
48
|
+
id === 'INVARIANT_CATALOG_EMPTY' ||
|
|
49
|
+
id === 'INVARIANT_TESTS_PATH_MISSING' ||
|
|
50
|
+
id === 'INVARIANT_COVERAGE_ROOTS_MISSING'
|
|
49
51
|
)
|
|
50
52
|
arkRulesStructureResidual += 1;
|
|
51
53
|
}
|
|
@@ -262,7 +262,9 @@ function mapViolations(byId, violations) {
|
|
|
262
262
|
}
|
|
263
263
|
if (upper.startsWith('ARKRULE_') ||
|
|
264
264
|
upper === 'INVARIANT_UNCOVERED' ||
|
|
265
|
-
upper === 'INVARIANT_CATALOG_EMPTY'
|
|
265
|
+
upper === 'INVARIANT_CATALOG_EMPTY' ||
|
|
266
|
+
upper === 'INVARIANT_TESTS_PATH_MISSING' ||
|
|
267
|
+
upper === 'INVARIANT_COVERAGE_ROOTS_MISSING') {
|
|
266
268
|
attach('encapsulation', 'ArkRules structure / invariant residual inside a layer.', {
|
|
267
269
|
kind: 'skill',
|
|
268
270
|
ref: '/ark-autopilot',
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/invariant-coverage.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/** Adopted + catalogued invariants, but no declared tests path (P2 §10). */
|
|
12
|
+
export const INVARIANT_TESTS_PATH_RULE_ID = 'INVARIANT_TESTS_PATH_MISSING';
|
|
13
|
+
export const INVARIANT_TESTS_PATH_MESSAGE = 'This project is adopted and has domain invariants, but ark.config.json does not name a real tests path. Add coverage.testGlobs or coverage.coverageRoots pointing at the folder where those tests live, then re-run. Without that path, coverage is an empty checkbox.';
|
|
14
|
+
/** Enforced invariant, but no declared runner roots (P2 §10 residual). */
|
|
15
|
+
export const INVARIANT_COVERAGE_ROOTS_RULE_ID = 'INVARIANT_COVERAGE_ROOTS_MISSING';
|
|
16
|
+
export const INVARIANT_COVERAGE_ROOTS_MESSAGE = 'A domain invariant is enforced, but ark.config.json does not name coverage.coverageRoots — the folders where this project\'s test runner actually goes. Add coverage.coverageRoots pointing at that folder, then re-run. Without it, coverage can certify a test no runner runs.';
|
|
11
17
|
/**
|
|
12
18
|
* Human-readable discard tail. Empty when the scan discarded nothing.
|
|
13
19
|
* `omitBudget` drops the budget clause and the load totals for messages whose
|
|
@@ -149,6 +155,7 @@ export function evaluateInvariantCoverage(input) {
|
|
|
149
155
|
description: inv.description,
|
|
150
156
|
...(testEvidenceFile !== undefined ? { testEvidenceFile } : {}),
|
|
151
157
|
...(outsideDeclaredRoots !== undefined ? { outsideDeclaredRoots } : {}),
|
|
158
|
+
coverageRootsDeclared: rootsDeclared,
|
|
152
159
|
});
|
|
153
160
|
// The covering test exists but sits outside the roots the project declared
|
|
154
161
|
// its runner walks. ArkGate does not execute tests, so it cannot tell the
|
|
@@ -233,5 +240,119 @@ export function canPromoteInvariant(coverage) {
|
|
|
233
240
|
reason: `Invariant ${coverage.invariantId} is covered only by ${coverage.testEvidenceFile ?? 'a test'}, outside the declared coverage roots; ArkGate cannot tell whether that test runs, so it will not promote on it.`,
|
|
234
241
|
};
|
|
235
242
|
}
|
|
243
|
+
// Explicit false only: hand-built evidence may omit the field. The evaluator
|
|
244
|
+
// always sets it. Promoting without roots would make OUTSIDE_ROOTS silent.
|
|
245
|
+
if (coverage.coverageRootsDeclared === false) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
reason: `Declare coverage.coverageRoots in ark.config.json before promoting ${coverage.invariantId} to enforced. Without that, ArkGate cannot tell whether a covering test is one the runner executes.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
236
251
|
return { ok: true, reason: `Invariant ${coverage.invariantId} has coverage evidence.` };
|
|
237
252
|
}
|
|
253
|
+
function nonEmptyPathStrings(value) {
|
|
254
|
+
if (!Array.isArray(value))
|
|
255
|
+
return [];
|
|
256
|
+
const out = [];
|
|
257
|
+
for (const item of value) {
|
|
258
|
+
if (typeof item !== 'string')
|
|
259
|
+
continue;
|
|
260
|
+
const trimmed = item.trim();
|
|
261
|
+
if (trimmed.length > 0)
|
|
262
|
+
out.push(trimmed);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Declared tests homes: `coverage.testGlobs` and/or `coverage.coverageRoots`.
|
|
268
|
+
* Either is a configured path. Empty strings do not count.
|
|
269
|
+
*/
|
|
270
|
+
export function configuredInvariantTestsPaths(coverage) {
|
|
271
|
+
if (!coverage || typeof coverage !== 'object')
|
|
272
|
+
return [];
|
|
273
|
+
return [...nonEmptyPathStrings(coverage.testGlobs), ...nonEmptyPathStrings(coverage.coverageRoots)];
|
|
274
|
+
}
|
|
275
|
+
export function hasConfiguredInvariantTestsPath(coverage) {
|
|
276
|
+
return configuredInvariantTestsPaths(coverage).length > 0;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* True when at least one catalogued invariant wants test evidence.
|
|
280
|
+
* Same default as AR10: `coverage.test !== false`. `test: false` is an explicit
|
|
281
|
+
* opt-out (starter Domain phrases use it) and does not demand a tests path.
|
|
282
|
+
*/
|
|
283
|
+
export function catalogDemandsInvariantTestsPath(invariants) {
|
|
284
|
+
if (!Array.isArray(invariants) || invariants.length === 0)
|
|
285
|
+
return false;
|
|
286
|
+
return invariants.some((inv) => inv != null && inv.coverage?.test !== false);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* §10 — adopted + invariants that want tests require a real tests path.
|
|
290
|
+
* Fail-closed. Not freezable. Silent when not adopted, the catalog is empty,
|
|
291
|
+
* or every entry sets `coverage.test: false`.
|
|
292
|
+
*/
|
|
293
|
+
export function collectMissingInvariantTestsPathFindings(input) {
|
|
294
|
+
const demanded = input.hasDomainInvariants === true ||
|
|
295
|
+
(input.hasDomainInvariants !== false && catalogDemandsInvariantTestsPath(input.invariants));
|
|
296
|
+
if (input.adopted !== true || !demanded)
|
|
297
|
+
return [];
|
|
298
|
+
const configured = hasConfiguredInvariantTestsPath(input.coverage);
|
|
299
|
+
if (configured && input.declaredPathPresent !== false)
|
|
300
|
+
return [];
|
|
301
|
+
return [
|
|
302
|
+
{
|
|
303
|
+
ruleId: INVARIANT_TESTS_PATH_RULE_ID,
|
|
304
|
+
message: INVARIANT_TESTS_PATH_MESSAGE,
|
|
305
|
+
file: 'ark.config.json',
|
|
306
|
+
line: 1,
|
|
307
|
+
severity: 'error',
|
|
308
|
+
failsStrict: true,
|
|
309
|
+
freezable: false,
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Declared runner homes: `coverage.coverageRoots` only.
|
|
315
|
+
* Empty strings do not count. testGlobs is not a runner root.
|
|
316
|
+
*/
|
|
317
|
+
export function configuredCoverageRoots(coverage) {
|
|
318
|
+
if (!coverage || typeof coverage !== 'object')
|
|
319
|
+
return [];
|
|
320
|
+
return nonEmptyPathStrings(coverage.coverageRoots);
|
|
321
|
+
}
|
|
322
|
+
export function hasConfiguredCoverageRoots(coverage) {
|
|
323
|
+
return configuredCoverageRoots(coverage).length > 0;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* True when at least one catalogued invariant is `mode: "enforced"`.
|
|
327
|
+
* Structure-sensor enforced is not this — only `invariants[]`.
|
|
328
|
+
*/
|
|
329
|
+
export function catalogHasEnforcedInvariant(invariants) {
|
|
330
|
+
if (!Array.isArray(invariants) || invariants.length === 0)
|
|
331
|
+
return false;
|
|
332
|
+
return invariants.some((inv) => inv != null && inv.mode === 'enforced');
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* P2 §10 residual — any enforced invariant requires `coverage.coverageRoots`.
|
|
336
|
+
* Fail-closed. Not freezable. Silent when no invariant is enforced.
|
|
337
|
+
* testGlobs alone does not satisfy this: without roots, OUTSIDE_ROOTS cannot fire.
|
|
338
|
+
*/
|
|
339
|
+
export function collectMissingCoverageRootsFindings(input) {
|
|
340
|
+
const enforced = input.hasEnforcedInvariant === true ||
|
|
341
|
+
(input.hasEnforcedInvariant !== false && catalogHasEnforcedInvariant(input.invariants));
|
|
342
|
+
if (!enforced)
|
|
343
|
+
return [];
|
|
344
|
+
const configured = hasConfiguredCoverageRoots(input.coverage);
|
|
345
|
+
if (configured && input.declaredPathPresent !== false)
|
|
346
|
+
return [];
|
|
347
|
+
return [
|
|
348
|
+
{
|
|
349
|
+
ruleId: INVARIANT_COVERAGE_ROOTS_RULE_ID,
|
|
350
|
+
message: INVARIANT_COVERAGE_ROOTS_MESSAGE,
|
|
351
|
+
file: 'ark.config.json',
|
|
352
|
+
line: 1,
|
|
353
|
+
severity: 'error',
|
|
354
|
+
failsStrict: true,
|
|
355
|
+
freezable: false,
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adopted-mode domain-invariant tests path (P2 §10 / Contener).
|
|
3
|
+
* Tooling I/O over Domain collectMissingInvariantTestsPathFindings.
|
|
4
|
+
* Fail-closed when adopted. Silent when not adopted or the catalog is empty.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import {
|
|
10
|
+
classifyAdopted,
|
|
11
|
+
isAdopted,
|
|
12
|
+
readAdoptionStance,
|
|
13
|
+
} from './adoption-stance.mjs';
|
|
14
|
+
import {
|
|
15
|
+
catalogDemandsInvariantTestsPath,
|
|
16
|
+
catalogHasEnforcedInvariant,
|
|
17
|
+
configuredCoverageRoots,
|
|
18
|
+
configuredInvariantTestsPaths,
|
|
19
|
+
collectMissingCoverageRootsFindings,
|
|
20
|
+
collectMissingInvariantTestsPathFindings,
|
|
21
|
+
hasConfiguredCoverageRoots,
|
|
22
|
+
hasConfiguredInvariantTestsPath,
|
|
23
|
+
INVARIANT_COVERAGE_ROOTS_MESSAGE,
|
|
24
|
+
INVARIANT_TESTS_PATH_MESSAGE,
|
|
25
|
+
} from './invariant-coverage.mjs';
|
|
26
|
+
import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
|
|
27
|
+
|
|
28
|
+
export const INVARIANT_TESTS_PATH_ASK = INVARIANT_TESTS_PATH_MESSAGE;
|
|
29
|
+
|
|
30
|
+
export const INVARIANT_TESTS_PATH_NEXT =
|
|
31
|
+
'Add coverage.testGlobs or coverage.coverageRoots in ark.config.json pointing at a real tests folder, then re-run.';
|
|
32
|
+
|
|
33
|
+
export const INVARIANT_COVERAGE_ROOTS_ASK = INVARIANT_COVERAGE_ROOTS_MESSAGE;
|
|
34
|
+
|
|
35
|
+
export const INVARIANT_COVERAGE_ROOTS_NEXT =
|
|
36
|
+
'Add coverage.coverageRoots in ark.config.json pointing at the folder the test runner uses, then re-run.';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* --require-gates / --strict-merge, or an explicit advisory-only ack.
|
|
40
|
+
* GitHub required-merge is known later in doctor; the scan uses this cheap side.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} root
|
|
43
|
+
* @param {{ requireGates?: boolean }} [args]
|
|
44
|
+
*/
|
|
45
|
+
export function scanDemandsInvariantTestsPath(root, args = {}) {
|
|
46
|
+
if (args.requireGates === true) return true;
|
|
47
|
+
return isAdopted(classifyAdopted({ stance: readAdoptionStance(root) }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* First concrete path segment exists, or every declared glob is wildcard-only.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} root
|
|
54
|
+
* @param {{ testGlobs?: unknown, coverageRoots?: unknown } | null | undefined} coverage
|
|
55
|
+
*/
|
|
56
|
+
export function declaredInvariantTestsPathPresent(root, coverage) {
|
|
57
|
+
const declared = configuredInvariantTestsPaths(coverage);
|
|
58
|
+
if (declared.length === 0) return false;
|
|
59
|
+
if (typeof root !== 'string' || root.length === 0) return true;
|
|
60
|
+
let sawConcrete = false;
|
|
61
|
+
for (const entry of declared) {
|
|
62
|
+
const prefix = entry.split(/[*?]/)[0].replace(/\/$/, '');
|
|
63
|
+
if (!prefix) continue;
|
|
64
|
+
sawConcrete = true;
|
|
65
|
+
const abs = path.resolve(root, prefix);
|
|
66
|
+
const rel = path.relative(path.resolve(root), abs).replace(/\\/g, '/');
|
|
67
|
+
if (!rel || rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) continue;
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(abs)) return true;
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return !sawConcrete;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Doctor residual when adopted + invariants + missing/empty tests path.
|
|
79
|
+
*
|
|
80
|
+
* @param {{
|
|
81
|
+
* adopted?: boolean,
|
|
82
|
+
* coverage?: { testGlobs?: unknown, coverageRoots?: unknown } | null,
|
|
83
|
+
* config?: object,
|
|
84
|
+
* invariants?: unknown[],
|
|
85
|
+
* hasDomainInvariants?: boolean,
|
|
86
|
+
* root?: string,
|
|
87
|
+
* }} [input]
|
|
88
|
+
* @returns {{ missing: true, ask: string, nextAction: string } | null}
|
|
89
|
+
*/
|
|
90
|
+
function residualDemandsInvariantTests(input) {
|
|
91
|
+
if (input.hasDomainInvariants === true) return true;
|
|
92
|
+
if (input.hasDomainInvariants === false) return false;
|
|
93
|
+
if (Array.isArray(input.invariants)) return catalogDemandsInvariantTestsPath(input.invariants);
|
|
94
|
+
if (typeof input.root === 'string' && input.config) {
|
|
95
|
+
try {
|
|
96
|
+
const loaded = loadEffectiveArkRulesFromDisk(input.root, input.config);
|
|
97
|
+
if (loaded.errors?.length) return false;
|
|
98
|
+
return catalogDemandsInvariantTestsPath(loaded.arkRules?.invariants);
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function collectInvariantTestsPathResidual(input = {}) {
|
|
107
|
+
if (input.adopted !== true) return null;
|
|
108
|
+
const coverage = input.coverage ?? input.config?.coverage;
|
|
109
|
+
const present =
|
|
110
|
+
typeof input.root === 'string' && input.root.length > 0
|
|
111
|
+
? declaredInvariantTestsPathPresent(input.root, coverage)
|
|
112
|
+
: hasConfiguredInvariantTestsPath(coverage);
|
|
113
|
+
const findings = collectMissingInvariantTestsPathFindings({
|
|
114
|
+
adopted: true,
|
|
115
|
+
hasDomainInvariants: residualDemandsInvariantTests(input),
|
|
116
|
+
coverage,
|
|
117
|
+
declaredPathPresent: present,
|
|
118
|
+
});
|
|
119
|
+
if (findings.length === 0) return null;
|
|
120
|
+
return {
|
|
121
|
+
missing: true,
|
|
122
|
+
ask: findings[0]?.message ?? INVARIANT_TESTS_PATH_ASK,
|
|
123
|
+
nextAction: INVARIANT_TESTS_PATH_NEXT,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* First concrete coverageRoots segment exists, or every declared glob is wildcard-only.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} root
|
|
131
|
+
* @param {{ coverageRoots?: unknown } | null | undefined} coverage
|
|
132
|
+
*/
|
|
133
|
+
export function declaredCoverageRootsPresent(root, coverage) {
|
|
134
|
+
const declared = configuredCoverageRoots(coverage);
|
|
135
|
+
if (declared.length === 0) return false;
|
|
136
|
+
if (typeof root !== 'string' || root.length === 0) return true;
|
|
137
|
+
let sawConcrete = false;
|
|
138
|
+
for (const entry of declared) {
|
|
139
|
+
const prefix = entry.split(/[*?]/)[0].replace(/\/$/, '');
|
|
140
|
+
if (!prefix) continue;
|
|
141
|
+
sawConcrete = true;
|
|
142
|
+
const abs = path.resolve(root, prefix);
|
|
143
|
+
const rel = path.relative(path.resolve(root), abs).replace(/\\/g, '/');
|
|
144
|
+
if (!rel || rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) continue;
|
|
145
|
+
try {
|
|
146
|
+
if (fs.existsSync(abs)) return true;
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return !sawConcrete;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function residualHasEnforcedInvariant(input) {
|
|
155
|
+
if (input.hasEnforcedInvariant === true) return true;
|
|
156
|
+
if (input.hasEnforcedInvariant === false) return false;
|
|
157
|
+
if (Array.isArray(input.invariants)) return catalogHasEnforcedInvariant(input.invariants);
|
|
158
|
+
const refs = input.config?.arkRules;
|
|
159
|
+
if (!refs || typeof refs !== 'object' || Object.keys(refs).length === 0) return false;
|
|
160
|
+
if (typeof input.root === 'string' && input.config) {
|
|
161
|
+
try {
|
|
162
|
+
const loaded = loadEffectiveArkRulesFromDisk(input.root, input.config);
|
|
163
|
+
if (loaded.errors?.length) return false;
|
|
164
|
+
return catalogHasEnforcedInvariant(loaded.arkRules?.invariants);
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** One doctor light: roots residual wins when both would fire. */
|
|
173
|
+
export function collectInvariantCoverageResiduals(input = {}) {
|
|
174
|
+
const invariantCoverageRoots = collectCoverageRootsResidual(input);
|
|
175
|
+
return {
|
|
176
|
+
invariantCoverageRoots,
|
|
177
|
+
invariantTestsPath: invariantCoverageRoots
|
|
178
|
+
? null
|
|
179
|
+
: collectInvariantTestsPathResidual(input),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Doctor residual when any invariant is enforced and coverageRoots is missing/empty.
|
|
185
|
+
*
|
|
186
|
+
* @param {{
|
|
187
|
+
* coverage?: { coverageRoots?: unknown } | null,
|
|
188
|
+
* config?: object,
|
|
189
|
+
* invariants?: unknown[],
|
|
190
|
+
* hasEnforcedInvariant?: boolean,
|
|
191
|
+
* root?: string,
|
|
192
|
+
* }} [input]
|
|
193
|
+
* @returns {{ missing: true, ask: string, nextAction: string } | null}
|
|
194
|
+
*/
|
|
195
|
+
export function collectCoverageRootsResidual(input = {}) {
|
|
196
|
+
const coverage = input.coverage ?? input.config?.coverage;
|
|
197
|
+
const present =
|
|
198
|
+
typeof input.root === 'string' && input.root.length > 0
|
|
199
|
+
? declaredCoverageRootsPresent(input.root, coverage)
|
|
200
|
+
: hasConfiguredCoverageRoots(coverage);
|
|
201
|
+
const findings = collectMissingCoverageRootsFindings({
|
|
202
|
+
hasEnforcedInvariant: residualHasEnforcedInvariant(input),
|
|
203
|
+
coverage,
|
|
204
|
+
declaredPathPresent: present,
|
|
205
|
+
});
|
|
206
|
+
if (findings.length === 0) return null;
|
|
207
|
+
return {
|
|
208
|
+
missing: true,
|
|
209
|
+
ask: findings[0]?.message ?? INVARIANT_COVERAGE_ROOTS_ASK,
|
|
210
|
+
nextAction: INVARIANT_COVERAGE_ROOTS_NEXT,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -91,6 +91,14 @@ export function arkCommand(root, bin, argsStr = '') {
|
|
|
91
91
|
return `${execRunner(root)} ${bin}${argsStr ? ` ${argsStr}` : ''}`;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Run an Ark binary from package `arkgate` when local bins may be missing.
|
|
96
|
+
* Bare `npx arkgate-check` 404s — npx treats that bin name as its own package.
|
|
97
|
+
*/
|
|
98
|
+
export function arkPackageRecoveryCommand(bin, argsStr = '') {
|
|
99
|
+
return `npx --package=arkgate ${bin}${argsStr ? ` ${argsStr}` : ''}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
/**
|
|
95
103
|
* Split { command, args } form for JSON/TOML configs (.mcp.json, config.toml) that spawn
|
|
96
104
|
* the binary directly. `pnpm exec ark-mcp` becomes command "pnpm" + args ["exec","ark-mcp",…]
|