arkgate 4.8.14 → 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 +172 -0
- package/README.md +17 -7
- package/bin/ark-check-runtime.mjs +17 -49
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +142 -11
- package/bin/ark.mjs +24 -33
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-skills-package.mjs +179 -1
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +43 -4
- 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 +6 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/config-contract.mjs +65 -2
- package/bin/lib/diagnostic-catalog.mjs +5 -1
- package/bin/lib/doctor-advisories.mjs +120 -10
- package/bin/lib/doctor-green-cite.mjs +139 -0
- package/bin/lib/doctor-human.mjs +115 -67
- package/bin/lib/doctor-next-actions.mjs +23 -0
- package/bin/lib/doctor-plan.mjs +35 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/field-install.mjs +47 -7
- package/bin/lib/first-run-help.mjs +9 -1
- 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 +8 -1
- package/bin/lib/improvement-compass-map.mjs +5 -1
- package/bin/lib/invariant-coverage.mjs +121 -0
- package/bin/lib/invariant-tests-path.mjs +212 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +56 -0
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/package-manager.mjs +8 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +15 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/prototype-shortcuts.mjs +224 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +15 -0
- package/bin/lib/resolved-candidate-facts.mjs +83 -66
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +24 -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/violations.mjs +13 -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-VD0qcubY.d.ts} +21 -1
- package/dist/{diagnosticCatalog-DVx_2RmF.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 +34 -34
- package/dist/index.d.ts +71 -10
- package/dist/index.js +34 -34
- 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-tGhZUiGX.d.ts → types-BSzRy2X1.d.ts} +1 -1
- package/dist/{types-Djbs3KjE.d.ts → types-D5GT5ZT8.d.ts} +3 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +8 -4
- package/docs/ai-gates.md +10 -3
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +66 -10
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +48 -4
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +7 -5
- package/docs/use.md +10 -3
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +24 -3
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +97 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +12 -3
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -2
- package/templates/agent-skills/ark-explain/SKILL.md +6 -3
- package/templates/agent-skills/ark-explore/SKILL.md +33 -1
- package/templates/agent-skills/ark-place/SKILL.md +42 -3
- package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +97 -1
- package/templates/skills/ark-autopilot.md +12 -3
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-explain.md +6 -3
- package/templates/skills/ark-explore.md +33 -1
- package/templates/skills/ark-place.md +42 -3
- package/templates/skills/ark-runtime.md +4 -2
- package/templates/skills/ark-upgrade.md +1 -1
|
@@ -180,6 +180,8 @@ export function buildBaselineHonesty(input = {}) {
|
|
|
180
180
|
* packagePinAbsent?: boolean,
|
|
181
181
|
* selfHost?: boolean,
|
|
182
182
|
* motherCli?: boolean,
|
|
183
|
+
* nativeFailClosed?: boolean | null,
|
|
184
|
+
* nativeFailClosedPolicy?: 'required' | 'unsupported' | 'none',
|
|
183
185
|
* }} [extras]
|
|
184
186
|
*/
|
|
185
187
|
export function buildWritePathHonesty(activeHost, hardWriteActive = false, extras = {}) {
|
|
@@ -202,7 +204,21 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
|
|
|
202
204
|
const hardAllowed = packageInstalled && !pinAbsentForUser;
|
|
203
205
|
const effectiveHard =
|
|
204
206
|
Boolean(hardWriteActive) && hardCapable && !softWriteHost && hardAllowed;
|
|
205
|
-
const
|
|
207
|
+
const nativeFailClosedPolicy =
|
|
208
|
+
extras.nativeFailClosedPolicy === 'required' ||
|
|
209
|
+
extras.nativeFailClosedPolicy === 'none' ||
|
|
210
|
+
extras.nativeFailClosedPolicy === 'unsupported'
|
|
211
|
+
? extras.nativeFailClosedPolicy
|
|
212
|
+
: HOST_SUPPORT_MATRIX[host]?.nativeFailClosed ?? (hardCapable ? 'unsupported' : 'none');
|
|
213
|
+
const nativeFailClosed =
|
|
214
|
+
extras.nativeFailClosed === true ? true : extras.nativeFailClosed === false ? false : null;
|
|
215
|
+
const failOpen = nativeFailClosedPolicy === 'required' && nativeFailClosed === false;
|
|
216
|
+
const honestyMessage = doctorWritePathHonestyMessage(host, effectiveHard);
|
|
217
|
+
const failOpenMessage =
|
|
218
|
+
`${HOST_SUPPORT_MATRIX[host]?.label ?? 'This host'}: the write hook is fail-open. ` +
|
|
219
|
+
'If the checker cannot run, the write still lands. Set failClosed: true on the write hook — ' +
|
|
220
|
+
'same idea as a file permission: no checker, no write. Required CI is the shared merge line.';
|
|
221
|
+
const message = failOpen ? failOpenMessage : honestyMessage;
|
|
206
222
|
|
|
207
223
|
/** @type {Record<string, unknown>} */
|
|
208
224
|
const out = {
|
|
@@ -212,6 +228,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
|
|
|
212
228
|
hardWriteSupported: hardCapable,
|
|
213
229
|
hardWriteActive: effectiveHard,
|
|
214
230
|
hardWriteUnverified: hardCapable && !effectiveHard,
|
|
231
|
+
nativeFailClosed,
|
|
232
|
+
nativeFailClosedPolicy,
|
|
215
233
|
hardMergeBoundary:
|
|
216
234
|
'required-github-status-context (CLI: arkgate-check --strict-merge / ark-check --strict-merge)',
|
|
217
235
|
packageInstalled,
|
|
@@ -385,6 +403,16 @@ export function buildProductHonesty(input = {}) {
|
|
|
385
403
|
});
|
|
386
404
|
}
|
|
387
405
|
|
|
406
|
+
if (write?.nativeFailClosed === false && write?.nativeFailClosedPolicy === 'required') {
|
|
407
|
+
reasons.push({
|
|
408
|
+
id: 'native-fail-open',
|
|
409
|
+
bucket: 'environment',
|
|
410
|
+
message:
|
|
411
|
+
write.message ||
|
|
412
|
+
'The write hook is fail-open. If the checker cannot run, the write still lands. Set failClosed: true on the write hook.',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
388
416
|
const adopted =
|
|
389
417
|
typeof input.adopted === 'string'
|
|
390
418
|
? input.adopted
|
|
@@ -442,7 +470,7 @@ export function buildProductHonesty(input = {}) {
|
|
|
442
470
|
}
|
|
443
471
|
|
|
444
472
|
// EH05: environment residual deny-list (future reason ids stay architecture debt by default).
|
|
445
|
-
const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host']);
|
|
473
|
+
const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host', 'native-fail-open']);
|
|
446
474
|
|
|
447
475
|
const environmentResiduals = reasons.filter((r) => ENVIRONMENT_REASON_IDS.has(r.id));
|
|
448
476
|
const architectureReasons = reasons.filter((r) => !ENVIRONMENT_REASON_IDS.has(r.id));
|
|
@@ -457,6 +485,8 @@ export function buildProductHonesty(input = {}) {
|
|
|
457
485
|
|
|
458
486
|
const softWriteOnly =
|
|
459
487
|
!unfinished && environmentResiduals.some((r) => r.id === 'soft-write-host');
|
|
488
|
+
const failOpenOnly =
|
|
489
|
+
!unfinished && environmentResiduals.some((r) => r.id === 'native-fail-open');
|
|
460
490
|
const hostLabel = (() => {
|
|
461
491
|
const h = typeof write?.activeHost === 'string' ? write.activeHost.trim().toLowerCase() : '';
|
|
462
492
|
if (h === 'codex') return 'Codex';
|
|
@@ -493,6 +523,10 @@ export function buildProductHonesty(input = {}) {
|
|
|
493
523
|
'This tree acked advisory-only in .ark/adoption-stance.json. That is not a required GitHub merge status.';
|
|
494
524
|
} else if (softWriteOnly) {
|
|
495
525
|
primaryMessage = `${hostLabel} local writes stay advisory/bypassable; architecture contract on this slice is ready. Hard merge boundary is a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).`;
|
|
526
|
+
} else if (failOpenOnly) {
|
|
527
|
+
primaryMessage =
|
|
528
|
+
write?.message ||
|
|
529
|
+
`${hostLabel} write hook is fail-open — if the checker cannot run, the write still lands. Set failClosed: true. Required CI is the shared merge line.`;
|
|
496
530
|
} else if (wholeTreeGoverned) {
|
|
497
531
|
primaryMessage =
|
|
498
532
|
'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.';
|
|
@@ -509,6 +543,10 @@ export function buildProductHonesty(input = {}) {
|
|
|
509
543
|
headline = wholeTreeGoverned
|
|
510
544
|
? `Architecture contract ready; ${hostLabel} local writes are advisory`
|
|
511
545
|
: `Contract residual clear; ${hostLabel} local writes are advisory`;
|
|
546
|
+
} else if (!unfinished && failOpenOnly) {
|
|
547
|
+
headline = wholeTreeGoverned
|
|
548
|
+
? `Architecture contract ready; ${hostLabel} write hook is fail-open`
|
|
549
|
+
: `Contract residual clear; ${hostLabel} write hook is fail-open`;
|
|
512
550
|
} else if (!unfinished && adopted === 'advisory-only-acked') {
|
|
513
551
|
headline = 'Advisory-only adoption — merge status is not required';
|
|
514
552
|
} else if (!unfinished) {
|
|
@@ -536,6 +574,9 @@ export function buildProductHonesty(input = {}) {
|
|
|
536
574
|
primaryNextAction =
|
|
537
575
|
input.stewardNudge?.nextAction ||
|
|
538
576
|
'/ark-adopt (ask, then update stewards[] — do not invent names)';
|
|
577
|
+
} else if (!primaryNextAction && failOpenOnly) {
|
|
578
|
+
primaryNextAction =
|
|
579
|
+
'Set failClosed: true on the write hook, then re-run doctor. Required CI is the shared merge line.';
|
|
539
580
|
} else if (!primaryNextAction && softWriteOnly) {
|
|
540
581
|
primaryNextAction =
|
|
541
582
|
'Confirm the GitHub required status context name runs arkgate-check --strict-merge (or ark-check --strict-merge). Soft-write hosts stay advisory at local write; the required status is the hard merge boundary.';
|
|
@@ -613,6 +654,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
613
654
|
adoptionStance,
|
|
614
655
|
emptyStewards,
|
|
615
656
|
stewardNudge,
|
|
657
|
+
nativeFailClosed,
|
|
658
|
+
nativeFailClosedPolicy,
|
|
616
659
|
} = {}) {
|
|
617
660
|
const coverageHonesty = buildCoverageHonesty({
|
|
618
661
|
percent: governedPercent,
|
|
@@ -632,6 +675,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
632
675
|
packagePinAbsent: packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT',
|
|
633
676
|
selfHost,
|
|
634
677
|
motherCli,
|
|
678
|
+
nativeFailClosed,
|
|
679
|
+
nativeFailClosedPolicy,
|
|
635
680
|
});
|
|
636
681
|
// Prefer explicit blocking count; fall back to activeViolations only when callers
|
|
637
682
|
// already pass blocking-only totals (legacy tests). Type-only must not invent debt.
|
|
@@ -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
|
}
|
|
@@ -11,7 +11,9 @@ ${NORTH_STAR_ONE_LINE}
|
|
|
11
11
|
|
|
12
12
|
arkgate start preview (no writes)
|
|
13
13
|
arkgate start --apply write host + CI setup
|
|
14
|
+
(refuses weak coverage/shape; lock with --archetype/--preset/--force)
|
|
14
15
|
arkgate-check --doctor status — one next step
|
|
16
|
+
(if missing: npx --package=arkgate arkgate-check --doctor)
|
|
15
17
|
|
|
16
18
|
Stuck? Run status (--doctor). Do #1.
|
|
17
19
|
`;
|
|
@@ -167,6 +169,7 @@ export function checkUsage() {
|
|
|
167
169
|
NORTH_STAR_ONE_LINE,
|
|
168
170
|
'',
|
|
169
171
|
' arkgate-check --doctor where you are: one status light, one next action',
|
|
172
|
+
' arkgate-check --local --base <ref> optional local / multi-worktree cheap check',
|
|
170
173
|
' arkgate-check --changed --base <ref> local / pre-push: checks touched files only',
|
|
171
174
|
' arkgate-check --strict-merge CI / merge gate (required GitHub status)',
|
|
172
175
|
' arkgate-check --sensors which sensors can ever be enforced (does not run analysis)',
|
|
@@ -183,7 +186,7 @@ export function checkUsageAll() {
|
|
|
183
186
|
'',
|
|
184
187
|
'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
|
|
185
188
|
' arkgate-check --version',
|
|
186
|
-
' arkgate-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--policy-base <file> | --policy-base-ref <git-ref>] [--policy-ack <file>] [--fail-on-new-smells --base-ref <git-ref>] [--contract-diff] [--contract-session] [--changed] [--against <git-ref>] [--base <git-ref>] [--persona touch|contributor|agent|steward] [--author <id>] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
189
|
+
' arkgate-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--policy-base <file> | --policy-base-ref <git-ref>] [--policy-ack <file>] [--fail-on-new-smells --base-ref <git-ref>] [--contract-diff] [--contract-session] [--local] [--changed] [--against <git-ref>] [--base <git-ref>] [--persona touch|contributor|agent|steward] [--author <id>] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
187
190
|
' ark-check --doctor [--json] [--all] [--resident] [--fail-on-new-smells --base-ref <git-ref>] compact first screen; --all prints Details; resident JSON falls back cold',
|
|
188
191
|
' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
|
|
189
192
|
' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
|
|
@@ -225,6 +228,9 @@ export function checkUsageAll() {
|
|
|
225
228
|
'',
|
|
226
229
|
'Team parliament: law files (ark.config / arkrules / .ark-baseline.json) cannot ship in',
|
|
227
230
|
'the same diff as product source. --changed --base <ref> checks touched files only.',
|
|
231
|
+
'--local (or ARK_CHECK_LOCAL=1) is the same cheap path: it turns on --changed and',
|
|
232
|
+
'stays per worktree root. It cannot combine with --strict-merge. Write hooks stay',
|
|
233
|
+
'on the lexical snippet path; they do not run a full-tree check.',
|
|
228
234
|
'--against <ref> ratchets new keys vs that ref\'s baseline. --contract-session is a',
|
|
229
235
|
'steward law-only PR. Loosen / baseline-grow need stewards[] + --author when set.',
|
|
230
236
|
'',
|
|
@@ -279,6 +285,8 @@ export function checkUsageAll() {
|
|
|
279
285
|
'--require-gates implies --strict-config and fails when the Ark contract in AGENTS.md,',
|
|
280
286
|
'the project-rooted Ark server in .mcp.json, or fail-closed CI is missing/invalid.',
|
|
281
287
|
'Included but unclassified source files therefore stay red instead of false-green.',
|
|
288
|
+
'If gates are required and there is no short decision note yet, it points at docs/adr/',
|
|
289
|
+
'(or docs/decisions/) — a hint, not a fail. Off without --require-gates.',
|
|
282
290
|
'',
|
|
283
291
|
'--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
|
|
284
292
|
'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok, antigravity',
|
|
@@ -35,6 +35,11 @@ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, re
|
|
|
35
35
|
'repair-envelope-emitted': repairEnvelopeEmitted,
|
|
36
36
|
'repair-reinjection-guaranteed': repairReinjectionGuaranteed,
|
|
37
37
|
}),
|
|
38
|
+
// Host-native "deny when the checker cannot run" flag (FS-permission analog).
|
|
39
|
+
// required = schema field must be true for hard-write evidence (Cursor failClosed).
|
|
40
|
+
// unsupported = host has no documented flag; hook-crash behavior is host-owned.
|
|
41
|
+
// none = advisory host; no local write block.
|
|
42
|
+
nativeFailClosed: extras.nativeFailClosed ?? (hardWrite ? 'unsupported' : 'none'),
|
|
38
43
|
// EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
|
|
39
44
|
operationCoverage: Object.freeze(
|
|
40
45
|
extras.operationCoverage ||
|
|
@@ -83,6 +88,7 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
83
88
|
false,
|
|
84
89
|
{
|
|
85
90
|
repairEnvelopeEmitted: true,
|
|
91
|
+
nativeFailClosed: 'required',
|
|
86
92
|
operationCoverage: {
|
|
87
93
|
Write: true,
|
|
88
94
|
StrReplace: true,
|
|
@@ -186,7 +192,9 @@ ${rows}
|
|
|
186
192
|
**Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
|
|
187
193
|
GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
|
|
188
194
|
Codex hard write covers only a complete local \`apply_patch\`; Cursor covers only listed
|
|
189
|
-
\`preToolUse\` ops
|
|
195
|
+
\`preToolUse\` ops with \`failClosed: true\`. A Cursor hook without that flag is fail-open
|
|
196
|
+
(if the checker cannot run, the write still lands). In both cases the project hook must be
|
|
197
|
+
installed + trusted, while shell/direct
|
|
190
198
|
filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
|
|
191
199
|
|
|
192
200
|
This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;
|
|
@@ -127,6 +127,8 @@ export function buildReportDepthPayload(
|
|
|
127
127
|
selfHost:
|
|
128
128
|
packageVersionTruth?.selfHost === true ||
|
|
129
129
|
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
130
|
+
nativeFailClosed: writePath?.nativeFailClosed,
|
|
131
|
+
nativeFailClosedPolicy: writePath?.nativeFailClosedPolicy,
|
|
130
132
|
});
|
|
131
133
|
const classification = {
|
|
132
134
|
governedPercent: coverage?.governed?.percent ?? null,
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { capabilityBadgesFor, renderAdvisorySections } from './html-report-advis
|
|
|
25
25
|
import { renderEvolutionSection } from './html-report-evolution.mjs';
|
|
26
26
|
import { arkGitignoreAppendDecision } from './ark-gitignore.mjs';
|
|
27
27
|
import { captureGitSnapshot } from './report-snapshot-context.mjs';
|
|
28
|
-
import { layerDescriptionCaption } from './layer-description.mjs';
|
|
28
|
+
import { layerDescriptionCaption, layerGuidanceLine, layerOwners, layerTrustBoundary } from './layer-description.mjs';
|
|
29
29
|
|
|
30
30
|
export { arkGitignoreAppendDecision, gitignoreCoversArkState, gitignoreHasArkNegationException } from './ark-gitignore.mjs';
|
|
31
31
|
|
|
@@ -423,8 +423,13 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
423
423
|
const placementRows = layers
|
|
424
424
|
.map((layer) => {
|
|
425
425
|
const purpose = layerDescriptionCaption(layer) || 'See ark.config.json';
|
|
426
|
+
const guidance = layerGuidanceLine(layer);
|
|
427
|
+
const extra = guidance && guidance !== purpose ? guidance.slice(purpose.length).replace(/^ · /, '') : '';
|
|
428
|
+
const purposeCell = extra
|
|
429
|
+
? `${esc(purpose)} <span class="tag">${esc(extra)}</span>`
|
|
430
|
+
: esc(purpose);
|
|
426
431
|
const folders = (layer.patterns || []).join(', ') || '—';
|
|
427
|
-
return `<tr><td><strong>${esc(layer.name)}</strong></td><td>${
|
|
432
|
+
return `<tr><td><strong>${esc(layer.name)}</strong></td><td>${purposeCell}</td><td><code>${esc(folders)}</code></td></tr>`;
|
|
428
433
|
})
|
|
429
434
|
.join('\n');
|
|
430
435
|
|
|
@@ -776,6 +781,12 @@ export function renderHtmlReport({
|
|
|
776
781
|
? `<span class="tag">${layer.intentPrefixes.map(esc).join(' ')}</span>`
|
|
777
782
|
: '',
|
|
778
783
|
layer.optional ? '<span class="tag dim-tag">optional</span>' : '',
|
|
784
|
+
layerTrustBoundary(layer)
|
|
785
|
+
? `<span class="tag">trust: ${esc(layerTrustBoundary(layer))}</span>`
|
|
786
|
+
: '',
|
|
787
|
+
layerOwners(layer)
|
|
788
|
+
? `<span class="tag">owner: ${esc(layerOwners(layer).map((id) => (id.includes('@') ? id : `@${id}`)).join(', '))}</span>`
|
|
789
|
+
: '',
|
|
779
790
|
].join(' ');
|
|
780
791
|
const example = exampleByLayer?.get?.(layer.name);
|
|
781
792
|
const files = counts.get(layer.name) || 0;
|
|
@@ -4,7 +4,46 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
layerForRelativePath,
|
|
9
|
+
matchingLayersForRelativePath,
|
|
10
|
+
} from '../ark-layer-match.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Same candidate list as ark-check `resolveSpecifier` (src/kernel/moduleGraph.ts).
|
|
14
|
+
* The write hook must feed `layerForRelativePath` a path with the extension
|
|
15
|
+
* ark-check already sees on disk — otherwise an explicit `money.ts` pattern
|
|
16
|
+
* loses to a broader `src/lib/**` bag.
|
|
17
|
+
*/
|
|
18
|
+
const SPECIFIER_SUFFIXES = Object.freeze([
|
|
19
|
+
'',
|
|
20
|
+
'.ts',
|
|
21
|
+
'.tsx',
|
|
22
|
+
'.mts',
|
|
23
|
+
'.cts',
|
|
24
|
+
'/index.ts',
|
|
25
|
+
'/index.tsx',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/i;
|
|
29
|
+
|
|
30
|
+
function posixRel(value) {
|
|
31
|
+
return String(value).split(/[/\\]/).join('/');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function specifierRelCandidates(rel) {
|
|
35
|
+
const base = posixRel(rel);
|
|
36
|
+
if (SOURCE_EXT.test(base)) return [base];
|
|
37
|
+
return SPECIFIER_SUFFIXES.map((suffix) => `${base}${suffix}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isOnDiskFile(root, rel) {
|
|
41
|
+
try {
|
|
42
|
+
return fs.statSync(path.join(root, rel)).isFile();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
8
47
|
|
|
9
48
|
/**
|
|
10
49
|
* Read tsconfig path aliases via the TypeScript config parser (JSONC + extends).
|
|
@@ -86,16 +125,38 @@ function filePathToRel(filePath, root) {
|
|
|
86
125
|
return relative.split(path.sep).join('/');
|
|
87
126
|
}
|
|
88
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Classify a specifier-relative path with the same specificity scorer as ark-check.
|
|
130
|
+
* Prefer an on-disk candidate; otherwise pick the candidate whose winning
|
|
131
|
+
* `layerForRelativePath` pattern scores highest (explicit file beats `src/lib/**`).
|
|
132
|
+
*/
|
|
89
133
|
function classifyProbe(root, rel, layers) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
134
|
+
const candidates = specifierRelCandidates(rel);
|
|
135
|
+
const existing = candidates.find((candidate) => isOnDiskFile(root, candidate));
|
|
136
|
+
if (existing) {
|
|
137
|
+
return {
|
|
138
|
+
relPath: existing,
|
|
139
|
+
layer: layerForRelativePath(existing, layers),
|
|
140
|
+
onDisk: true,
|
|
141
|
+
};
|
|
95
142
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
143
|
+
let bestRel = posixRel(rel);
|
|
144
|
+
let bestLayer;
|
|
145
|
+
let bestScore = -1;
|
|
146
|
+
for (const candidate of candidates) {
|
|
147
|
+
const layer = layerForRelativePath(candidate, layers);
|
|
148
|
+
if (!layer) continue;
|
|
149
|
+
const hit = matchingLayersForRelativePath(candidate, layers).find(
|
|
150
|
+
(row) => row.layer === layer
|
|
151
|
+
);
|
|
152
|
+
const score = hit?.score ?? -1;
|
|
153
|
+
if (score > bestScore) {
|
|
154
|
+
bestScore = score;
|
|
155
|
+
bestLayer = layer;
|
|
156
|
+
bestRel = candidate;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { relPath: bestRel, layer: bestLayer, onDisk: false };
|
|
99
160
|
}
|
|
100
161
|
|
|
101
162
|
/**
|
|
@@ -114,7 +175,7 @@ export function createImportTargetResolver(ts, root, config) {
|
|
|
114
175
|
if (path.isAbsolute(specifierOrFilePath)) {
|
|
115
176
|
const relPath = filePathToRel(specifierOrFilePath, root);
|
|
116
177
|
if (!relPath) return undefined;
|
|
117
|
-
return
|
|
178
|
+
return classifyProbe(root, relPath, layers);
|
|
118
179
|
}
|
|
119
180
|
|
|
120
181
|
// Relative or path-alias import
|
|
@@ -130,7 +191,7 @@ export function createImportTargetResolver(ts, root, config) {
|
|
|
130
191
|
tsAliases
|
|
131
192
|
);
|
|
132
193
|
if (!rel) return undefined;
|
|
133
|
-
return
|
|
194
|
+
return classifyProbe(root, rel, layers);
|
|
134
195
|
}
|
|
135
196
|
|
|
136
197
|
// Try as import alias / bare package first
|
|
@@ -141,13 +202,13 @@ export function createImportTargetResolver(ts, root, config) {
|
|
|
141
202
|
tsAliases
|
|
142
203
|
);
|
|
143
204
|
if (asImport) {
|
|
144
|
-
return
|
|
205
|
+
return classifyProbe(root, asImport, layers);
|
|
145
206
|
}
|
|
146
207
|
|
|
147
208
|
// Repo-relative source file path (not an import specifier)
|
|
148
209
|
const asFile = filePathToRel(specifierOrFilePath, root);
|
|
149
210
|
if (asFile) {
|
|
150
|
-
return
|
|
211
|
+
return classifyProbe(root, asFile, layers);
|
|
151
212
|
}
|
|
152
213
|
|
|
153
214
|
return undefined;
|
|
@@ -42,7 +42,14 @@ export function buildDoctorImprovementCompass(input = {}) {
|
|
|
42
42
|
if (id.includes('PEER_ISOLATION')) peerIsolationCount += 1;
|
|
43
43
|
if (id === 'CAPABILITY_VIOLATION') pureOrCapabilityResidual += 1;
|
|
44
44
|
if (id === 'FORBIDDEN_GLOBAL' || id.startsWith('FORBIDDEN_')) forbiddenGlobalResidual += 1;
|
|
45
|
-
if (
|
|
45
|
+
if (
|
|
46
|
+
id.startsWith('ARKRULE_') ||
|
|
47
|
+
id === 'INVARIANT_UNCOVERED' ||
|
|
48
|
+
id === 'INVARIANT_CATALOG_EMPTY' ||
|
|
49
|
+
id === 'INVARIANT_TESTS_PATH_MISSING' ||
|
|
50
|
+
id === 'INVARIANT_COVERAGE_ROOTS_MISSING'
|
|
51
|
+
)
|
|
52
|
+
arkRulesStructureResidual += 1;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
const pcFindings = input.physicalCohesion?.findings;
|
|
@@ -260,7 +260,11 @@ function mapViolations(byId, violations) {
|
|
|
260
260
|
});
|
|
261
261
|
continue;
|
|
262
262
|
}
|
|
263
|
-
if (upper.startsWith('ARKRULE_') ||
|
|
263
|
+
if (upper.startsWith('ARKRULE_') ||
|
|
264
|
+
upper === 'INVARIANT_UNCOVERED' ||
|
|
265
|
+
upper === 'INVARIANT_CATALOG_EMPTY' ||
|
|
266
|
+
upper === 'INVARIANT_TESTS_PATH_MISSING' ||
|
|
267
|
+
upper === 'INVARIANT_COVERAGE_ROOTS_MISSING') {
|
|
264
268
|
attach('encapsulation', 'ArkRules structure / invariant residual inside a layer.', {
|
|
265
269
|
kind: 'skill',
|
|
266
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
|
+
}
|