@codapult/guard 0.9.0 → 0.10.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.
- package/README.md +14 -10
- package/dist/core/history/runs.d.ts +43 -1
- package/dist/core/history/runs.js +135 -18
- package/dist/core/verification/verify.js +125 -18
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
## Architecture guardrails for AI coding agents
|
|
11
11
|
|
|
12
|
-
`@codapult/guard` is
|
|
13
|
-
|
|
12
|
+
`@codapult/guard` is local-first, model-agnostic **architecture guardrail software for AI coding
|
|
13
|
+
agents**. Use it to detect architectural drift, enforce approved software architecture rules, and
|
|
14
|
+
check code changes in JavaScript and TypeScript projects.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
changes
|
|
16
|
+
Guard learns how your codebase is structured, records project architecture as reviewable memory, and
|
|
17
|
+
checks future changes against the rules your team approves. It connects AI coding agents to those
|
|
18
|
+
rules and returns deterministic evidence when a change introduces a covered architecture
|
|
19
|
+
regression. Run the same project-specific checks locally and in CI.
|
|
17
20
|
|
|
18
21
|
### See the value in one change
|
|
19
22
|
|
|
@@ -455,6 +458,13 @@ audits this metadata and reports missing or overlapping declared actors. It cont
|
|
|
455
458
|
or external telemetry. These manifests make a failed run explainable without turning Guard into a
|
|
456
459
|
production tracing system or an identity/signature ledger.
|
|
457
460
|
|
|
461
|
+
The manifest also contains a `verification-receipt` object. It is a compact, machine-readable
|
|
462
|
+
record of the commit, policy base and fingerprint, decision IDs, check statuses, finding counts,
|
|
463
|
+
contract issues, waiver state, and Guard version. It intentionally excludes command output and
|
|
464
|
+
source code. The receipt describes what Guard decided; it is not a cryptographic signature or proof
|
|
465
|
+
of CI identity. When a policy base is supplied, it separates the base diff from dirty working-tree
|
|
466
|
+
files so local verification cannot silently present an incomplete change set.
|
|
467
|
+
|
|
458
468
|
## Security and data handling
|
|
459
469
|
|
|
460
470
|
- Deterministic discovery and checks run locally.
|
|
@@ -499,12 +509,6 @@ Starting a SaaS from scratch? [Codapult](https://codapult.dev) is a production-r
|
|
|
499
509
|
foundation built for AI-assisted development — with MCP context, built-in AI, modular
|
|
500
510
|
architecture, and the source code under your control.
|
|
501
511
|
|
|
502
|
-
## Project status
|
|
503
|
-
|
|
504
|
-
`@codapult/guard` starts at `0.1.0` as a public alpha. The deterministic core is usable, tested,
|
|
505
|
-
and intended for real projects, while policy schema and integration surfaces may still evolve
|
|
506
|
-
before `1.0.0`.
|
|
507
|
-
|
|
508
512
|
## License
|
|
509
513
|
|
|
510
514
|
MIT. See [LICENSE](LICENSE).
|
|
@@ -13,6 +13,47 @@ export interface GuardRunPolicy {
|
|
|
13
13
|
changedFromBase?: boolean | undefined;
|
|
14
14
|
decisionIds?: string[] | undefined;
|
|
15
15
|
}
|
|
16
|
+
export interface GuardVerificationReceiptCheck {
|
|
17
|
+
status: 'passed' | 'failed' | 'not-configured';
|
|
18
|
+
exitCode: number;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface GuardVerificationReceipt {
|
|
22
|
+
version: 1;
|
|
23
|
+
kind: 'verification-receipt';
|
|
24
|
+
runId: string;
|
|
25
|
+
generatedAt: string;
|
|
26
|
+
outcome: GuardRunManifest['outcome'];
|
|
27
|
+
gate: string;
|
|
28
|
+
subject: {
|
|
29
|
+
commit?: string | undefined;
|
|
30
|
+
baseRef?: string | undefined;
|
|
31
|
+
baseCommit?: string | undefined;
|
|
32
|
+
changedFiles: string[];
|
|
33
|
+
baseChangedFiles?: string[] | undefined;
|
|
34
|
+
workingTreeFiles?: string[] | undefined;
|
|
35
|
+
workingTreeDirty: boolean;
|
|
36
|
+
};
|
|
37
|
+
policy?: GuardRunPolicy | undefined;
|
|
38
|
+
stages: Record<string, 'ok' | 'fail' | 'skipped'>;
|
|
39
|
+
checks: Record<string, GuardVerificationReceiptCheck>;
|
|
40
|
+
findings: {
|
|
41
|
+
total: number;
|
|
42
|
+
errors: number;
|
|
43
|
+
warnings: number;
|
|
44
|
+
waived: number;
|
|
45
|
+
waiverWarnings: number;
|
|
46
|
+
expiredWaivers: number;
|
|
47
|
+
fingerprints: string[];
|
|
48
|
+
ruleIds: string[];
|
|
49
|
+
};
|
|
50
|
+
contractIssues: string[];
|
|
51
|
+
decisionIds: string[];
|
|
52
|
+
tool: {
|
|
53
|
+
name: string;
|
|
54
|
+
version: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
16
57
|
export interface GuardRunManifest {
|
|
17
58
|
version: 1;
|
|
18
59
|
runId: string;
|
|
@@ -25,6 +66,7 @@ export interface GuardRunManifest {
|
|
|
25
66
|
stages: Record<string, GuardRunStage>;
|
|
26
67
|
commit?: string | undefined;
|
|
27
68
|
policy?: GuardRunPolicy | undefined;
|
|
69
|
+
receipt?: GuardVerificationReceipt | undefined;
|
|
28
70
|
}
|
|
29
71
|
export interface GuardRunContext {
|
|
30
72
|
runId: string;
|
|
@@ -49,4 +91,4 @@ export declare function writeGuardRun(root: string, manifest: GuardRunManifest):
|
|
|
49
91
|
/** Reads locally persisted run manifests without reaching a remote service. */
|
|
50
92
|
export declare function listGuardRuns(root: string, limit?: number): GuardRunManifest[];
|
|
51
93
|
export declare function summarizeGuardRuns(root: string, limit?: number): GuardRunSummary;
|
|
52
|
-
export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string, policy?: GuardRunPolicy): GuardRunManifest;
|
|
94
|
+
export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string, policy?: GuardRunPolicy, receipt?: Omit<GuardVerificationReceipt, 'runId' | 'generatedAt' | 'outcome' | 'gate' | 'subject'> & Pick<GuardVerificationReceipt, 'subject'>): GuardRunManifest;
|
|
@@ -12,11 +12,109 @@ export class GuardRunManifestError extends Error {
|
|
|
12
12
|
this.name = 'GuardRunManifestError';
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
function isGuardRunPolicy(value) {
|
|
16
|
+
if (value === null || typeof value !== 'object')
|
|
17
|
+
return false;
|
|
18
|
+
const policy = value;
|
|
19
|
+
return (typeof policy.fingerprint === 'string' &&
|
|
20
|
+
policy.fingerprint.length > 0 &&
|
|
21
|
+
(policy.source === 'working-tree' || policy.source === 'git-ref') &&
|
|
22
|
+
(policy.revision === undefined ||
|
|
23
|
+
(Number.isInteger(policy.revision) && policy.revision >= 0)) &&
|
|
24
|
+
(policy.ref === undefined || typeof policy.ref === 'string') &&
|
|
25
|
+
(policy.changedFromBase === undefined || typeof policy.changedFromBase === 'boolean') &&
|
|
26
|
+
(policy.decisionIds === undefined ||
|
|
27
|
+
(Array.isArray(policy.decisionIds) &&
|
|
28
|
+
new Set(policy.decisionIds).size === policy.decisionIds.length &&
|
|
29
|
+
policy.decisionIds.every((decisionId) => typeof decisionId === 'string' && decisionId.trim().length > 0) &&
|
|
30
|
+
(policy.source === 'working-tree' || policy.decisionIds.length === 0))));
|
|
31
|
+
}
|
|
15
32
|
function isGuardRunManifest(value) {
|
|
16
33
|
if (value === null || typeof value !== 'object')
|
|
17
34
|
return false;
|
|
18
35
|
const manifest = value;
|
|
19
36
|
const stages = manifest.stages;
|
|
37
|
+
const receipt = manifest.receipt;
|
|
38
|
+
const validReceipt = receipt === undefined ||
|
|
39
|
+
(receipt !== null &&
|
|
40
|
+
typeof receipt === 'object' &&
|
|
41
|
+
receipt.version === 1 &&
|
|
42
|
+
receipt.kind === 'verification-receipt' &&
|
|
43
|
+
typeof receipt.runId === 'string' &&
|
|
44
|
+
receipt.runId === manifest.runId &&
|
|
45
|
+
typeof receipt.generatedAt === 'string' &&
|
|
46
|
+
Number.isFinite(Date.parse(receipt.generatedAt)) &&
|
|
47
|
+
(receipt.outcome === 'pass' ||
|
|
48
|
+
receipt.outcome === 'fail' ||
|
|
49
|
+
receipt.outcome === 'warning' ||
|
|
50
|
+
receipt.outcome === 'needs-review' ||
|
|
51
|
+
receipt.outcome === 'not-configured') &&
|
|
52
|
+
typeof receipt.gate === 'string' &&
|
|
53
|
+
receipt.subject !== null &&
|
|
54
|
+
typeof receipt.subject === 'object' &&
|
|
55
|
+
Array.isArray(receipt.subject.changedFiles) &&
|
|
56
|
+
receipt.subject.changedFiles.every((file) => typeof file === 'string') &&
|
|
57
|
+
(receipt.subject.baseChangedFiles === undefined ||
|
|
58
|
+
(Array.isArray(receipt.subject.baseChangedFiles) &&
|
|
59
|
+
receipt.subject.baseChangedFiles.every((file) => typeof file === 'string'))) &&
|
|
60
|
+
(receipt.subject.workingTreeFiles === undefined ||
|
|
61
|
+
(Array.isArray(receipt.subject.workingTreeFiles) &&
|
|
62
|
+
receipt.subject.workingTreeFiles.every((file) => typeof file === 'string'))) &&
|
|
63
|
+
typeof receipt.subject.workingTreeDirty === 'boolean' &&
|
|
64
|
+
(receipt.subject.commit === undefined || typeof receipt.subject.commit === 'string') &&
|
|
65
|
+
(receipt.subject.baseRef === undefined || typeof receipt.subject.baseRef === 'string') &&
|
|
66
|
+
(receipt.subject.baseCommit === undefined ||
|
|
67
|
+
typeof receipt.subject.baseCommit === 'string') &&
|
|
68
|
+
(receipt.policy === undefined || isGuardRunPolicy(receipt.policy)) &&
|
|
69
|
+
receipt.stages !== null &&
|
|
70
|
+
typeof receipt.stages === 'object' &&
|
|
71
|
+
!Array.isArray(receipt.stages) &&
|
|
72
|
+
Object.values(receipt.stages).every((status) => status === 'ok' || status === 'fail' || status === 'skipped') &&
|
|
73
|
+
receipt.checks !== null &&
|
|
74
|
+
typeof receipt.checks === 'object' &&
|
|
75
|
+
!Array.isArray(receipt.checks) &&
|
|
76
|
+
Object.values(receipt.checks).every((check) => {
|
|
77
|
+
if (check === null || typeof check !== 'object')
|
|
78
|
+
return false;
|
|
79
|
+
const candidate = check;
|
|
80
|
+
return ((candidate.status === 'passed' ||
|
|
81
|
+
candidate.status === 'failed' ||
|
|
82
|
+
candidate.status === 'not-configured') &&
|
|
83
|
+
Number.isInteger(candidate.exitCode) &&
|
|
84
|
+
typeof candidate.durationMs === 'number' &&
|
|
85
|
+
Number.isFinite(candidate.durationMs) &&
|
|
86
|
+
candidate.durationMs >= 0);
|
|
87
|
+
}) &&
|
|
88
|
+
receipt.findings !== null &&
|
|
89
|
+
typeof receipt.findings === 'object' &&
|
|
90
|
+
['total', 'errors', 'warnings', 'waived', 'waiverWarnings', 'expiredWaivers'].every((key) => {
|
|
91
|
+
const countValue = receipt.findings[key];
|
|
92
|
+
return typeof countValue === 'number' && Number.isInteger(countValue) && countValue >= 0;
|
|
93
|
+
}) &&
|
|
94
|
+
Array.isArray(receipt.findings.fingerprints) &&
|
|
95
|
+
receipt.findings.fingerprints.every((fingerprint) => typeof fingerprint === 'string') &&
|
|
96
|
+
Array.isArray(receipt.findings.ruleIds) &&
|
|
97
|
+
receipt.findings.ruleIds.every((ruleId) => typeof ruleId === 'string') &&
|
|
98
|
+
Array.isArray(receipt.contractIssues) &&
|
|
99
|
+
receipt.contractIssues.every((issue) => typeof issue === 'string') &&
|
|
100
|
+
Array.isArray(receipt.decisionIds) &&
|
|
101
|
+
new Set(receipt.decisionIds).size === receipt.decisionIds.length &&
|
|
102
|
+
receipt.decisionIds.every((decisionId) => typeof decisionId === 'string') &&
|
|
103
|
+
receipt.tool !== null &&
|
|
104
|
+
typeof receipt.tool === 'object' &&
|
|
105
|
+
typeof receipt.tool.name === 'string' &&
|
|
106
|
+
typeof receipt.tool.version === 'string');
|
|
107
|
+
const receiptMatchesManifest = receipt === undefined ||
|
|
108
|
+
(receipt.outcome === manifest.outcome &&
|
|
109
|
+
receipt.gate === manifest.gate &&
|
|
110
|
+
(manifest.commit === undefined
|
|
111
|
+
? receipt.subject.commit === undefined
|
|
112
|
+
: receipt.subject.commit === manifest.commit) &&
|
|
113
|
+
(manifest.policy === undefined
|
|
114
|
+
? receipt.policy === undefined
|
|
115
|
+
: receipt.policy?.fingerprint === manifest.policy.fingerprint &&
|
|
116
|
+
receipt.policy.source === manifest.policy.source &&
|
|
117
|
+
receipt.policy.revision === manifest.policy.revision));
|
|
20
118
|
const validStages = stages !== null &&
|
|
21
119
|
typeof stages === 'object' &&
|
|
22
120
|
!Array.isArray(stages) &&
|
|
@@ -51,22 +149,9 @@ function isGuardRunManifest(value) {
|
|
|
51
149
|
manifest.outcome === 'not-configured') &&
|
|
52
150
|
typeof manifest.gate === 'string' &&
|
|
53
151
|
validStages &&
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
typeof manifest.policy.fingerprint === 'string' &&
|
|
58
|
-
manifest.policy.fingerprint.length > 0 &&
|
|
59
|
-
(manifest.policy.source === 'working-tree' || manifest.policy.source === 'git-ref') &&
|
|
60
|
-
(manifest.policy.revision === undefined ||
|
|
61
|
-
(Number.isInteger(manifest.policy.revision) && manifest.policy.revision >= 0)) &&
|
|
62
|
-
(manifest.policy.ref === undefined || typeof manifest.policy.ref === 'string') &&
|
|
63
|
-
(manifest.policy.changedFromBase === undefined ||
|
|
64
|
-
typeof manifest.policy.changedFromBase === 'boolean') &&
|
|
65
|
-
(manifest.policy.decisionIds === undefined ||
|
|
66
|
-
(manifest.policy.source === 'working-tree' &&
|
|
67
|
-
Array.isArray(manifest.policy.decisionIds) &&
|
|
68
|
-
new Set(manifest.policy.decisionIds).size === manifest.policy.decisionIds.length &&
|
|
69
|
-
manifest.policy.decisionIds.every((decisionId) => typeof decisionId === 'string' && decisionId.trim().length > 0))))));
|
|
152
|
+
validReceipt &&
|
|
153
|
+
receiptMatchesManifest &&
|
|
154
|
+
(manifest.policy === undefined || isGuardRunPolicy(manifest.policy)));
|
|
70
155
|
}
|
|
71
156
|
export function startGuardRun() {
|
|
72
157
|
return {
|
|
@@ -177,7 +262,7 @@ export function summarizeGuardRuns(root, limit = 100) {
|
|
|
177
262
|
...(total > 0 ? { last: runs[0] } : {}),
|
|
178
263
|
};
|
|
179
264
|
}
|
|
180
|
-
export function finishGuardRun(root, context, outcome, gate, command = 'verify', policy) {
|
|
265
|
+
export function finishGuardRun(root, context, outcome, gate, command = 'verify', policy, receipt) {
|
|
181
266
|
let commit;
|
|
182
267
|
try {
|
|
183
268
|
commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, stdio: 'pipe' })
|
|
@@ -187,18 +272,50 @@ export function finishGuardRun(root, context, outcome, gate, command = 'verify',
|
|
|
187
272
|
catch {
|
|
188
273
|
commit = undefined;
|
|
189
274
|
}
|
|
275
|
+
const completedAt = new Date().toISOString();
|
|
276
|
+
const completedReceipt = {
|
|
277
|
+
version: 1,
|
|
278
|
+
kind: 'verification-receipt',
|
|
279
|
+
runId: context.runId,
|
|
280
|
+
generatedAt: completedAt,
|
|
281
|
+
outcome,
|
|
282
|
+
gate,
|
|
283
|
+
subject: {
|
|
284
|
+
...receipt?.subject,
|
|
285
|
+
...(commit ? { commit } : {}),
|
|
286
|
+
changedFiles: receipt?.subject.changedFiles ?? [],
|
|
287
|
+
workingTreeDirty: receipt?.subject.workingTreeDirty ?? false,
|
|
288
|
+
},
|
|
289
|
+
...(policy ? { policy } : {}),
|
|
290
|
+
stages: receipt?.stages ?? {},
|
|
291
|
+
checks: receipt?.checks ?? {},
|
|
292
|
+
findings: receipt?.findings ?? {
|
|
293
|
+
total: 0,
|
|
294
|
+
errors: 0,
|
|
295
|
+
warnings: 0,
|
|
296
|
+
waived: 0,
|
|
297
|
+
waiverWarnings: 0,
|
|
298
|
+
expiredWaivers: 0,
|
|
299
|
+
fingerprints: [],
|
|
300
|
+
ruleIds: [],
|
|
301
|
+
},
|
|
302
|
+
contractIssues: receipt?.contractIssues ?? [],
|
|
303
|
+
decisionIds: receipt?.decisionIds ?? policy?.decisionIds ?? [],
|
|
304
|
+
tool: receipt?.tool ?? { name: '@codapult/guard', version: 'unknown' },
|
|
305
|
+
};
|
|
190
306
|
const manifest = {
|
|
191
307
|
version: 1,
|
|
192
308
|
runId: context.runId,
|
|
193
309
|
command,
|
|
194
310
|
startedAt: context.startedAt,
|
|
195
|
-
completedAt
|
|
311
|
+
completedAt,
|
|
196
312
|
durationMs: Math.max(0, Date.now() - context.startedAtMs),
|
|
197
313
|
outcome,
|
|
198
314
|
gate,
|
|
199
315
|
stages: context.stages,
|
|
200
316
|
...(commit ? { commit } : {}),
|
|
201
317
|
...(policy ? { policy } : {}),
|
|
318
|
+
receipt: completedReceipt,
|
|
202
319
|
};
|
|
203
320
|
try {
|
|
204
321
|
writeGuardRun(root, manifest);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
1
3
|
import {} from '../../adapters/command.js';
|
|
2
4
|
import { runProjectAdapters, runProjectChecks, runWorkspaceProjectChecks, inspectProjectRuntime, } from '../../adapters/project-checks.js';
|
|
3
5
|
import { loadGuardPolicyAtRevision, loadGuardWorkingTreePolicy, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
|
|
4
6
|
import { analyzeProjectImpact } from '../analysis/impact.js';
|
|
5
7
|
import { discoverProject } from '../discovery/discovery.js';
|
|
6
8
|
import { finishGuardRun, recordGuardRunStage, startGuardRun, } from '../history/runs.js';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const packageJson = require('../../../package.json');
|
|
7
11
|
function buildRunPolicy(snapshot, base, changedFromBase, decisionIds = []) {
|
|
8
12
|
return {
|
|
9
13
|
...(snapshot.revision !== undefined ? { revision: snapshot.revision } : {}),
|
|
@@ -14,6 +18,94 @@ function buildRunPolicy(snapshot, base, changedFromBase, decisionIds = []) {
|
|
|
14
18
|
...(decisionIds.length > 0 ? { decisionIds } : {}),
|
|
15
19
|
};
|
|
16
20
|
}
|
|
21
|
+
function receiptCheck(result) {
|
|
22
|
+
return {
|
|
23
|
+
status: result.status,
|
|
24
|
+
exitCode: result.exitCode,
|
|
25
|
+
durationMs: result.durationMs,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveGitCommit(root, ref) {
|
|
29
|
+
if (!ref)
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
const commit = execFileSync('git', ['rev-parse', `${ref}^{commit}`], {
|
|
33
|
+
cwd: root,
|
|
34
|
+
stdio: 'pipe',
|
|
35
|
+
timeout: 5_000,
|
|
36
|
+
})
|
|
37
|
+
.toString()
|
|
38
|
+
.trim();
|
|
39
|
+
return /^[0-9a-f]{40}$/i.test(commit) ? commit : undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function changedFilesSince(root, baseRef) {
|
|
46
|
+
if (!baseRef)
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
return execFileSync('git', ['diff', '--name-only', `${baseRef}...HEAD`], {
|
|
50
|
+
cwd: root,
|
|
51
|
+
stdio: 'pipe',
|
|
52
|
+
timeout: 5_000,
|
|
53
|
+
})
|
|
54
|
+
.toString()
|
|
55
|
+
.split('\n')
|
|
56
|
+
.map((file) => file.trim().replace(/\\/g, '/'))
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.sort();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function buildVerificationReceipt(root, policy, impact, checks, workspaceChecks, adapters, architecture, contractIssues, stages) {
|
|
65
|
+
const baseCommit = resolveGitCommit(root, policy.ref);
|
|
66
|
+
const baseChangedFiles = changedFilesSince(root, policy.ref);
|
|
67
|
+
const workingTreeFiles = impact.requestedFiles;
|
|
68
|
+
const receiptChecks = {};
|
|
69
|
+
for (const [name, result] of Object.entries(checks)) {
|
|
70
|
+
receiptChecks[name] = receiptCheck(result);
|
|
71
|
+
}
|
|
72
|
+
for (const [workspacePackage, packageChecks] of Object.entries(workspaceChecks)) {
|
|
73
|
+
for (const [name, result] of Object.entries(packageChecks)) {
|
|
74
|
+
receiptChecks[`${workspacePackage}:${name}`] = receiptCheck(result);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [name, result] of Object.entries(adapters)) {
|
|
78
|
+
receiptChecks[`adapter:${name}`] = receiptCheck(result);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
kind: 'verification-receipt',
|
|
83
|
+
subject: {
|
|
84
|
+
...(policy.ref ? { baseRef: policy.ref } : {}),
|
|
85
|
+
...(baseCommit ? { baseCommit } : {}),
|
|
86
|
+
...(baseChangedFiles ? { baseChangedFiles } : {}),
|
|
87
|
+
...(workingTreeFiles.length > 0 ? { workingTreeFiles } : {}),
|
|
88
|
+
changedFiles: [...new Set([...(baseChangedFiles ?? []), ...workingTreeFiles])].sort(),
|
|
89
|
+
workingTreeDirty: workingTreeFiles.length > 0,
|
|
90
|
+
},
|
|
91
|
+
policy,
|
|
92
|
+
stages,
|
|
93
|
+
checks: receiptChecks,
|
|
94
|
+
findings: {
|
|
95
|
+
total: architecture.findings.length,
|
|
96
|
+
errors: architecture.findings.filter((finding) => finding.severity === 'error').length,
|
|
97
|
+
warnings: architecture.findings.filter((finding) => finding.severity === 'warning').length,
|
|
98
|
+
waived: architecture.waived,
|
|
99
|
+
waiverWarnings: architecture.waiverWarnings.length,
|
|
100
|
+
expiredWaivers: architecture.expiredWaivers.length,
|
|
101
|
+
fingerprints: architecture.findings.map((finding) => finding.fingerprint).sort(),
|
|
102
|
+
ruleIds: [...new Set(architecture.findings.map((finding) => finding.ruleId))].sort(),
|
|
103
|
+
},
|
|
104
|
+
contractIssues: contractIssues.map((issue) => issue.contractId),
|
|
105
|
+
decisionIds: policy.decisionIds ?? [],
|
|
106
|
+
tool: { name: '@codapult/guard', version: packageJson.version },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
17
109
|
export function runGuardVerification(root, options = {}) {
|
|
18
110
|
const runContext = startGuardRun();
|
|
19
111
|
const requirement = {
|
|
@@ -65,11 +157,12 @@ export function runGuardVerification(root, options = {}) {
|
|
|
65
157
|
};
|
|
66
158
|
}
|
|
67
159
|
if (!workingTreeState) {
|
|
68
|
-
const
|
|
160
|
+
const policyEnforcementFailed = options.failOnPolicyChange === true;
|
|
161
|
+
const run = finishGuardRun(root, runContext, policyEnforcementFailed ? 'fail' : 'not-configured', policyEnforcementFailed ? 'policy-change' : 'configuration');
|
|
69
162
|
return {
|
|
70
|
-
status: 'not-configured',
|
|
71
|
-
outcome: 'not-configured',
|
|
72
|
-
errorCode: 'GUARD_NOT_CONFIGURED',
|
|
163
|
+
status: policyEnforcementFailed ? 'fail' : 'not-configured',
|
|
164
|
+
outcome: policyEnforcementFailed ? 'fail' : 'not-configured',
|
|
165
|
+
errorCode: policyEnforcementFailed ? 'GUARD_POLICY_CHANGED' : 'GUARD_NOT_CONFIGURED',
|
|
73
166
|
recoverable: true,
|
|
74
167
|
checks: {},
|
|
75
168
|
workspaceChecks: {},
|
|
@@ -193,20 +286,27 @@ export function runGuardVerification(root, options = {}) {
|
|
|
193
286
|
const commandFailed = Object.values(results).some((result) => result.status === 'failed');
|
|
194
287
|
const workspaceCommandFailed = Object.values(workspaceChecks).some((packageResults) => Object.values(packageResults).some((result) => result.status === 'failed'));
|
|
195
288
|
const adapterFailed = Object.values(adapters).some((result) => result.status === 'failed');
|
|
196
|
-
const
|
|
197
|
-
|
|
289
|
+
const missingProjectChecks = projectChecks &&
|
|
290
|
+
(checks.length === 0 ||
|
|
291
|
+
Object.values(results).some((result) => result.status === 'not-configured') ||
|
|
292
|
+
Object.values(workspaceChecks).some((packageResults) => Object.values(packageResults).some((result) => result.status === 'not-configured')));
|
|
293
|
+
const missingAdapterTools = options.strict && Object.values(adapters).some((result) => result.status === 'not-configured');
|
|
294
|
+
const runtimeFailed = projectChecks && !runtime.compatible;
|
|
198
295
|
const architectureFailed = architecture.findings.some((finding) => finding.severity === 'error');
|
|
296
|
+
const expiredWaivers = architecture.expiredWaivers.length > 0;
|
|
199
297
|
const architectureWarnings = architecture.findings.some((finding) => finding.severity === 'warning') ||
|
|
200
298
|
(architecture.waiverWarnings ?? []).length > 0;
|
|
201
|
-
const failed =
|
|
299
|
+
const failed = runtimeFailed ||
|
|
202
300
|
commandFailed ||
|
|
203
301
|
workspaceCommandFailed ||
|
|
204
302
|
adapterFailed ||
|
|
205
303
|
architectureFailed ||
|
|
304
|
+
expiredWaivers ||
|
|
206
305
|
contractIssues.length > 0 ||
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
306
|
+
missingProjectChecks ||
|
|
307
|
+
missingAdapterTools;
|
|
308
|
+
const projectChecksFailed = commandFailed || workspaceCommandFailed || missingProjectChecks || runtimeFailed;
|
|
309
|
+
const adaptersFailed = adapterFailed || missingAdapterTools;
|
|
210
310
|
runContext.stages['project-checks'] = {
|
|
211
311
|
...runContext.stages['project-checks'],
|
|
212
312
|
status: projectChecks ? (projectChecksFailed ? 'fail' : 'ok') : 'skipped',
|
|
@@ -215,15 +315,19 @@ export function runGuardVerification(root, options = {}) {
|
|
|
215
315
|
...runContext.stages.adapters,
|
|
216
316
|
status: toolMode === 'off' ? 'skipped' : adaptersFailed ? 'fail' : 'ok',
|
|
217
317
|
};
|
|
318
|
+
runContext.stages.runtime = {
|
|
319
|
+
...runContext.stages.runtime,
|
|
320
|
+
status: runtimeFailed ? 'fail' : 'ok',
|
|
321
|
+
};
|
|
218
322
|
runContext.stages['architecture-policy'] = {
|
|
219
323
|
...runContext.stages['architecture-policy'],
|
|
220
|
-
status: architectureFailed || contractIssues.length > 0 ? 'fail' : 'ok',
|
|
324
|
+
status: architectureFailed || expiredWaivers || contractIssues.length > 0 ? 'fail' : 'ok',
|
|
221
325
|
};
|
|
222
326
|
const outcome = classifyGuardOutcome({
|
|
223
327
|
errors: failed ? 1 : 0,
|
|
224
328
|
warnings: architectureWarnings ? 1 : 0,
|
|
225
329
|
});
|
|
226
|
-
const gate = architectureFailed
|
|
330
|
+
const gate = architectureFailed || expiredWaivers
|
|
227
331
|
? 'architecture'
|
|
228
332
|
: contractIssues.length > 0
|
|
229
333
|
? 'contracts'
|
|
@@ -231,12 +335,15 @@ export function runGuardVerification(root, options = {}) {
|
|
|
231
335
|
? 'project-checks'
|
|
232
336
|
: adapterFailed
|
|
233
337
|
? 'adapters'
|
|
234
|
-
:
|
|
235
|
-
? '
|
|
236
|
-
:
|
|
237
|
-
? '
|
|
238
|
-
:
|
|
239
|
-
|
|
338
|
+
: missingProjectChecks
|
|
339
|
+
? 'project-checks'
|
|
340
|
+
: missingAdapterTools
|
|
341
|
+
? 'required-tools'
|
|
342
|
+
: runtimeFailed
|
|
343
|
+
? 'runtime'
|
|
344
|
+
: 'none';
|
|
345
|
+
const receipt = buildVerificationReceipt(root, policy, impact, results, workspaceChecks, adapters, architecture, contractIssues, Object.fromEntries(Object.entries(runContext.stages).map(([name, stage]) => [name, stage.status])));
|
|
346
|
+
const run = finishGuardRun(root, runContext, outcome, gate, 'verify', policy, receipt);
|
|
240
347
|
return {
|
|
241
348
|
status: failed ? 'fail' : 'ok',
|
|
242
349
|
outcome,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codapult/guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"mcpName": "io.github.codapult/guard",
|
|
5
5
|
"description": "Local-first architecture guardrails for JavaScript and TypeScript projects, with first-class support for Next.js SaaS and AI-assisted development",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"packageManager": "pnpm@12.
|
|
7
|
+
"packageManager": "pnpm@12.14.0+sha512.c18ba6d87993d3d164cb5e8948f32b2c06ee854487e06cd8d86781342fb3e5bb4634367034711e86802baa9c37bea906ce1929ad08d70884c97d801df13389a6",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=20.19.0"
|
|
10
10
|
},
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"semver": "^7.8.5",
|
|
105
105
|
"ts-morph": "^28.0.0",
|
|
106
106
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
107
|
-
"zod": "^4.6.
|
|
107
|
+
"zod": "^4.6.7"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
110
|
"@js-toolkit/config-utils": "^1.0.1",
|
|
@@ -114,13 +114,13 @@
|
|
|
114
114
|
"@release-it/conventional-changelog": "^12.0.2",
|
|
115
115
|
"@types/node": "^26.6.5",
|
|
116
116
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
117
|
-
"@vitest/coverage-v8": "^5.0.
|
|
118
|
-
"eslint": "^10.
|
|
117
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
118
|
+
"eslint": "^10.13.0",
|
|
119
119
|
"eslint-config-prettier": "^10.1.8",
|
|
120
|
-
"eslint-plugin-prettier": "^5.5.
|
|
120
|
+
"eslint-plugin-prettier": "^5.5.7",
|
|
121
121
|
"prettier": "^3.9.10",
|
|
122
122
|
"release-it": "^21.1.1",
|
|
123
|
-
"typescript-eslint": "^8.71.
|
|
124
|
-
"vitest": "^5.0.
|
|
123
|
+
"typescript-eslint": "^8.71.2",
|
|
124
|
+
"vitest": "^5.0.5"
|
|
125
125
|
}
|
|
126
126
|
}
|