@guilz-dev/belay 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +60 -10
  2. package/dist/adapters/shared/gate-runtime.d.ts +5 -0
  3. package/dist/adapters/shared/gate-runtime.js +237 -33
  4. package/dist/bundle/claude-runtime.mjs +4082 -1230
  5. package/dist/bundle/codex-runtime.mjs +4057 -1205
  6. package/dist/bundle/cursor-runtime.mjs +4057 -1205
  7. package/dist/commands/classify-for-report.js +5 -0
  8. package/dist/commands/doctor.js +90 -27
  9. package/dist/commands/explain.js +3 -2
  10. package/dist/commands/metrics.js +75 -4
  11. package/dist/commands/quality.js +2 -6
  12. package/dist/commands/recovery-checkpoints.d.ts +8 -1
  13. package/dist/commands/recovery-checkpoints.js +44 -11
  14. package/dist/commands/session.d.ts +5 -0
  15. package/dist/commands/session.js +8 -1
  16. package/dist/commands/status.js +1 -1
  17. package/dist/config-io.js +2 -2
  18. package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
  19. package/dist/conformance/contained-execution-guarantee.js +121 -0
  20. package/dist/conformance/guarantee-table.js +7 -0
  21. package/dist/core/audit-io.js +8 -0
  22. package/dist/core/audit-metrics.d.ts +34 -0
  23. package/dist/core/audit-metrics.js +87 -15
  24. package/dist/core/audit-recovery-metrics.d.ts +33 -0
  25. package/dist/core/audit-recovery-metrics.js +146 -0
  26. package/dist/core/audit-types.d.ts +7 -1
  27. package/dist/core/audit-types.js +1 -1
  28. package/dist/core/bounded-output.d.ts +11 -0
  29. package/dist/core/bounded-output.js +26 -0
  30. package/dist/core/capability/attestation.d.ts +56 -0
  31. package/dist/core/capability/attestation.js +110 -3
  32. package/dist/core/capability/boundary-session.d.ts +5 -0
  33. package/dist/core/capability/boundary-session.js +38 -1
  34. package/dist/core/capability/index.d.ts +1 -1
  35. package/dist/core/capability/index.js +1 -1
  36. package/dist/core/config.d.ts +18 -0
  37. package/dist/core/config.js +89 -10
  38. package/dist/core/contained-execution/docker-policy.d.ts +18 -0
  39. package/dist/core/contained-execution/docker-policy.js +97 -0
  40. package/dist/core/contained-execution/docker.d.ts +223 -0
  41. package/dist/core/contained-execution/docker.js +846 -0
  42. package/dist/core/contained-execution/eligibility.d.ts +9 -0
  43. package/dist/core/contained-execution/eligibility.js +103 -0
  44. package/dist/core/contained-execution/failure.d.ts +18 -0
  45. package/dist/core/contained-execution/failure.js +105 -0
  46. package/dist/core/contained-execution/mirror.d.ts +54 -0
  47. package/dist/core/contained-execution/mirror.js +587 -0
  48. package/dist/core/contained-execution/policy.d.ts +10 -0
  49. package/dist/core/contained-execution/policy.js +20 -0
  50. package/dist/core/effect-ir/index.d.ts +1 -1
  51. package/dist/core/effect-ir/index.js +1 -1
  52. package/dist/core/effect-ir/normalize.d.ts +5 -0
  53. package/dist/core/effect-ir/normalize.js +17 -7
  54. package/dist/core/effect-ir/shell-build.js +2 -14
  55. package/dist/core/effect-ir/shell-lower.js +100 -25
  56. package/dist/core/gate-contract.d.ts +4 -1
  57. package/dist/core/gate-contract.js +2 -0
  58. package/dist/core/process-runner.d.ts +10 -3
  59. package/dist/core/process-runner.js +66 -10
  60. package/dist/core/recovery/checkpoint.d.ts +3 -0
  61. package/dist/core/recovery/checkpoint.js +33 -4
  62. package/dist/core/recovery/types.d.ts +1 -0
  63. package/dist/core/scrub.d.ts +17 -0
  64. package/dist/core/scrub.js +499 -5
  65. package/dist/core/standing-allow.d.ts +0 -20
  66. package/dist/core/standing-allow.js +0 -71
  67. package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
  68. package/dist/core/transactional/apply-observed-changes.js +11 -0
  69. package/dist/core/transactional/backend-selector.d.ts +2 -1
  70. package/dist/core/transactional/backend-selector.js +32 -29
  71. package/dist/core/transactional/backend.d.ts +3 -0
  72. package/dist/core/transactional/file-checkpoint-backend.js +242 -54
  73. package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
  74. package/dist/core/transactional/file-checkpoint-git.js +53 -0
  75. package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
  76. package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
  77. package/dist/core/transactional/git-worktree-backend.js +3 -0
  78. package/dist/core/transactional/git-worktree.d.ts +1 -0
  79. package/dist/core/transactional/git-worktree.js +1 -0
  80. package/dist/core/transactional/runner.js +59 -13
  81. package/dist/core/types.d.ts +13 -0
  82. package/dist/core/verdict/parser.d.ts +10 -0
  83. package/dist/core/verdict/parser.js +271 -40
  84. package/dist/core/verdict/shell-semantics.js +21 -0
  85. package/dist/corpus/adversarial-probe.d.ts +1 -1
  86. package/dist/corpus/adversarial-probe.js +3 -3
  87. package/dist/corpus/benign-probe-cores.d.ts +6 -0
  88. package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
  89. package/dist/corpus/coverage-compare.d.ts +36 -0
  90. package/dist/corpus/coverage-compare.js +197 -0
  91. package/dist/corpus/coverage-contexts.d.ts +17 -0
  92. package/dist/corpus/coverage-contexts.js +88 -0
  93. package/dist/corpus/coverage-matrix.d.ts +37 -0
  94. package/dist/corpus/coverage-matrix.js +159 -0
  95. package/dist/corpus/coverage-probe.d.ts +91 -0
  96. package/dist/corpus/coverage-probe.js +341 -0
  97. package/dist/corpus/evaluate.d.ts +1 -1
  98. package/dist/corpus/evaluate.js +1 -1
  99. package/dist/corpus/structural-fixture-root.d.ts +2 -0
  100. package/dist/corpus/structural-fixture-root.js +5 -0
  101. package/dist/operational-insights.d.ts +3 -0
  102. package/dist/operational-insights.js +7 -3
  103. package/dist/runtime-provenance.d.ts +8 -0
  104. package/dist/runtime-provenance.js +34 -0
  105. package/dist/types.d.ts +3 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +3 -1
  109. package/skills/belay/SKILL.md +19 -0
  110. package/skills/belay/belay-status.md +5 -0
  111. package/dist/corpus/must-allow-commands.d.ts +0 -6
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Structural-suite MUST-ALLOW CI expectations.
2
+ * Structural-suite benign probe cores.
3
3
  * These commands guard classifier availability in tests and never grant runtime authority.
4
4
  * @see src/__tests__/verdict/structural-suite.test.ts
5
5
  */
6
- export const MUST_ALLOW_SHELL_COMMANDS = [
6
+ export const BENIGN_PROBE_CORES = [
7
7
  'npm test',
8
8
  'npm run build',
9
9
  'pnpm test',
@@ -11,4 +11,10 @@ export const MUST_ALLOW_SHELL_COMMANDS = [
11
11
  'pnpm vitest run src/example.test.ts',
12
12
  "bash -lc 'git status'",
13
13
  'belay approve belay_deadbeef1234',
14
+ 'bundle -v',
15
+ 'ruby -v',
16
+ 'yarn --version',
17
+ 'make -n test',
18
+ 'bin/rails routes',
19
+ 'bundle exec rubocop --version',
14
20
  ];
@@ -0,0 +1,36 @@
1
+ import type { CoverageContextId } from './coverage-matrix.js';
2
+ import type { CoverageProbeReport } from './coverage-probe.js';
3
+ export declare class CoverageCompareError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export type CoverageCompareKind = 'fixture_change' | 'classifier_drift' | 'added' | 'removed';
7
+ export interface CoverageCompareEntry {
8
+ caseId: string;
9
+ context: CoverageContextId;
10
+ kind: CoverageCompareKind;
11
+ beforeVerdict?: string;
12
+ afterVerdict?: string;
13
+ beforeReason?: string;
14
+ afterReason?: string;
15
+ beforeCommandHash?: string;
16
+ afterCommandHash?: string;
17
+ beforeExpectationHash?: string | null;
18
+ afterExpectationHash?: string | null;
19
+ }
20
+ export interface CoverageContextConfigDrift {
21
+ context: CoverageContextId;
22
+ beforeHash: string;
23
+ afterHash: string;
24
+ }
25
+ export interface CoverageCompareReport {
26
+ schemaVersion: 1;
27
+ matrixHashChanged: boolean;
28
+ beforeMatrixHash: string;
29
+ afterMatrixHash: string;
30
+ configDrift: CoverageContextConfigDrift[];
31
+ entries: CoverageCompareEntry[];
32
+ }
33
+ export declare function parseCoverageProbeReportForCompare(raw: unknown): CoverageProbeReport;
34
+ export declare function compareBaselineWarnings(baseline: CoverageProbeReport, current: CoverageProbeReport): string[];
35
+ export declare function compareCoverageReports(baseline: CoverageProbeReport, current: CoverageProbeReport): CoverageCompareReport;
36
+ export declare function formatCoverageCompareReport(report: CoverageCompareReport): string;
@@ -0,0 +1,197 @@
1
+ const SUPPORTED_COMPARE_REPORT_SCHEMA_VERSIONS = [1, 2];
2
+ export class CoverageCompareError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'CoverageCompareError';
6
+ }
7
+ }
8
+ function resultKey(caseId, context) {
9
+ return `${caseId}::${context}`;
10
+ }
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function normalizeCaseResult(raw) {
15
+ if (!isRecord(raw)) {
16
+ throw new CoverageCompareError('--compare baseline result row is invalid');
17
+ }
18
+ if (typeof raw.caseId !== 'string' || typeof raw.context !== 'string') {
19
+ throw new CoverageCompareError('--compare baseline result row is missing caseId or context');
20
+ }
21
+ if (!isRecord(raw.actual)) {
22
+ throw new CoverageCompareError('--compare baseline result row is missing actual verdict');
23
+ }
24
+ const expectationHash = raw.expectationHash === undefined || raw.expectationHash === null
25
+ ? null
26
+ : String(raw.expectationHash);
27
+ const verdict = String(raw.actual.verdict);
28
+ return {
29
+ ...raw,
30
+ expectationHash,
31
+ actual: {
32
+ verdict,
33
+ reason: String(raw.actual.reason ?? ''),
34
+ fingerprint: String(raw.actual.fingerprint ?? ''),
35
+ },
36
+ };
37
+ }
38
+ export function parseCoverageProbeReportForCompare(raw) {
39
+ if (!isRecord(raw)) {
40
+ throw new CoverageCompareError('--compare baseline must be a JSON object');
41
+ }
42
+ if (!Array.isArray(raw.results)) {
43
+ throw new CoverageCompareError('--compare baseline is missing results[]');
44
+ }
45
+ const schemaVersion = raw.reportSchemaVersion ?? 1;
46
+ if (typeof schemaVersion !== 'number' ||
47
+ !SUPPORTED_COMPARE_REPORT_SCHEMA_VERSIONS.includes(schemaVersion)) {
48
+ throw new CoverageCompareError(`--compare baseline reportSchemaVersion is unsupported: ${JSON.stringify(raw.reportSchemaVersion)}`);
49
+ }
50
+ if (!Array.isArray(raw.contexts)) {
51
+ throw new CoverageCompareError('--compare baseline is missing contexts[]');
52
+ }
53
+ return {
54
+ ...raw,
55
+ reportSchemaVersion: schemaVersion,
56
+ results: raw.results.map(normalizeCaseResult),
57
+ };
58
+ }
59
+ export function compareBaselineWarnings(baseline, current) {
60
+ const warnings = [];
61
+ if (baseline.reportSchemaVersion !== current.reportSchemaVersion) {
62
+ warnings.push(`baseline schema v${baseline.reportSchemaVersion} differs from current v${current.reportSchemaVersion}`);
63
+ }
64
+ const baselineContexts = new Set(baseline.contexts.map((context) => context.id));
65
+ const currentContexts = new Set(current.contexts.map((context) => context.id));
66
+ const onlyBaseline = [...baselineContexts].filter((context) => !currentContexts.has(context));
67
+ const onlyCurrent = [...currentContexts].filter((context) => !baselineContexts.has(context));
68
+ if (onlyBaseline.length > 0 || onlyCurrent.length > 0) {
69
+ warnings.push(`context set differs (baseline-only: ${onlyBaseline.join(', ') || '-'}; current-only: ${onlyCurrent.join(', ') || '-'})`);
70
+ }
71
+ return warnings;
72
+ }
73
+ function indexResults(report) {
74
+ const map = new Map();
75
+ for (const result of report.results) {
76
+ map.set(resultKey(result.caseId, result.context), result);
77
+ }
78
+ return map;
79
+ }
80
+ function classifierOutputChanged(before, after) {
81
+ return (before.actual.verdict !== after.actual.verdict ||
82
+ before.actual.reason !== after.actual.reason ||
83
+ before.actual.fingerprint !== after.actual.fingerprint);
84
+ }
85
+ export function compareCoverageReports(baseline, current) {
86
+ const beforeMatrixHash = baseline.matrixHash;
87
+ const afterMatrixHash = current.matrixHash;
88
+ const matrixHashChanged = beforeMatrixHash !== afterMatrixHash;
89
+ const beforeContextHashes = new Map(baseline.contexts.map((context) => [context.id, context.resolvedConfigHash]));
90
+ const afterContextHashes = new Map(current.contexts.map((context) => [context.id, context.resolvedConfigHash]));
91
+ const configDrift = [];
92
+ const sharedContexts = new Set([...beforeContextHashes.keys(), ...afterContextHashes.keys()]);
93
+ for (const context of sharedContexts) {
94
+ const beforeHash = beforeContextHashes.get(context);
95
+ const afterHash = afterContextHashes.get(context);
96
+ if (beforeHash !== undefined && afterHash !== undefined && beforeHash !== afterHash) {
97
+ configDrift.push({ context, beforeHash, afterHash });
98
+ }
99
+ }
100
+ const beforeResults = indexResults(baseline);
101
+ const afterResults = indexResults(current);
102
+ const keys = new Set([...beforeResults.keys(), ...afterResults.keys()]);
103
+ const entries = [];
104
+ for (const key of [...keys].sort()) {
105
+ const before = beforeResults.get(key);
106
+ const after = afterResults.get(key);
107
+ if (!before && after) {
108
+ entries.push({
109
+ caseId: after.caseId,
110
+ context: after.context,
111
+ kind: 'added',
112
+ afterVerdict: after.actual.verdict,
113
+ afterReason: after.actual.reason,
114
+ afterCommandHash: after.commandHash,
115
+ afterExpectationHash: after.expectationHash ?? null,
116
+ });
117
+ continue;
118
+ }
119
+ if (before && !after) {
120
+ entries.push({
121
+ caseId: before.caseId,
122
+ context: before.context,
123
+ kind: 'removed',
124
+ beforeVerdict: before.actual.verdict,
125
+ beforeReason: before.actual.reason,
126
+ beforeCommandHash: before.commandHash,
127
+ beforeExpectationHash: before.expectationHash ?? null,
128
+ });
129
+ continue;
130
+ }
131
+ if (!before || !after) {
132
+ continue;
133
+ }
134
+ const fixtureChanged = before.commandHash !== after.commandHash ||
135
+ (before.expectationHash ?? null) !== (after.expectationHash ?? null);
136
+ const classifierChanged = classifierOutputChanged(before, after);
137
+ if (fixtureChanged) {
138
+ entries.push({
139
+ caseId: after.caseId,
140
+ context: after.context,
141
+ kind: 'fixture_change',
142
+ beforeVerdict: before.actual.verdict,
143
+ afterVerdict: after.actual.verdict,
144
+ beforeReason: before.actual.reason,
145
+ afterReason: after.actual.reason,
146
+ beforeCommandHash: before.commandHash,
147
+ afterCommandHash: after.commandHash,
148
+ beforeExpectationHash: before.expectationHash ?? null,
149
+ afterExpectationHash: after.expectationHash ?? null,
150
+ });
151
+ continue;
152
+ }
153
+ if (classifierChanged) {
154
+ entries.push({
155
+ caseId: after.caseId,
156
+ context: after.context,
157
+ kind: 'classifier_drift',
158
+ beforeVerdict: before.actual.verdict,
159
+ afterVerdict: after.actual.verdict,
160
+ beforeReason: before.actual.reason,
161
+ afterReason: after.actual.reason,
162
+ beforeCommandHash: before.commandHash,
163
+ afterCommandHash: after.commandHash,
164
+ beforeExpectationHash: before.expectationHash ?? null,
165
+ afterExpectationHash: after.expectationHash ?? null,
166
+ });
167
+ }
168
+ }
169
+ return {
170
+ schemaVersion: 1,
171
+ matrixHashChanged,
172
+ beforeMatrixHash,
173
+ afterMatrixHash,
174
+ configDrift,
175
+ entries,
176
+ };
177
+ }
178
+ export function formatCoverageCompareReport(report) {
179
+ const lines = [];
180
+ lines.push('Coverage probe compare');
181
+ lines.push(`matrix hash changed: ${report.matrixHashChanged ? 'yes' : 'no'}`);
182
+ if (report.configDrift.length > 0) {
183
+ lines.push('config drift (warning only):');
184
+ for (const drift of report.configDrift) {
185
+ lines.push(` - ${drift.context}: ${drift.beforeHash.slice(0, 8)} -> ${drift.afterHash.slice(0, 8)}`);
186
+ }
187
+ }
188
+ if (report.entries.length === 0) {
189
+ lines.push('no fixture or classifier differences');
190
+ return lines.join('\n');
191
+ }
192
+ lines.push(`differences: ${report.entries.length}`);
193
+ for (const entry of report.entries) {
194
+ lines.push(` - [${entry.kind}] ${entry.caseId} (${entry.context}) ${entry.beforeVerdict ?? '-'} -> ${entry.afterVerdict ?? '-'}`);
195
+ }
196
+ return lines.join('\n');
197
+ }
@@ -0,0 +1,17 @@
1
+ import { type BelayConfigV4 } from '../core/config.js';
2
+ import type { ConfigProvenanceEntry } from '../core/config-layers.js';
3
+ import type { ClassifierOptions } from '../core/types.js';
4
+ import type { CoverageContextId } from './coverage-matrix.js';
5
+ export interface CoverageEvalContext {
6
+ id: CoverageContextId;
7
+ config: BelayConfigV4;
8
+ cwd: string;
9
+ repoRoot: string;
10
+ options: ClassifierOptions;
11
+ configProvenance?: ConfigProvenanceEntry[];
12
+ }
13
+ export declare function structuralCoverageConfig(): BelayConfigV4;
14
+ export declare function buildCoverageEvalContexts(contextIds: CoverageContextId[], repoRoot: string): Promise<CoverageEvalContext[]>;
15
+ export declare function stableJsonStringify(value: unknown): string;
16
+ export declare function hashStableJson(value: unknown): string;
17
+ export declare function resolvedConfigHash(context: CoverageEvalContext): string;
@@ -0,0 +1,88 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { loadLayeredConfig } from '../config-io.js';
4
+ import { DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, mergeConfig, } from '../core/config.js';
5
+ import { DEFAULT_CORPUS_REPO_ROOT } from './runtime-match.js';
6
+ import { structuralFixtureRoot } from './structural-fixture-root.js';
7
+ export function structuralCoverageConfig() {
8
+ return mergeConfig({
9
+ mode: 'enforce',
10
+ policy: {
11
+ unknownLocalEffect: 'deny',
12
+ unparseableShell: 'deny',
13
+ },
14
+ }, DEFAULT_CONFIG_V4);
15
+ }
16
+ export async function buildCoverageEvalContexts(contextIds, repoRoot) {
17
+ const resolvedRepoRoot = path.resolve(repoRoot);
18
+ const fixtureRoot = structuralFixtureRoot(resolvedRepoRoot);
19
+ const contexts = [];
20
+ for (const contextId of contextIds) {
21
+ if (contextId === 'default') {
22
+ contexts.push({
23
+ id: 'default',
24
+ config: DEFAULT_CONFIG_V3,
25
+ cwd: `${DEFAULT_CORPUS_REPO_ROOT}/src`,
26
+ repoRoot: DEFAULT_CORPUS_REPO_ROOT,
27
+ options: {},
28
+ });
29
+ continue;
30
+ }
31
+ if (contextId === 'structural') {
32
+ contexts.push({
33
+ id: 'structural',
34
+ config: structuralCoverageConfig(),
35
+ cwd: fixtureRoot,
36
+ repoRoot: fixtureRoot,
37
+ options: {
38
+ unknownLocalEffect: 'deny',
39
+ unparseableShell: 'deny',
40
+ trustedCwd: true,
41
+ },
42
+ });
43
+ continue;
44
+ }
45
+ if (contextId === 'audit') {
46
+ const layered = await loadLayeredConfig(resolvedRepoRoot);
47
+ contexts.push({
48
+ id: 'audit',
49
+ config: layered.config,
50
+ cwd: resolvedRepoRoot,
51
+ repoRoot: resolvedRepoRoot,
52
+ options: {},
53
+ configProvenance: layered.provenance,
54
+ });
55
+ }
56
+ }
57
+ return contexts;
58
+ }
59
+ export function stableJsonStringify(value) {
60
+ if (value === null || typeof value !== 'object') {
61
+ return JSON.stringify(value);
62
+ }
63
+ if (Array.isArray(value)) {
64
+ return `[${value.map((entry) => stableJsonStringify(entry)).join(',')}]`;
65
+ }
66
+ const record = value;
67
+ const keys = Object.keys(record).sort();
68
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJsonStringify(record[key])}`).join(',')}}`;
69
+ }
70
+ export function hashStableJson(value) {
71
+ return createHash('sha256').update(stableJsonStringify(value)).digest('hex');
72
+ }
73
+ export function resolvedConfigHash(context) {
74
+ return hashStableJson({
75
+ mode: context.config.mode,
76
+ policy: context.config.policy,
77
+ gates: context.config.gates,
78
+ classifier: context.config.classifier,
79
+ options: {
80
+ unknownLocalEffect: context.options.unknownLocalEffect,
81
+ unparseableShell: context.options.unparseableShell,
82
+ trustedCwd: context.options.trustedCwd,
83
+ },
84
+ cwd: context.cwd,
85
+ repoRoot: context.repoRoot,
86
+ configProvenance: context.configProvenance,
87
+ });
88
+ }
@@ -0,0 +1,37 @@
1
+ import type { HookVerdict } from '../core/types.js';
2
+ export declare const COVERAGE_MATRIX_VERSION: 1;
3
+ export declare const COVERAGE_CONTEXT_IDS: readonly ["default", "structural", "audit"];
4
+ export type CoverageContextId = (typeof COVERAGE_CONTEXT_IDS)[number];
5
+ /** Default probe run excludes opt-in `audit`. */
6
+ export declare const DEFAULT_PROBE_CONTEXT_IDS: readonly ["default", "structural"];
7
+ export interface CoverageExpectation {
8
+ verdict: HookVerdict;
9
+ reason?: string;
10
+ }
11
+ export interface CoverageMatrixCase {
12
+ id: string;
13
+ command: string;
14
+ tags: string[];
15
+ expectations?: Partial<Record<CoverageContextId, CoverageExpectation>>;
16
+ notes?: string;
17
+ }
18
+ export interface CoverageMatrixGroup {
19
+ id: string;
20
+ label: string;
21
+ cases: CoverageMatrixCase[];
22
+ }
23
+ export interface CoverageMatrix {
24
+ version: typeof COVERAGE_MATRIX_VERSION;
25
+ groups: CoverageMatrixGroup[];
26
+ }
27
+ export declare class CoverageMatrixSchemaError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ export declare function parseCoverageMatrix(raw: unknown): CoverageMatrix;
31
+ export declare function loadCoverageMatrix(matrixPath: string): Promise<CoverageMatrix>;
32
+ export declare function flattenCoverageCases(matrix: CoverageMatrix): Array<CoverageMatrixCase & {
33
+ groupId: string;
34
+ groupLabel: string;
35
+ }>;
36
+ export declare function defaultCoverageMatrixPath(repoRoot: string): string;
37
+ export declare function assertKnownContextIds(contextIds: string[]): CoverageContextId[];
@@ -0,0 +1,159 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export const COVERAGE_MATRIX_VERSION = 1;
4
+ export const COVERAGE_CONTEXT_IDS = ['default', 'structural', 'audit'];
5
+ /** Default probe run excludes opt-in `audit`. */
6
+ export const DEFAULT_PROBE_CONTEXT_IDS = ['default', 'structural'];
7
+ export class CoverageMatrixSchemaError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'CoverageMatrixSchemaError';
11
+ }
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ }
16
+ function parseVerdict(value, pathLabel) {
17
+ if (value !== 'allow' && value !== 'allow_flagged' && value !== 'deny_pending_approval') {
18
+ throw new CoverageMatrixSchemaError(`${pathLabel}.verdict is invalid: ${JSON.stringify(value)}`);
19
+ }
20
+ return value;
21
+ }
22
+ function parseExpectation(value, pathLabel) {
23
+ if (!isRecord(value)) {
24
+ throw new CoverageMatrixSchemaError(`${pathLabel} must be an object`);
25
+ }
26
+ const expectation = {
27
+ verdict: parseVerdict(value.verdict, pathLabel),
28
+ };
29
+ if (value.reason !== undefined) {
30
+ if (typeof value.reason !== 'string' || value.reason.trim() === '') {
31
+ throw new CoverageMatrixSchemaError(`${pathLabel}.reason must be a non-empty string`);
32
+ }
33
+ expectation.reason = value.reason;
34
+ }
35
+ return expectation;
36
+ }
37
+ function parseCase(raw, groupId, index) {
38
+ const pathLabel = `group[${groupId}].cases[${index}]`;
39
+ if (!isRecord(raw)) {
40
+ throw new CoverageMatrixSchemaError(`${pathLabel} must be an object`);
41
+ }
42
+ if (typeof raw.id !== 'string' || raw.id.trim() === '') {
43
+ throw new CoverageMatrixSchemaError(`${pathLabel}.id must be a non-empty string`);
44
+ }
45
+ if (typeof raw.command !== 'string' || raw.command.trim() === '') {
46
+ throw new CoverageMatrixSchemaError(`${pathLabel}.command must be a non-empty string`);
47
+ }
48
+ if (!Array.isArray(raw.tags) || raw.tags.length === 0) {
49
+ throw new CoverageMatrixSchemaError(`${pathLabel}.tags must be a non-empty array`);
50
+ }
51
+ for (const tag of raw.tags) {
52
+ if (typeof tag !== 'string' || tag.trim() === '') {
53
+ throw new CoverageMatrixSchemaError(`${pathLabel}.tags entries must be non-empty strings`);
54
+ }
55
+ }
56
+ let expectations;
57
+ if (raw.expectations !== undefined) {
58
+ if (!isRecord(raw.expectations)) {
59
+ throw new CoverageMatrixSchemaError(`${pathLabel}.expectations must be an object`);
60
+ }
61
+ expectations = {};
62
+ for (const [contextId, expectationRaw] of Object.entries(raw.expectations)) {
63
+ if (!COVERAGE_CONTEXT_IDS.includes(contextId)) {
64
+ throw new CoverageMatrixSchemaError(`${pathLabel}.expectations.${contextId} is unknown (allowed: ${COVERAGE_CONTEXT_IDS.join(', ')})`);
65
+ }
66
+ expectations[contextId] = parseExpectation(expectationRaw, `${pathLabel}.expectations.${contextId}`);
67
+ }
68
+ }
69
+ if (raw.notes !== undefined && typeof raw.notes !== 'string') {
70
+ throw new CoverageMatrixSchemaError(`${pathLabel}.notes must be a string`);
71
+ }
72
+ return {
73
+ id: raw.id,
74
+ command: raw.command,
75
+ tags: raw.tags,
76
+ expectations,
77
+ notes: raw.notes,
78
+ };
79
+ }
80
+ export function parseCoverageMatrix(raw) {
81
+ if (!isRecord(raw)) {
82
+ throw new CoverageMatrixSchemaError('coverage matrix root must be an object');
83
+ }
84
+ if (raw.version !== COVERAGE_MATRIX_VERSION) {
85
+ throw new CoverageMatrixSchemaError(`coverage matrix version must be ${COVERAGE_MATRIX_VERSION} (got ${JSON.stringify(raw.version)})`);
86
+ }
87
+ if (!Array.isArray(raw.groups) || raw.groups.length === 0) {
88
+ throw new CoverageMatrixSchemaError('coverage matrix groups must be a non-empty array');
89
+ }
90
+ const groupIds = new Set();
91
+ const caseIds = new Set();
92
+ const groups = [];
93
+ for (let groupIndex = 0; groupIndex < raw.groups.length; groupIndex += 1) {
94
+ const groupRaw = raw.groups[groupIndex];
95
+ if (!isRecord(groupRaw)) {
96
+ throw new CoverageMatrixSchemaError(`groups[${groupIndex}] must be an object`);
97
+ }
98
+ if (typeof groupRaw.id !== 'string' || groupRaw.id.trim() === '') {
99
+ throw new CoverageMatrixSchemaError(`groups[${groupIndex}].id must be a non-empty string`);
100
+ }
101
+ if (groupIds.has(groupRaw.id)) {
102
+ throw new CoverageMatrixSchemaError(`duplicate group id: ${groupRaw.id}`);
103
+ }
104
+ groupIds.add(groupRaw.id);
105
+ if (typeof groupRaw.label !== 'string' || groupRaw.label.trim() === '') {
106
+ throw new CoverageMatrixSchemaError(`groups[${groupIndex}].label must be a non-empty string`);
107
+ }
108
+ if (!Array.isArray(groupRaw.cases) || groupRaw.cases.length === 0) {
109
+ throw new CoverageMatrixSchemaError(`group[${groupRaw.id}].cases must be a non-empty array`);
110
+ }
111
+ const cases = [];
112
+ for (let caseIndex = 0; caseIndex < groupRaw.cases.length; caseIndex += 1) {
113
+ const testCase = parseCase(groupRaw.cases[caseIndex], groupRaw.id, caseIndex);
114
+ if (caseIds.has(testCase.id)) {
115
+ throw new CoverageMatrixSchemaError(`duplicate case id: ${testCase.id}`);
116
+ }
117
+ caseIds.add(testCase.id);
118
+ cases.push(testCase);
119
+ }
120
+ groups.push({
121
+ id: groupRaw.id,
122
+ label: groupRaw.label,
123
+ cases,
124
+ });
125
+ }
126
+ return {
127
+ version: COVERAGE_MATRIX_VERSION,
128
+ groups,
129
+ };
130
+ }
131
+ export async function loadCoverageMatrix(matrixPath) {
132
+ const raw = JSON.parse(await readFile(matrixPath, 'utf8'));
133
+ return parseCoverageMatrix(raw);
134
+ }
135
+ export function flattenCoverageCases(matrix) {
136
+ const flat = [];
137
+ for (const group of matrix.groups) {
138
+ for (const testCase of group.cases) {
139
+ flat.push({ ...testCase, groupId: group.id, groupLabel: group.label });
140
+ }
141
+ }
142
+ return flat;
143
+ }
144
+ export function defaultCoverageMatrixPath(repoRoot) {
145
+ return path.join(repoRoot, 'corpus', 'coverage-matrix.json');
146
+ }
147
+ export function assertKnownContextIds(contextIds) {
148
+ if (contextIds.length === 0) {
149
+ throw new CoverageMatrixSchemaError('at least one context is required');
150
+ }
151
+ const parsed = [];
152
+ for (const contextId of contextIds) {
153
+ if (!COVERAGE_CONTEXT_IDS.includes(contextId)) {
154
+ throw new CoverageMatrixSchemaError(`unknown context ${JSON.stringify(contextId)} (allowed: ${COVERAGE_CONTEXT_IDS.join(', ')})`);
155
+ }
156
+ parsed.push(contextId);
157
+ }
158
+ return parsed;
159
+ }
@@ -0,0 +1,91 @@
1
+ import type { ConfigProvenanceEntry } from '../core/config-layers.js';
2
+ import type { ClassifyResult, HookVerdict } from '../core/types.js';
3
+ import { type CoverageEvalContext } from './coverage-contexts.js';
4
+ import { type CoverageContextId, type CoverageExpectation, type CoverageMatrix } from './coverage-matrix.js';
5
+ export declare class CoverageProbeCliError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ export declare const COVERAGE_PROBE_REPORT_SCHEMA_VERSION: 2;
9
+ export type ClassifyFn = (command: string, evalContext: CoverageEvalContext) => Promise<ClassifyResult>;
10
+ export interface CoverageProbeOptions {
11
+ repoRoot?: string;
12
+ matrixPath?: string;
13
+ contextIds?: CoverageContextId[];
14
+ filters?: string[];
15
+ repeat?: number;
16
+ outputDir?: string;
17
+ json?: boolean;
18
+ classifyFn?: ClassifyFn;
19
+ evalContexts?: CoverageEvalContext[];
20
+ }
21
+ export interface CoverageCaseResult {
22
+ caseId: string;
23
+ groupId: string;
24
+ command: string;
25
+ commandHash: string;
26
+ context: CoverageContextId;
27
+ tags: string[];
28
+ observeOnly: boolean;
29
+ expectation?: CoverageExpectation;
30
+ expectationHash: string | null;
31
+ actual: {
32
+ verdict: HookVerdict;
33
+ reason: string;
34
+ fingerprint: string;
35
+ };
36
+ match: boolean | null;
37
+ }
38
+ export interface CoverageProbeReport {
39
+ reportSchemaVersion: typeof COVERAGE_PROBE_REPORT_SCHEMA_VERSION;
40
+ generatedAt: string;
41
+ packageVersion: string | null;
42
+ gitSha: string | null;
43
+ matrixPath: string;
44
+ matrixHash: string;
45
+ repeat: number;
46
+ driftRuns: number;
47
+ contexts: Array<{
48
+ id: CoverageContextId;
49
+ cwd: string;
50
+ repoRoot: string;
51
+ resolvedConfigHash: string;
52
+ configProvenance?: ConfigProvenanceEntry[];
53
+ }>;
54
+ summary: {
55
+ total: number;
56
+ observeOnly: number;
57
+ matched: number;
58
+ mismatched: number;
59
+ filteredEmpty: boolean;
60
+ byGroup: Record<string, {
61
+ total: number;
62
+ matched: number;
63
+ mismatched: number;
64
+ observeOnly: number;
65
+ }>;
66
+ byTag: Record<string, {
67
+ total: number;
68
+ matched: number;
69
+ mismatched: number;
70
+ observeOnly: number;
71
+ }>;
72
+ };
73
+ results: CoverageCaseResult[];
74
+ mismatches: CoverageCaseResult[];
75
+ }
76
+ export declare const defaultClassifyFn: ClassifyFn;
77
+ export declare function evaluateCoverageMatrix(matrix: CoverageMatrix, options?: CoverageProbeOptions): Promise<CoverageCaseResult[]>;
78
+ export declare function runCoverageProbe(options?: CoverageProbeOptions): Promise<CoverageProbeReport>;
79
+ export declare function formatCoverageProbeSummary(report: CoverageProbeReport): string;
80
+ export declare function parseCoverageProbeCliArgs(argv: string[]): {
81
+ repoRoot?: string;
82
+ matrixPath?: string;
83
+ contextIds?: CoverageContextId[];
84
+ filters: string[];
85
+ repeat: number;
86
+ strict: boolean;
87
+ outputDir?: string;
88
+ comparePath?: string;
89
+ json: boolean;
90
+ };
91
+ export declare function main(argv?: string[]): Promise<number>;