@codapult/guard 0.4.0 → 0.5.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
@@ -327,6 +327,9 @@ and Guard rejects the same declared actor when a proposal was generated with `GU
327
327
  These variables provide declared provenance only; external branch protection or signed identity
328
328
  remains responsible for proving who approved the change.
329
329
 
330
+ See [the extension map](docs/extensions.md) for the supported AI-host, CI/PR, policy-pack, tool
331
+ adapter, approval-governance, and observability integrations.
332
+
330
333
  ## CI
331
334
 
332
335
  Copy the consumer workflow into a project that has installed and initialized Guard:
@@ -5,6 +5,7 @@ export interface CommandResult {
5
5
  exitCode: number;
6
6
  stdout: string;
7
7
  stderr: string;
8
+ durationMs: number;
8
9
  timedOut?: boolean | undefined;
9
10
  truncated?: boolean | undefined;
10
11
  }
@@ -24,6 +24,7 @@ function parseCommand(command) {
24
24
  return { executable: windowsExecutable, args };
25
25
  }
26
26
  export function runProjectCommand(command, cwd, options = {}) {
27
+ const startedAt = Date.now();
27
28
  const parsed = parseCommand(command);
28
29
  if (!parsed) {
29
30
  return {
@@ -33,6 +34,7 @@ export function runProjectCommand(command, cwd, options = {}) {
33
34
  exitCode: 2,
34
35
  stdout: '',
35
36
  stderr: 'Unsafe or empty project command rejected.',
37
+ durationMs: 0,
36
38
  };
37
39
  }
38
40
  try {
@@ -51,6 +53,7 @@ export function runProjectCommand(command, cwd, options = {}) {
51
53
  exitCode: 0,
52
54
  stdout: captured.value,
53
55
  stderr: '',
56
+ durationMs: Math.max(0, Date.now() - startedAt),
54
57
  truncated: captured.truncated,
55
58
  };
56
59
  }
@@ -68,6 +71,7 @@ export function runProjectCommand(command, cwd, options = {}) {
68
71
  exitCode,
69
72
  stdout: stdout.value,
70
73
  stderr: timedOut ? `${stderr.value}\n[command timed out]` : stderr.value,
74
+ durationMs: Math.max(0, Date.now() - startedAt),
71
75
  timedOut,
72
76
  truncated: stdout.truncated || stderr.truncated,
73
77
  };
@@ -12,6 +12,7 @@ function notConfigured(check) {
12
12
  exitCode: 0,
13
13
  stdout: '',
14
14
  stderr: `${check} script is not configured`,
15
+ durationMs: 0,
15
16
  };
16
17
  }
17
18
  function readPackageMetadata(root) {
@@ -119,6 +120,9 @@ const adapterScriptPatterns = {
119
120
  'dependency-graph': /(?:dep(?:endency)?[-:]?(?:check|graph|cruise)|madge|architecture)/i,
120
121
  security: /(?:security|semgrep|gitleaks|snyk|trivy|audit)/i,
121
122
  'dependency-hygiene': /(?:knip|dep(?:endency)?[-:]?(?:unused|hygiene))/i,
123
+ sast: /(?:semgrep|codeql|bearer|sast)/i,
124
+ 'secret-scanning': /(?:gitleaks|trufflehog|secret[-_ ]?scan)/i,
125
+ 'dependency-audit': /(?:npm audit|pnpm audit|yarn audit|osv|snyk|trivy|dependency[-_ ]?audit)/i,
122
126
  };
123
127
  /** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
124
128
  export function runProjectAdapters(root, options = {}) {
@@ -126,6 +130,7 @@ export function runProjectAdapters(root, options = {}) {
126
130
  const manager = packageManager(root, metadata.packageManager);
127
131
  const scripts = metadata.scripts ?? {};
128
132
  const results = {};
133
+ const executed = new Map();
129
134
  for (const [adapter, pattern] of Object.entries(adapterScriptPatterns)) {
130
135
  const configured = options.tooling?.[adapter];
131
136
  const script = configured?.enabled
@@ -141,9 +146,13 @@ export function runProjectAdapters(root, options = {}) {
141
146
  continue;
142
147
  }
143
148
  const command = manager === 'npm' ? `npm run ${script}` : `${manager} run ${script}`;
144
- results[adapter] = runProjectCommand(command, root, {
145
- timeout: options.timeout ?? 120_000,
146
- });
149
+ const previous = executed.get(command);
150
+ const result = previous ??
151
+ runProjectCommand(command, root, {
152
+ timeout: options.timeout ?? 120_000,
153
+ });
154
+ executed.set(command, result);
155
+ results[adapter] = result;
147
156
  }
148
157
  return results;
149
158
  }
@@ -42,6 +42,7 @@ export declare function guardDoctorCommand(options?: GuardOutputOptions & {
42
42
  fixCache?: boolean | undefined;
43
43
  }): void;
44
44
  export declare function guardHistoryCommand(): void;
45
+ export declare function guardRunsCommand(options?: GuardOutputOptions): void;
45
46
  export declare function guardImpactCommand(files: string[], options?: GuardOutputOptions): void;
46
47
  export declare function guardHistoryDiffCommand(from: string, to: string, options?: GuardOutputOptions): void;
47
48
  export declare function guardVerifyCommand(options?: GuardVerifyOptions): void;
@@ -9,6 +9,7 @@ import { guardAgentTargets, installGuardAgentInstructions, installGuardAgentTarg
9
9
  import { dim, fail, heading, info, success, warn } from '../ui.js';
10
10
  import { guardFindingsToSarif } from '../../core/output/sarif.js';
11
11
  import { analyzeProjectImpact } from '../../core/analysis/impact.js';
12
+ import { listGuardRuns, summarizeGuardRuns } from '../../core/history/runs.js';
12
13
  function renderFindings(findings) {
13
14
  for (const finding of findings) {
14
15
  const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
@@ -194,6 +195,19 @@ export function guardHistoryCommand() {
194
195
  info(`${snapshot.revision}: ${snapshot.files} files, ${snapshot.modules} modules, ${snapshot.cycles} cycles`);
195
196
  }
196
197
  }
198
+ export function guardRunsCommand(options = {}) {
199
+ const root = getRoot();
200
+ const result = { summary: summarizeGuardRuns(root), runs: listGuardRuns(root) };
201
+ if (options.json) {
202
+ console.log(JSON.stringify(result, null, 2));
203
+ return;
204
+ }
205
+ heading('Codapult Guard Runs');
206
+ info(`${result.summary.total} run(s); ${result.summary.failed} failed, ${result.summary.warnings} warning, average ${result.summary.averageDurationMs} ms`);
207
+ for (const run of result.runs) {
208
+ info(`${run.runId}: ${run.outcome}, gate=${run.gate}, ${run.durationMs} ms`);
209
+ }
210
+ }
197
211
  export function guardImpactCommand(files, options = {}) {
198
212
  const root = getRoot();
199
213
  if (files.length === 0) {
package/dist/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
- import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
4
+ import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRunsCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
5
5
  import { config } from '../core/config.js';
6
6
  import { GuardBaselineReasonError, GuardConfigError, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
7
7
  import { guardErrorPayload } from '../core/errors.js';
@@ -22,6 +22,7 @@ guard.command('propose').option('--json').action(guardProposeCommand);
22
22
  guard.command('install-agent [target]').option('--json').action(guardInstallAgentCommand);
23
23
  guard.command('doctor').option('--json').option('--fix-cache').action(guardDoctorCommand);
24
24
  guard.command('history').action(guardHistoryCommand);
25
+ guard.command('runs').option('--json').action(guardRunsCommand);
25
26
  guard.command('history-diff <from> <to>').option('--json').action(guardHistoryDiffCommand);
26
27
  guard.command('impact <files...>').option('--json').action(guardImpactCommand);
27
28
  guard
@@ -1,4 +1,5 @@
1
1
  export declare const GUARD_RUNS_DIR: string;
2
+ export declare const GUARD_RUN_RETENTION = 200;
2
3
  export interface GuardRunStage {
3
4
  durationMs: number;
4
5
  status: 'ok' | 'fail' | 'skipped';
@@ -22,7 +23,21 @@ export interface GuardRunContext {
22
23
  startedAtMs: number;
23
24
  stages: Record<string, GuardRunStage>;
24
25
  }
26
+ export interface GuardRunSummary {
27
+ total: number;
28
+ passed: number;
29
+ failed: number;
30
+ warnings: number;
31
+ averageDurationMs: number;
32
+ last?: GuardRunManifest | undefined;
33
+ }
34
+ export declare class GuardRunManifestError extends Error {
35
+ constructor();
36
+ }
25
37
  export declare function startGuardRun(): GuardRunContext;
26
38
  export declare function recordGuardRunStage(context: GuardRunContext, name: string, status: GuardRunStage['status'], startedAtMs: number, detail?: string): void;
27
39
  export declare function writeGuardRun(root: string, manifest: GuardRunManifest): void;
40
+ /** Reads locally persisted run manifests without reaching a remote service. */
41
+ export declare function listGuardRuns(root: string, limit?: number): GuardRunManifest[];
42
+ export declare function summarizeGuardRuns(root: string, limit?: number): GuardRunSummary;
28
43
  export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string): GuardRunManifest;
@@ -1,9 +1,40 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { execFileSync } from 'node:child_process';
3
- import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
5
  import { config } from '../config.js';
6
6
  export const GUARD_RUNS_DIR = `.${config.appName}/guard/history/runs`;
7
+ export const GUARD_RUN_RETENTION = 200;
8
+ export class GuardRunManifestError extends Error {
9
+ constructor() {
10
+ super('Guard run manifest is invalid or contains an unsafe run ID.');
11
+ this.name = 'GuardRunManifestError';
12
+ }
13
+ }
14
+ function isGuardRunManifest(value) {
15
+ if (value === null || typeof value !== 'object')
16
+ return false;
17
+ const manifest = value;
18
+ return (manifest.version === 1 &&
19
+ typeof manifest.runId === 'string' &&
20
+ /^guard-[A-Za-z0-9-]+$/.test(manifest.runId) &&
21
+ typeof manifest.command === 'string' &&
22
+ typeof manifest.startedAt === 'string' &&
23
+ Number.isFinite(Date.parse(manifest.startedAt)) &&
24
+ typeof manifest.completedAt === 'string' &&
25
+ Number.isFinite(Date.parse(manifest.completedAt)) &&
26
+ typeof manifest.durationMs === 'number' &&
27
+ Number.isFinite(manifest.durationMs) &&
28
+ manifest.durationMs >= 0 &&
29
+ (manifest.outcome === 'pass' ||
30
+ manifest.outcome === 'fail' ||
31
+ manifest.outcome === 'warning' ||
32
+ manifest.outcome === 'needs-review' ||
33
+ manifest.outcome === 'not-configured') &&
34
+ typeof manifest.gate === 'string' &&
35
+ manifest.stages !== null &&
36
+ typeof manifest.stages === 'object');
37
+ }
7
38
  export function startGuardRun() {
8
39
  return {
9
40
  runId: `guard-${randomUUID()}`,
@@ -19,13 +50,77 @@ export function recordGuardRunStage(context, name, status, startedAtMs, detail)
19
50
  ...(detail ? { detail } : {}),
20
51
  };
21
52
  }
53
+ function pruneGuardRuns(root) {
54
+ const directory = resolve(root, GUARD_RUNS_DIR);
55
+ try {
56
+ const files = readdirSync(directory)
57
+ .filter((file) => file.endsWith('.json'))
58
+ .map((file) => ({
59
+ file,
60
+ modifiedAt: statSync(resolve(directory, file)).mtimeMs,
61
+ }))
62
+ .sort((left, right) => right.modifiedAt - left.modifiedAt);
63
+ for (const entry of files.slice(GUARD_RUN_RETENTION)) {
64
+ try {
65
+ unlinkSync(resolve(directory, entry.file));
66
+ }
67
+ catch {
68
+ // Retention is best effort; a concurrent reader may own the file.
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ // Diagnostics must never fail because retention cannot be completed.
74
+ }
75
+ }
22
76
  export function writeGuardRun(root, manifest) {
77
+ if (!isGuardRunManifest(manifest))
78
+ throw new GuardRunManifestError();
23
79
  const directory = resolve(root, GUARD_RUNS_DIR);
24
80
  mkdirSync(directory, { recursive: true });
25
81
  const path = resolve(directory, `${manifest.runId}.json`);
26
82
  const temporaryPath = `${path}.tmp-${process.pid}-${manifest.runId}`;
27
83
  writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
28
84
  renameSync(temporaryPath, path);
85
+ pruneGuardRuns(root);
86
+ }
87
+ /** Reads locally persisted run manifests without reaching a remote service. */
88
+ export function listGuardRuns(root, limit = 20) {
89
+ if (!Number.isInteger(limit) || limit < 1)
90
+ return [];
91
+ const boundedLimit = Math.min(limit, 1_000);
92
+ const directory = resolve(root, GUARD_RUNS_DIR);
93
+ try {
94
+ return readdirSync(directory)
95
+ .filter((file) => file.endsWith('.json'))
96
+ .map((file) => {
97
+ try {
98
+ const value = JSON.parse(readFileSync(resolve(directory, file), 'utf8'));
99
+ return isGuardRunManifest(value) ? value : undefined;
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ })
105
+ .filter((manifest) => manifest !== undefined)
106
+ .sort((left, right) => right.completedAt.localeCompare(left.completedAt))
107
+ .slice(0, boundedLimit);
108
+ }
109
+ catch {
110
+ return [];
111
+ }
112
+ }
113
+ export function summarizeGuardRuns(root, limit = 100) {
114
+ const runs = listGuardRuns(root, limit);
115
+ const total = runs.length;
116
+ return {
117
+ total,
118
+ passed: runs.filter((run) => run.outcome === 'pass').length,
119
+ failed: runs.filter((run) => run.outcome === 'fail').length,
120
+ warnings: runs.filter((run) => run.outcome === 'warning').length,
121
+ averageDurationMs: total === 0 ? 0 : Math.round(runs.reduce((sum, run) => sum + run.durationMs, 0) / total),
122
+ ...(total > 0 ? { last: runs[0] } : {}),
123
+ };
29
124
  }
30
125
  export function finishGuardRun(root, context, outcome, gate, command = 'verify') {
31
126
  let commit;
@@ -6,5 +6,5 @@ export type GuardContractKind = 'guidance' | 'import-boundary' | 'required-call'
6
6
  export type GuardBudgetMetric = 'lines' | 'bytes' | 'imports';
7
7
  export type GuardToolMode = 'auto' | 'on' | 'off';
8
8
  export type GuardApprovalMode = 'local' | 'protected';
9
- export type GuardAdapterName = 'dependency-graph' | 'security' | 'dependency-hygiene';
9
+ export type GuardAdapterName = 'dependency-graph' | 'security' | 'dependency-hygiene' | 'sast' | 'secret-scanning' | 'dependency-audit';
10
10
  export type ProjectCheck = 'lint' | 'typecheck' | 'test' | 'build';
@@ -1,6 +1,7 @@
1
1
  import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
2
2
  import { guardErrorPayload } from '../core/errors.js';
3
3
  import { DiscoveryStaleError } from '../core/discovery/discovery.js';
4
+ import { listGuardRuns, summarizeGuardRuns } from '../core/history/runs.js';
4
5
  function jsonResource(uri, value) {
5
6
  return {
6
7
  contents: [{ uri, text: JSON.stringify(value, null, 2), mimeType: 'application/json' }],
@@ -20,6 +21,19 @@ function resourceError(uri, error) {
20
21
  }));
21
22
  }
22
23
  export function registerGuardResources(server) {
24
+ server.registerResource('codapult_guard_runs', 'codapult://guard/runs', {
25
+ title: 'Guard Run History',
26
+ description: 'Local verification outcomes, gates, and stage timings.',
27
+ mimeType: 'application/json',
28
+ }, () => {
29
+ const root = findGuardRoot();
30
+ return jsonResource('codapult://guard/runs', {
31
+ version: 1,
32
+ root,
33
+ summary: summarizeGuardRuns(root),
34
+ runs: listGuardRuns(root),
35
+ });
36
+ });
23
37
  server.registerResource('codapult_guard_rules', 'codapult://guard/rules', {
24
38
  title: 'Architecture Guard Rules',
25
39
  description: 'Local architecture invariants available to CLI checks and AI agents',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codapult/guard",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Local-first architecture guardrails for JavaScript and TypeScript projects, with first-class support for Next.js SaaS and AI-assisted development",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@12.4.2+sha512.08adc6613180275c7c9edada39dcf08c9c61ad4e7eaf330a4f3461f102b0f907423454d117f98e72d47fef0616070644d7bffc973a6a57f5090a6d7c368b07c9",