arkgate 4.8.23 → 4.8.25
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 +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +5 -1
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
execRunner,
|
|
11
11
|
presentLockfiles,
|
|
12
12
|
} from '../ark-shared.mjs';
|
|
13
|
+
import { npxArkgatePrefixLength } from './package-manager.mjs';
|
|
13
14
|
import {
|
|
14
15
|
codexPromptsDir,
|
|
15
16
|
codexSkillsDir,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
grokHooks,
|
|
30
31
|
grokProjectConfig,
|
|
31
32
|
mergeAntigravityArkHook,
|
|
33
|
+
mergeClaudeStyleArkHooks,
|
|
32
34
|
mergeCursorArkHook,
|
|
33
35
|
mergeOpencodeArkMcp,
|
|
34
36
|
opencodeProjectConfig,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
compactRouterHost,
|
|
40
42
|
mergeArkMcpJson,
|
|
41
43
|
MCP_JSON_GATE_FILES,
|
|
44
|
+
CLAUDE_STYLE_HOOK_FILES,
|
|
42
45
|
writeTemplate,
|
|
43
46
|
} from './gate-files.mjs';
|
|
44
47
|
import {
|
|
@@ -119,7 +122,10 @@ export function staleRunnerGateFiles(root) {
|
|
|
119
122
|
continue;
|
|
120
123
|
}
|
|
121
124
|
const ark = json?.mcpServers?.ark;
|
|
122
|
-
if (ark
|
|
125
|
+
if (!ark || !ark.command) continue;
|
|
126
|
+
// Unpinned project: a bare `npx <bin>` MCP entry 404s the same way a text command does.
|
|
127
|
+
const barePinnedNpx = want.startsWith('npx ') && npxArkgatePrefixLength(ark.args) === 0;
|
|
128
|
+
if (ark.command !== want.split(' ')[0] || barePinnedNpx) stale.push(rel);
|
|
123
129
|
}
|
|
124
130
|
return stale;
|
|
125
131
|
}
|
|
@@ -341,6 +347,18 @@ export function runMigrateCommands(root) {
|
|
|
341
347
|
warnLockfileConflict(root);
|
|
342
348
|
}
|
|
343
349
|
|
|
350
|
+
/** Shared host files Ark merges into (keeps sibling servers / hooks / keys). */
|
|
351
|
+
function mergeableGateFile(relativePath) {
|
|
352
|
+
if (relativePath === 'opencode.json') return { fn: mergeOpencodeArkMcp, forceReplaces: false };
|
|
353
|
+
if (relativePath === '.cursor/hooks.json') return { fn: mergeCursorArkHook, forceReplaces: false };
|
|
354
|
+
if (relativePath === '.agents/hooks.json') return { fn: mergeAntigravityArkHook, forceReplaces: false };
|
|
355
|
+
if (CLAUDE_STYLE_HOOK_FILES.includes(relativePath)) {
|
|
356
|
+
return { fn: mergeClaudeStyleArkHooks, forceReplaces: true };
|
|
357
|
+
}
|
|
358
|
+
if (MCP_JSON_GATE_FILES.includes(relativePath)) return { fn: mergeArkMcpJson, forceReplaces: true };
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
344
362
|
export function runInstallAgentGates(args) {
|
|
345
363
|
const root = args.root;
|
|
346
364
|
if (args.migrateCommands) {
|
|
@@ -454,6 +472,11 @@ export function runInstallAgentGates(args) {
|
|
|
454
472
|
`Added package.json script "typecheck": "${typecheckBootstrap.script}" (tsconfig present; local/CI parity).`
|
|
455
473
|
);
|
|
456
474
|
}
|
|
475
|
+
if (typecheckBootstrap.reason === 'no-typescript' && !args.compact && !args.json) {
|
|
476
|
+
console.log(
|
|
477
|
+
'tsconfig/jsconfig found but typescript is not a dependency — skipped the "typecheck" script and CI step (add typescript as a devDependency, then "typecheck": "tsc --noEmit").'
|
|
478
|
+
);
|
|
479
|
+
}
|
|
457
480
|
}
|
|
458
481
|
const catalog = buildManagedAssetCatalog({
|
|
459
482
|
root,
|
|
@@ -518,81 +541,24 @@ export function runInstallAgentGates(args) {
|
|
|
518
541
|
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
519
542
|
return writeTemplate(root, relativePath, merged, true);
|
|
520
543
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
try {
|
|
525
|
-
existing = fs.readFileSync(fullPath, 'utf8');
|
|
526
|
-
} catch {
|
|
527
|
-
// Missing project config → write the generated Ark MCP block.
|
|
528
|
-
}
|
|
529
|
-
if (!existing) {
|
|
530
|
-
return writeTemplate(root, relativePath, content, true);
|
|
531
|
-
}
|
|
532
|
-
const merged = mergeOpencodeArkMcp(existing, content);
|
|
533
|
-
if (merged == null) {
|
|
534
|
-
return { relativePath, status: 'skipped-non-ark' };
|
|
535
|
-
}
|
|
536
|
-
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
537
|
-
return writeTemplate(root, relativePath, merged, true);
|
|
538
|
-
}
|
|
539
|
-
if (relativePath === '.cursor/hooks.json') {
|
|
540
|
-
const fullPath = path.join(root, relativePath);
|
|
541
|
-
let existing = '';
|
|
542
|
-
try {
|
|
543
|
-
existing = fs.readFileSync(fullPath, 'utf8');
|
|
544
|
-
} catch {
|
|
545
|
-
// Missing hooks file → write generated Cursor preToolUse gate.
|
|
546
|
-
}
|
|
547
|
-
if (!existing) {
|
|
548
|
-
return writeTemplate(root, relativePath, content, true);
|
|
549
|
-
}
|
|
550
|
-
const merged = mergeCursorArkHook(existing, content);
|
|
551
|
-
if (merged == null) {
|
|
552
|
-
return { relativePath, status: 'skipped-non-ark' };
|
|
553
|
-
}
|
|
554
|
-
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
555
|
-
// Upsert Ark preToolUse without requiring --force; never wipe sibling hooks.
|
|
556
|
-
return writeTemplate(root, relativePath, merged, true);
|
|
557
|
-
}
|
|
558
|
-
if (relativePath === '.agents/hooks.json') {
|
|
559
|
-
const fullPath = path.join(root, relativePath);
|
|
560
|
-
let existing = '';
|
|
561
|
-
try {
|
|
562
|
-
existing = fs.readFileSync(fullPath, 'utf8');
|
|
563
|
-
} catch {
|
|
564
|
-
// Missing hooks file → write generated ark-write-gate map.
|
|
565
|
-
}
|
|
566
|
-
if (!existing) {
|
|
567
|
-
return writeTemplate(root, relativePath, content, true);
|
|
568
|
-
}
|
|
569
|
-
const merged = mergeAntigravityArkHook(existing, content);
|
|
570
|
-
if (merged == null) {
|
|
571
|
-
return { relativePath, status: 'skipped-non-ark' };
|
|
572
|
-
}
|
|
573
|
-
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
574
|
-
// Upsert ark-write-gate without requiring --force; never wipe sibling named hooks.
|
|
575
|
-
return writeTemplate(root, relativePath, merged, true);
|
|
576
|
-
}
|
|
577
|
-
if (MCP_JSON_GATE_FILES.includes(relativePath)) {
|
|
578
|
-
const fullPath = path.join(root, relativePath);
|
|
544
|
+
const merge = mergeableGateFile(relativePath);
|
|
545
|
+
if (merge) {
|
|
546
|
+
// Upsert Ark's entry into a shared host file without --force; never wipe sibling keys.
|
|
579
547
|
let existing = '';
|
|
580
548
|
try {
|
|
581
|
-
existing = fs.readFileSync(
|
|
549
|
+
existing = fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
582
550
|
} catch {
|
|
583
|
-
// Missing
|
|
584
|
-
}
|
|
585
|
-
if (!existing) {
|
|
586
|
-
return writeTemplate(root, relativePath, content, true);
|
|
551
|
+
// Missing file → write the generated Ark template.
|
|
587
552
|
}
|
|
588
|
-
|
|
553
|
+
if (!existing) return writeTemplate(root, relativePath, content, true);
|
|
554
|
+
const merged = merge.fn(existing, content);
|
|
589
555
|
if (merged == null) {
|
|
590
|
-
|
|
556
|
+
// Unmergeable file: never guess. --force replaces it only where that is explicit consent.
|
|
557
|
+
return merge.forceReplaces && args.force
|
|
591
558
|
? writeTemplate(root, relativePath, content, true)
|
|
592
559
|
: { relativePath, status: 'skipped-non-ark' };
|
|
593
560
|
}
|
|
594
561
|
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
595
|
-
// Upsert mcpServers.ark without requiring --force; never wipe sibling servers.
|
|
596
562
|
return writeTemplate(root, relativePath, merged, true);
|
|
597
563
|
}
|
|
598
564
|
return writeTemplate(
|
|
@@ -794,14 +760,25 @@ export function runInstallAgentGates(args) {
|
|
|
794
760
|
}
|
|
795
761
|
if (tools.has('codex') && !args.compact) {
|
|
796
762
|
console.log(' Codex write path (honest):');
|
|
797
|
-
console.log(' - Local:
|
|
798
|
-
console.log(' -
|
|
799
|
-
console.log(' -
|
|
763
|
+
console.log(' - Local: .codex/hooks.json PreToolUse hard-blocks a complete local apply_patch (Codex CLI / local Desktop) once the project is trusted and a hook run is runtime-observed; on disk it is configured, not verified.');
|
|
764
|
+
console.log(' - Outside that claim (hosted tools, hook opt-out, shell writes, incomplete patch reconstruction): CI --strict-merge + required status check.');
|
|
765
|
+
console.log(' - Repair envelopes may emit, but Codex reinjection is not guaranteed (unlike Claude/Grok hard-write + repair).');
|
|
800
766
|
}
|
|
801
767
|
if (args.codexHome) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
768
|
+
// Report what the home write actually did (it is skipped when the project catalog exists).
|
|
769
|
+
if (homeResults.some((r) => r.status === 'written')) {
|
|
770
|
+
console.log(
|
|
771
|
+
` Codex: refreshed home skills under ${codexSkillsDir()}/<name>/SKILL.md (Codex skill catalog).`
|
|
772
|
+
);
|
|
773
|
+
} else if (homeResults.some((r) => r.status === 'failed')) {
|
|
774
|
+
console.log(` Codex: home skills NOT written under ${codexSkillsDir()} (see failures above).`);
|
|
775
|
+
} else if (homeResults.length === 0) {
|
|
776
|
+
console.log(
|
|
777
|
+
' Codex: home skills not written — project .agents/skills is the catalog (Codex reads repo scope).'
|
|
778
|
+
);
|
|
779
|
+
} else {
|
|
780
|
+
console.log(` Codex: home skills under ${codexSkillsDir()} already current (no changes).`);
|
|
781
|
+
}
|
|
805
782
|
} else if (tools.has('codex') && skills.length > 0 && !args.compact) {
|
|
806
783
|
console.log(
|
|
807
784
|
' Codex: project skills at `.agents/skills/<name>/SKILL.md` (Agent Skills REPO scope).'
|
|
@@ -10,7 +10,7 @@ const DEFAULT_TEST_NAME_RE =
|
|
|
10
10
|
/\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$|\/__tests__\/|\/tests?\//i;
|
|
11
11
|
|
|
12
12
|
/** Default max files to load for coverage evidence (budget). Config: `coverage.maxFiles`. */
|
|
13
|
-
|
|
13
|
+
const DEFAULT_MAX_COVERAGE_FILES = 400;
|
|
14
14
|
/**
|
|
15
15
|
* Hard ceiling on `coverage.maxFiles`. The config validator implements no
|
|
16
16
|
* `maximum` keyword for integers, so a schema bound would be accepted and then
|
|
@@ -38,11 +38,12 @@ function isPathInsideRoot(root, absolute) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Minimal glob match for testGlobs
|
|
41
|
+
* Minimal glob match for testGlobs and ArkRules `appliesTo` fileHints
|
|
42
|
+
* (double-star slash = zero path segments).
|
|
42
43
|
* @param {string} glob
|
|
43
44
|
* @param {string} file
|
|
44
45
|
*/
|
|
45
|
-
function matchSimpleGlob(glob, file) {
|
|
46
|
+
export function matchSimpleGlob(glob, file) {
|
|
46
47
|
const pattern = String(glob || '').replace(/\\/g, '/');
|
|
47
48
|
const target = String(file || '').replace(/\\/g, '/');
|
|
48
49
|
if (!pattern) return false;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/invariant-coverage.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { findClassDeclarations, findMemberContainers, objectLiteralMemberNames, scanClassMembers, } from './class-source-scan.mjs';
|
|
12
|
+
import { probeRefusalReason, verdictRefusesPromotion } from './invariant-probe.mjs';
|
|
11
13
|
/** Adopted + catalogued invariants, but no declared tests path (P2 §10). */
|
|
12
14
|
export const INVARIANT_TESTS_PATH_RULE_ID = 'INVARIANT_TESTS_PATH_MISSING';
|
|
13
15
|
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.';
|
|
@@ -185,6 +187,38 @@ function declarationShape(content, name) {
|
|
|
185
187
|
}
|
|
186
188
|
return undefined;
|
|
187
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* `Name.member` coverage: the member must be declared inside the body of
|
|
192
|
+
* `class Name` (exported, default-exported, abstract, or local), a
|
|
193
|
+
* `const Name = class { … }` expression, a `namespace Name { … }` (function or
|
|
194
|
+
* const member), or a `const Name = { … }` object literal, in this file. A
|
|
195
|
+
* member of another container, a free function, or a file that merely mentions
|
|
196
|
+
* the name does not count.
|
|
197
|
+
*/
|
|
198
|
+
function classMemberShape(content, className, member) {
|
|
199
|
+
const hasClassMethod = (body) => scanClassMembers(body).members.some((m) => m.kind === 'method' && m.name === member);
|
|
200
|
+
for (const decl of findClassDeclarations(content)) {
|
|
201
|
+
if (decl.name !== className || decl.bodyStart == null)
|
|
202
|
+
continue;
|
|
203
|
+
if (hasClassMethod(content.slice(decl.bodyStart, decl.bodyEnd ?? content.length))) {
|
|
204
|
+
return 'method';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const container of findMemberContainers(content, className)) {
|
|
208
|
+
const body = content.slice(container.bodyStart, container.bodyEnd);
|
|
209
|
+
if (container.kind === 'class-expression' && hasClassMethod(body))
|
|
210
|
+
return 'method';
|
|
211
|
+
if (container.kind === 'object' && objectLiteralMemberNames(body).includes(member)) {
|
|
212
|
+
return 'method';
|
|
213
|
+
}
|
|
214
|
+
if (container.kind === 'namespace') {
|
|
215
|
+
const shape = declarationShape(body, member);
|
|
216
|
+
if (shape === 'function' || shape === 'const')
|
|
217
|
+
return shape;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
188
222
|
function lineAt(content, index) {
|
|
189
223
|
const start = content.lastIndexOf('\n', index - 1) + 1;
|
|
190
224
|
const end = content.indexOf('\n', index);
|
|
@@ -298,7 +332,8 @@ function symbolNeedle(symbol) {
|
|
|
298
332
|
const name = parts[parts.length - 1] ?? '';
|
|
299
333
|
if (!name)
|
|
300
334
|
return undefined;
|
|
301
|
-
|
|
335
|
+
// `Aggregate.method` (or `ns.Aggregate.method`): the segment before the member names the class.
|
|
336
|
+
const className = parts.length > 1 ? (parts[parts.length - 2] || null) : null;
|
|
302
337
|
return { className, name };
|
|
303
338
|
}
|
|
304
339
|
function declaredLabel(invariant) {
|
|
@@ -344,9 +379,9 @@ function rankCoverage(invariant, files, scanTests = true) {
|
|
|
344
379
|
const content = files.fileContents[file];
|
|
345
380
|
if (!content)
|
|
346
381
|
continue;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
382
|
+
const shape = needle.className
|
|
383
|
+
? classMemberShape(content, needle.className, needle.name)
|
|
384
|
+
: declarationShape(content, needle.name);
|
|
350
385
|
if (!shape)
|
|
351
386
|
continue;
|
|
352
387
|
declaration = { file, shape };
|
|
@@ -513,10 +548,14 @@ export function evaluateInvariantCoverage(input) {
|
|
|
513
548
|
// Covered if the policy says this verdict counts.
|
|
514
549
|
// When coverage declares neither test nor symbol, require at least description-only advisory presence = not covered.
|
|
515
550
|
const requiresEvidence = inv.coverage?.test === true || Boolean(symbol) || inv.coverage === undefined;
|
|
551
|
+
// `coverage.test: false` without a symbol declares zero evidence. That opt-out is
|
|
552
|
+
// advisory-only: on an enforced invariant it is reported uncovered (never a false
|
|
553
|
+
// green), not refused at load, so the rest of the contract still evaluates.
|
|
554
|
+
const zeroEvidenceOptOut = inv.coverage?.test === false && !symbol;
|
|
516
555
|
const covered = requiresEvidence && counts
|
|
517
556
|
? true
|
|
518
|
-
: inv.
|
|
519
|
-
? true // explicitly no coverage requirements
|
|
557
|
+
: zeroEvidenceOptOut && inv.mode !== 'enforced'
|
|
558
|
+
? true // explicitly no coverage requirements (advisory-only opt-out)
|
|
520
559
|
: counts;
|
|
521
560
|
// Partial only when tests are missing *and* no other evidence (e.g. symbol) completed coverage.
|
|
522
561
|
const partial = testGlobsMissing && wantsTest && !counts;
|
|
@@ -562,11 +601,13 @@ export function evaluateInvariantCoverage(input) {
|
|
|
562
601
|
? coverageBudgetExhausted
|
|
563
602
|
? `Invariant ${inv.id} coverage cannot be proven (${budgetDetail}); reporting partial, not covered.`
|
|
564
603
|
: `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered (never-had-tests).`
|
|
565
|
-
:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
:
|
|
604
|
+
: zeroEvidenceOptOut
|
|
605
|
+
? `Invariant ${inv.id} is enforced but declares no evidence (coverage.test is false and no coverage.symbol); set coverage.symbol, drop test:false, or keep mode "advisory".`
|
|
606
|
+
: // The sentence is the verdict. It names a title, a declaration
|
|
607
|
+
// shape, a bare mention, or silence — it does not re-scan.
|
|
608
|
+
`Invariant ${inv.id}: ${describeCoverage(inv, ev)} (${kind === 'tests-disappeared'
|
|
609
|
+
? 'tests-disappeared — a suite exists'
|
|
610
|
+
: 'never-had-tests — the scan found no tests at all'}). ArkGate matches declared text; it never executes tests.`) +
|
|
570
611
|
(partial && coverageBudgetExhausted ? budgetExhaustedTail : discardTail),
|
|
571
612
|
file: inv.provenance.sourceFile,
|
|
572
613
|
line: 1,
|
|
@@ -608,6 +649,14 @@ export function canPromoteInvariant(coverage) {
|
|
|
608
649
|
reason: `Invariant ${coverage.invariantId} is uncovered; add a test title or symbol before promoting to enforced.`,
|
|
609
650
|
};
|
|
610
651
|
}
|
|
652
|
+
// A `coverage.test: false` opt-out without a symbol reports covered with no
|
|
653
|
+
// evidence at all. That is fine while advisory; it is not a basis for enforcing.
|
|
654
|
+
if (Array.isArray(coverage.evidence) && coverage.evidence.length === 0) {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
reason: `Invariant ${coverage.invariantId} declares no evidence (coverage.test:false and no symbol); add coverage.symbol or a test title before promoting to enforced.`,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
611
660
|
// Promotion is the moment coverage stops being advice, so an evidence file
|
|
612
661
|
// the project itself says its runner does not walk cannot carry it.
|
|
613
662
|
if (coverage.outsideDeclaredRoots === true) {
|
|
@@ -624,6 +673,15 @@ export function canPromoteInvariant(coverage) {
|
|
|
624
673
|
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.`,
|
|
625
674
|
};
|
|
626
675
|
}
|
|
676
|
+
// ADR 0039 D2: probe evidence can only subtract. A fresh survived or
|
|
677
|
+
// not-reached row refuses; stale, killed, inconclusive or absent changes nothing.
|
|
678
|
+
if (coverage.probe?.fresh === true && verdictRefusesPromotion(coverage.probe.verdict)) {
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
reason: probeRefusalReason(coverage.invariantId, coverage.probe),
|
|
682
|
+
blocker: 'probe-survived',
|
|
683
|
+
};
|
|
684
|
+
}
|
|
627
685
|
return { ok: true, reason: `Invariant ${coverage.invariantId} has coverage evidence.` };
|
|
628
686
|
}
|
|
629
687
|
function nonEmptyPathStrings(value) {
|
|
@@ -732,3 +790,19 @@ export function collectMissingCoverageRootsFindings(input) {
|
|
|
732
790
|
},
|
|
733
791
|
];
|
|
734
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Test files whose describe/it/test/context title names `invariant.id` — the
|
|
795
|
+
* same title matcher coverage uses. Sorted, deduplicated. The probe (ADR 0039)
|
|
796
|
+
* runs these as covering tests; coverage itself never runs anything.
|
|
797
|
+
*/
|
|
798
|
+
export function testFilesNamingInvariant(invariant, files) {
|
|
799
|
+
const out = new Set();
|
|
800
|
+
for (const file of files.testFiles ?? []) {
|
|
801
|
+
const content = files.fileContents[file];
|
|
802
|
+
if (!content)
|
|
803
|
+
continue;
|
|
804
|
+
if (matchTestTitle(content, invariant.id) !== undefined)
|
|
805
|
+
out.add(normalizePath(file));
|
|
806
|
+
}
|
|
807
|
+
return [...out].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
808
|
+
}
|