@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 CHANGED
@@ -9,11 +9,14 @@
9
9
 
10
10
  ## Architecture guardrails for AI coding agents
11
11
 
12
- `@codapult/guard` is a local-first, model-agnostic architecture guard for JavaScript and
13
- TypeScript projects.
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
- It learns what already exists, records the project’s architectural memory, and can protect future
16
- changes from introducing regressions covered by the project’s approved policy.
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
- (manifest.policy === undefined ||
55
- (manifest.policy !== null &&
56
- typeof manifest.policy === 'object' &&
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: new Date().toISOString(),
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 run = finishGuardRun(root, runContext, 'not-configured', 'configuration');
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 missingRequiredTools = options.strict &&
197
- [...Object.values(results), ...Object.values(adapters)].some((result) => result.status === 'not-configured');
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 = (projectChecks && !runtime.compatible) ||
299
+ const failed = runtimeFailed ||
202
300
  commandFailed ||
203
301
  workspaceCommandFailed ||
204
302
  adapterFailed ||
205
303
  architectureFailed ||
304
+ expiredWaivers ||
206
305
  contractIssues.length > 0 ||
207
- missingRequiredTools;
208
- const projectChecksFailed = commandFailed || workspaceCommandFailed || (projectChecks && !runtime.compatible);
209
- const adaptersFailed = adapterFailed || missingRequiredTools;
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
- : missingRequiredTools
235
- ? 'required-tools'
236
- : !runtime.compatible && projectChecks
237
- ? 'runtime'
238
- : 'none';
239
- const run = finishGuardRun(root, runContext, outcome, gate, 'verify', policy);
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.9.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.4.2+sha512.08adc6613180275c7c9edada39dcf08c9c61ad4e7eaf330a4f3461f102b0f907423454d117f98e72d47fef0616070644d7bffc973a6a57f5090a6d7c368b07c9",
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.5"
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.3",
118
- "eslint": "^10.12.0",
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.6",
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.1",
124
- "vitest": "^5.0.3"
123
+ "typescript-eslint": "^8.71.2",
124
+ "vitest": "^5.0.5"
125
125
  }
126
126
  }