@codapult/guard 0.3.0 → 0.4.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.
@@ -0,0 +1,28 @@
1
+ export declare const GUARD_RUNS_DIR: string;
2
+ export interface GuardRunStage {
3
+ durationMs: number;
4
+ status: 'ok' | 'fail' | 'skipped';
5
+ detail?: string | undefined;
6
+ }
7
+ export interface GuardRunManifest {
8
+ version: 1;
9
+ runId: string;
10
+ command: string;
11
+ startedAt: string;
12
+ completedAt: string;
13
+ durationMs: number;
14
+ outcome: 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
15
+ gate: string;
16
+ stages: Record<string, GuardRunStage>;
17
+ commit?: string | undefined;
18
+ }
19
+ export interface GuardRunContext {
20
+ runId: string;
21
+ startedAt: string;
22
+ startedAtMs: number;
23
+ stages: Record<string, GuardRunStage>;
24
+ }
25
+ export declare function startGuardRun(): GuardRunContext;
26
+ export declare function recordGuardRunStage(context: GuardRunContext, name: string, status: GuardRunStage['status'], startedAtMs: number, detail?: string): void;
27
+ export declare function writeGuardRun(root: string, manifest: GuardRunManifest): void;
28
+ export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string): GuardRunManifest;
@@ -0,0 +1,59 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { config } from '../config.js';
6
+ export const GUARD_RUNS_DIR = `.${config.appName}/guard/history/runs`;
7
+ export function startGuardRun() {
8
+ return {
9
+ runId: `guard-${randomUUID()}`,
10
+ startedAt: new Date().toISOString(),
11
+ startedAtMs: Date.now(),
12
+ stages: {},
13
+ };
14
+ }
15
+ export function recordGuardRunStage(context, name, status, startedAtMs, detail) {
16
+ context.stages[name] = {
17
+ durationMs: Math.max(0, Date.now() - startedAtMs),
18
+ status,
19
+ ...(detail ? { detail } : {}),
20
+ };
21
+ }
22
+ export function writeGuardRun(root, manifest) {
23
+ const directory = resolve(root, GUARD_RUNS_DIR);
24
+ mkdirSync(directory, { recursive: true });
25
+ const path = resolve(directory, `${manifest.runId}.json`);
26
+ const temporaryPath = `${path}.tmp-${process.pid}-${manifest.runId}`;
27
+ writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
28
+ renameSync(temporaryPath, path);
29
+ }
30
+ export function finishGuardRun(root, context, outcome, gate, command = 'verify') {
31
+ let commit;
32
+ try {
33
+ commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, stdio: 'pipe' })
34
+ .toString()
35
+ .trim();
36
+ }
37
+ catch {
38
+ commit = undefined;
39
+ }
40
+ const manifest = {
41
+ version: 1,
42
+ runId: context.runId,
43
+ command,
44
+ startedAt: context.startedAt,
45
+ completedAt: new Date().toISOString(),
46
+ durationMs: Math.max(0, Date.now() - context.startedAtMs),
47
+ outcome,
48
+ gate,
49
+ stages: context.stages,
50
+ ...(commit ? { commit } : {}),
51
+ };
52
+ try {
53
+ writeGuardRun(root, manifest);
54
+ }
55
+ catch {
56
+ // Verification must not fail because its local diagnostic record cannot be written.
57
+ }
58
+ return manifest;
59
+ }
@@ -0,0 +1,10 @@
1
+ /** Shared domain vocabulary used by Guard core and its adapters. */
2
+ export type GuardSeverity = 'error' | 'warning' | 'info';
3
+ export type GuardRuleKind = 'forbidden-import' | 'client-forbidden-import';
4
+ export type GuardRuleStatus = 'active' | 'proposed';
5
+ export type GuardContractKind = 'guidance' | 'import-boundary' | 'required-call' | 'package-boundary';
6
+ export type GuardBudgetMetric = 'lines' | 'bytes' | 'imports';
7
+ export type GuardToolMode = 'auto' | 'on' | 'off';
8
+ export type GuardApprovalMode = 'local' | 'protected';
9
+ export type GuardAdapterName = 'dependency-graph' | 'security' | 'dependency-hygiene';
10
+ export type ProjectCheck = 'lint' | 'typecheck' | 'test' | 'build';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,26 @@
1
1
  import { z } from 'zod';
2
+ export declare const guardBudgetSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ description: z.ZodString;
5
+ metric: z.ZodEnum<{
6
+ bytes: "bytes";
7
+ imports: "imports";
8
+ lines: "lines";
9
+ }>;
10
+ scope: z.ZodArray<z.ZodString>;
11
+ limit: z.ZodNumber;
12
+ severity: z.ZodEnum<{
13
+ error: "error";
14
+ info: "info";
15
+ warning: "warning";
16
+ }>;
17
+ reason: z.ZodString;
18
+ status: z.ZodOptional<z.ZodEnum<{
19
+ active: "active";
20
+ proposed: "proposed";
21
+ }>>;
22
+ evidence: z.ZodOptional<z.ZodArray<z.ZodString>>;
23
+ }, z.core.$strip>;
2
24
  export declare const guardRuleSchema: z.ZodObject<{
3
25
  id: z.ZodString;
4
26
  description: z.ZodString;
@@ -61,6 +83,8 @@ export declare const guardContractSchema: z.ZodObject<{
61
83
  }, z.core.$strip>;
62
84
  export declare const guardConfigSchema: z.ZodObject<{
63
85
  version: z.ZodLiteral<1>;
86
+ revision: z.ZodOptional<z.ZodNumber>;
87
+ contentFingerprint: z.ZodOptional<z.ZodString>;
64
88
  rules: z.ZodArray<z.ZodObject<{
65
89
  id: z.ZodString;
66
90
  description: z.ZodString;
@@ -121,6 +145,36 @@ export declare const guardConfigSchema: z.ZodObject<{
121
145
  }>>;
122
146
  evidence: z.ZodOptional<z.ZodArray<z.ZodString>>;
123
147
  }, z.core.$strip>>>;
148
+ budgets: z.ZodOptional<z.ZodArray<z.ZodObject<{
149
+ id: z.ZodString;
150
+ description: z.ZodString;
151
+ metric: z.ZodEnum<{
152
+ bytes: "bytes";
153
+ imports: "imports";
154
+ lines: "lines";
155
+ }>;
156
+ scope: z.ZodArray<z.ZodString>;
157
+ limit: z.ZodNumber;
158
+ severity: z.ZodEnum<{
159
+ error: "error";
160
+ info: "info";
161
+ warning: "warning";
162
+ }>;
163
+ reason: z.ZodString;
164
+ status: z.ZodOptional<z.ZodEnum<{
165
+ active: "active";
166
+ proposed: "proposed";
167
+ }>>;
168
+ evidence: z.ZodOptional<z.ZodArray<z.ZodString>>;
169
+ }, z.core.$strip>>>;
170
+ approval: z.ZodOptional<z.ZodObject<{
171
+ mode: z.ZodEnum<{
172
+ local: "local";
173
+ protected: "protected";
174
+ }>;
175
+ allowMcpApproval: z.ZodBoolean;
176
+ requireDistinctActor: z.ZodBoolean;
177
+ }, z.core.$strip>>;
124
178
  }, z.core.$strip>;
125
179
  export declare const guardContractsFileSchema: z.ZodObject<{
126
180
  version: z.ZodLiteral<1>;
@@ -174,10 +228,18 @@ export declare const guardProposalDecisionSchema: z.ZodObject<{
174
228
  proposalId: z.ZodOptional<z.ZodString>;
175
229
  proposalFingerprint: z.ZodOptional<z.ZodString>;
176
230
  revision: z.ZodOptional<z.ZodNumber>;
231
+ source: z.ZodOptional<z.ZodEnum<{
232
+ cli: "cli";
233
+ external: "external";
234
+ mcp: "mcp";
235
+ }>>;
236
+ actor: z.ZodOptional<z.ZodString>;
237
+ commit: z.ZodOptional<z.ZodString>;
177
238
  }, z.core.$strip>;
178
239
  export declare const guardProposalSchema: z.ZodObject<{
179
240
  version: z.ZodLiteral<1>;
180
241
  generatedAt: z.ZodString;
242
+ generatedBy: z.ZodOptional<z.ZodString>;
181
243
  proposalId: z.ZodOptional<z.ZodString>;
182
244
  projectFingerprint: z.ZodOptional<z.ZodString>;
183
245
  revision: z.ZodOptional<z.ZodNumber>;
@@ -257,6 +319,13 @@ export declare const guardProposalSchema: z.ZodObject<{
257
319
  proposalId: z.ZodOptional<z.ZodString>;
258
320
  proposalFingerprint: z.ZodOptional<z.ZodString>;
259
321
  revision: z.ZodOptional<z.ZodNumber>;
322
+ source: z.ZodOptional<z.ZodEnum<{
323
+ cli: "cli";
324
+ external: "external";
325
+ mcp: "mcp";
326
+ }>>;
327
+ actor: z.ZodOptional<z.ZodString>;
328
+ commit: z.ZodOptional<z.ZodString>;
260
329
  }, z.core.$strip>>>;
261
330
  }, z.core.$strip>;
262
331
  export declare const guardAgentConfigSchema: z.ZodObject<{
@@ -1,6 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  const severity = z.enum(['error', 'warning', 'info']);
3
3
  const status = z.enum(['active', 'proposed']);
4
+ export const guardBudgetSchema = z.object({
5
+ id: z.string().trim().min(1),
6
+ description: z.string().trim().min(1),
7
+ metric: z.enum(['lines', 'bytes', 'imports']),
8
+ scope: z.array(z.string().trim().min(1)).min(1),
9
+ limit: z.number().int().positive(),
10
+ severity,
11
+ reason: z.string().trim().min(1),
12
+ status: status.optional(),
13
+ evidence: z.array(z.string()).optional(),
14
+ });
4
15
  export const guardRuleSchema = z.object({
5
16
  id: z.string().trim().min(1),
6
17
  description: z.string().trim().min(1),
@@ -33,8 +44,18 @@ export const guardContractSchema = z.object({
33
44
  });
34
45
  export const guardConfigSchema = z.object({
35
46
  version: z.literal(1),
47
+ revision: z.number().int().nonnegative().optional(),
48
+ contentFingerprint: z.string().optional(),
36
49
  rules: z.array(guardRuleSchema),
37
50
  contracts: z.array(guardContractSchema).optional(),
51
+ budgets: z.array(guardBudgetSchema).optional(),
52
+ approval: z
53
+ .object({
54
+ mode: z.enum(['local', 'protected']),
55
+ allowMcpApproval: z.boolean(),
56
+ requireDistinctActor: z.boolean(),
57
+ })
58
+ .optional(),
38
59
  });
39
60
  export const guardContractsFileSchema = z.object({
40
61
  version: z.literal(1),
@@ -48,10 +69,14 @@ export const guardProposalDecisionSchema = z.object({
48
69
  proposalId: z.string().optional(),
49
70
  proposalFingerprint: z.string().optional(),
50
71
  revision: z.number().optional(),
72
+ source: z.enum(['cli', 'mcp', 'external']).optional(),
73
+ actor: z.string().optional(),
74
+ commit: z.string().optional(),
51
75
  });
52
76
  export const guardProposalSchema = z.object({
53
77
  version: z.literal(1),
54
78
  generatedAt: z.string(),
79
+ generatedBy: z.string().trim().min(1).optional(),
55
80
  proposalId: z.string().optional(),
56
81
  projectFingerprint: z.string().optional(),
57
82
  revision: z.number().int().positive().optional(),
@@ -1,34 +1,38 @@
1
1
  import { type CommandResult } from '../../adapters/command.js';
2
- import { type GuardAdapter, type ProjectCheck, type ProjectCheckResults, type ProjectRuntimeDiagnostics } from '../../adapters/project-checks.js';
2
+ import type { ProjectCheck } from '../model/types.js';
3
+ import { type GuardAdapter, type ProjectCheckResults, type ProjectRuntimeDiagnostics } from '../../adapters/project-checks.js';
3
4
  import { type GuardToolMode, type GuardContractIssue, type GuardReport } from '../guard.js';
4
5
  import { type GuardImpactAnalysis } from '../analysis/impact.js';
6
+ import { type GuardRunManifest } from '../history/runs.js';
5
7
  export type GuardVerificationCheck = ProjectCheck;
8
+ export interface GuardVerificationOptions {
9
+ checks?: GuardVerificationCheck[] | undefined;
10
+ changedOnly?: boolean | undefined;
11
+ timeout?: number | undefined;
12
+ requirement?: string | undefined;
13
+ tools?: GuardToolMode | undefined;
14
+ strict?: boolean | undefined;
15
+ projectChecks?: boolean | undefined;
16
+ }
6
17
  export interface GuardVerificationResult {
7
18
  status: 'ok' | 'fail' | 'not-configured';
8
19
  outcome: 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
9
- errorCode?: 'GUARD_NOT_CONFIGURED' | 'GUARD_CONFIG_INVALID';
10
- recoverable?: boolean;
11
- tools?: GuardToolMode;
20
+ errorCode?: 'GUARD_NOT_CONFIGURED' | 'GUARD_CONFIG_INVALID' | undefined;
21
+ recoverable?: boolean | undefined;
22
+ tools?: GuardToolMode | undefined;
12
23
  checks: ProjectCheckResults;
13
24
  workspaceChecks: Record<string, ProjectCheckResults>;
14
25
  adapters: Partial<Record<GuardAdapter, CommandResult>>;
15
26
  runtime: ProjectRuntimeDiagnostics;
16
- configError?: string;
17
- architecture?: GuardReport;
18
- impact?: GuardImpactAnalysis;
27
+ configError?: string | undefined;
28
+ architecture?: GuardReport | undefined;
29
+ impact?: GuardImpactAnalysis | undefined;
19
30
  contractIssues: GuardContractIssue[];
20
31
  requirement: {
21
32
  status: 'delegated-to-review';
22
33
  provided: boolean;
23
34
  message: string;
24
35
  };
36
+ run: GuardRunManifest;
25
37
  }
26
- export declare function runGuardVerification(root: string, options?: {
27
- checks?: GuardVerificationCheck[];
28
- changedOnly?: boolean;
29
- timeout?: number;
30
- requirement?: string;
31
- tools?: GuardToolMode;
32
- strict?: boolean;
33
- projectChecks?: boolean;
34
- }): GuardVerificationResult;
38
+ export declare function runGuardVerification(root: string, options?: GuardVerificationOptions): GuardVerificationResult;
@@ -1,8 +1,11 @@
1
+ import {} from '../../adapters/command.js';
1
2
  import { runProjectAdapters, runProjectChecks, runWorkspaceProjectChecks, inspectProjectRuntime, } from '../../adapters/project-checks.js';
2
- import { loadBaseline, loadGuardAgentConfig, loadGuardConfig, scanGuard, classifyGuardOutcome, validateGuardContracts, } from '../guard.js';
3
+ import { loadBaseline, loadGuardAgentConfig, loadGuardConfig, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
3
4
  import { analyzeProjectImpact } from '../analysis/impact.js';
4
5
  import { discoverProject } from '../discovery/discovery.js';
6
+ import { finishGuardRun, recordGuardRunStage, startGuardRun, } from '../history/runs.js';
5
7
  export function runGuardVerification(root, options = {}) {
8
+ const runContext = startGuardRun();
6
9
  const requirement = {
7
10
  status: 'delegated-to-review',
8
11
  provided: Boolean(options.requirement?.trim()),
@@ -10,12 +13,15 @@ export function runGuardVerification(root, options = {}) {
10
13
  ? 'Requirement text is included in semantic review input; deterministic verify does not judge natural-language acceptance criteria.'
11
14
  : 'Requirement satisfaction is evaluated by guard review using the task and diff.',
12
15
  };
16
+ const runtimeStartedAt = Date.now();
13
17
  const runtime = inspectProjectRuntime(root);
18
+ recordGuardRunStage(runContext, 'runtime', 'ok', runtimeStartedAt);
14
19
  let config;
15
20
  try {
16
21
  config = loadGuardConfig(root);
17
22
  }
18
23
  catch (error) {
24
+ const run = finishGuardRun(root, runContext, 'fail', 'configuration');
19
25
  return {
20
26
  status: 'fail',
21
27
  outcome: 'fail',
@@ -28,9 +34,11 @@ export function runGuardVerification(root, options = {}) {
28
34
  configError: error instanceof Error ? error.message : String(error),
29
35
  contractIssues: [],
30
36
  requirement,
37
+ run,
31
38
  };
32
39
  }
33
- if (!config)
40
+ if (!config) {
41
+ const run = finishGuardRun(root, runContext, 'not-configured', 'configuration');
34
42
  return {
35
43
  status: 'not-configured',
36
44
  outcome: 'not-configured',
@@ -42,11 +50,16 @@ export function runGuardVerification(root, options = {}) {
42
50
  runtime,
43
51
  contractIssues: [],
44
52
  requirement,
53
+ run,
45
54
  };
55
+ }
46
56
  const agentConfig = loadGuardAgentConfig(root);
47
57
  const projectChecks = options.projectChecks ?? agentConfig.completionGate.projectChecks;
48
58
  const checks = projectChecks ? (options.checks ?? agentConfig.completionGate.checks) : [];
59
+ const checksStartedAt = Date.now();
49
60
  const results = runProjectChecks(root, checks, { timeout: options.timeout });
61
+ recordGuardRunStage(runContext, 'project-checks', projectChecks ? 'ok' : 'skipped', checksStartedAt);
62
+ const discoveryStartedAt = Date.now();
50
63
  const workspaceModel = discoverProject(root);
51
64
  const impact = analyzeProjectImpact(workspaceModel, options.changedOnly === false
52
65
  ? workspaceModel.modules.map((module) => module.path)
@@ -55,7 +68,9 @@ export function runGuardVerification(root, options = {}) {
55
68
  timeout: options.timeout,
56
69
  rootResults: results,
57
70
  });
71
+ recordGuardRunStage(runContext, 'discovery-impact', 'ok', discoveryStartedAt);
58
72
  const toolMode = options.tools ?? agentConfig.tools;
73
+ const adaptersStartedAt = Date.now();
59
74
  const adapters = toolMode === 'off'
60
75
  ? {}
61
76
  : runProjectAdapters(root, {
@@ -63,12 +78,15 @@ export function runGuardVerification(root, options = {}) {
63
78
  timeout: options.timeout,
64
79
  tooling: agentConfig.tooling,
65
80
  });
81
+ recordGuardRunStage(runContext, 'adapters', toolMode === 'off' ? 'skipped' : 'ok', adaptersStartedAt);
82
+ const architectureStartedAt = Date.now();
66
83
  const architecture = scanGuard(root, config, {
67
84
  changedOnly: options.changedOnly,
68
85
  baseline: loadBaseline(root),
69
86
  includeArchitectureInsights: true,
70
87
  });
71
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
88
+ const contractIssues = validateGuardPolicy(root, config);
89
+ recordGuardRunStage(runContext, 'architecture-policy', 'ok', architectureStartedAt);
72
90
  const commandFailed = Object.values(results).some((result) => result.status === 'failed');
73
91
  const workspaceCommandFailed = Object.values(workspaceChecks).some((packageResults) => Object.values(packageResults).some((result) => result.status === 'failed'));
74
92
  const adapterFailed = Object.values(adapters).some((result) => result.status === 'failed');
@@ -83,12 +101,41 @@ export function runGuardVerification(root, options = {}) {
83
101
  architectureFailed ||
84
102
  contractIssues.length > 0 ||
85
103
  missingRequiredTools;
104
+ const projectChecksFailed = commandFailed || workspaceCommandFailed || (projectChecks && !runtime.compatible);
105
+ const adaptersFailed = adapterFailed || missingRequiredTools;
106
+ runContext.stages['project-checks'] = {
107
+ ...runContext.stages['project-checks'],
108
+ status: projectChecks ? (projectChecksFailed ? 'fail' : 'ok') : 'skipped',
109
+ };
110
+ runContext.stages.adapters = {
111
+ ...runContext.stages.adapters,
112
+ status: toolMode === 'off' ? 'skipped' : adaptersFailed ? 'fail' : 'ok',
113
+ };
114
+ runContext.stages['architecture-policy'] = {
115
+ ...runContext.stages['architecture-policy'],
116
+ status: architectureFailed || contractIssues.length > 0 ? 'fail' : 'ok',
117
+ };
118
+ const outcome = classifyGuardOutcome({
119
+ errors: failed ? 1 : 0,
120
+ warnings: architectureWarnings ? 1 : 0,
121
+ });
122
+ const gate = architectureFailed
123
+ ? 'architecture'
124
+ : contractIssues.length > 0
125
+ ? 'contracts'
126
+ : commandFailed || workspaceCommandFailed
127
+ ? 'project-checks'
128
+ : adapterFailed
129
+ ? 'adapters'
130
+ : missingRequiredTools
131
+ ? 'required-tools'
132
+ : !runtime.compatible && projectChecks
133
+ ? 'runtime'
134
+ : 'none';
135
+ const run = finishGuardRun(root, runContext, outcome, gate);
86
136
  return {
87
137
  status: failed ? 'fail' : 'ok',
88
- outcome: classifyGuardOutcome({
89
- errors: failed ? 1 : 0,
90
- warnings: architectureWarnings ? 1 : 0,
91
- }),
138
+ outcome,
92
139
  tools: toolMode,
93
140
  checks: results,
94
141
  workspaceChecks,
@@ -98,5 +145,6 @@ export function runGuardVerification(root, options = {}) {
98
145
  impact,
99
146
  contractIssues,
100
147
  requirement,
148
+ run,
101
149
  };
102
150
  }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  /** Public, framework-agnostic Guard API. */
2
+ export * from './core/model/types.js';
2
3
  export * from './core/guard.js';
3
4
  export * from './core/errors.js';
4
5
  export * from './core/discovery/discovery.js';
5
6
  export { runGuardVerification } from './core/verification/verify.js';
6
7
  export type { GuardVerificationCheck, GuardVerificationResult, } from './core/verification/verify.js';
7
8
  export * from './core/history/history.js';
9
+ export * from './core/history/runs.js';
8
10
  export * from './core/output/sarif.js';
9
11
  export * from './core/analysis/doctor.js';
10
12
  export * from './core/analysis/packs.js';
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /** Public, framework-agnostic Guard API. */
2
+ export * from './core/model/types.js';
2
3
  export * from './core/guard.js';
3
4
  export * from './core/errors.js';
4
5
  export * from './core/discovery/discovery.js';
5
6
  export { runGuardVerification } from './core/verification/verify.js';
6
7
  export * from './core/history/history.js';
8
+ export * from './core/history/runs.js';
7
9
  export * from './core/output/sarif.js';
8
10
  export * from './core/analysis/doctor.js';
9
11
  export * from './core/analysis/packs.js';
@@ -1,9 +1,24 @@
1
- import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, } from '../core/guard.js';
1
+ import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
2
+ import { guardErrorPayload } from '../core/errors.js';
3
+ import { DiscoveryStaleError } from '../core/discovery/discovery.js';
2
4
  function jsonResource(uri, value) {
3
5
  return {
4
6
  contents: [{ uri, text: JSON.stringify(value, null, 2), mimeType: 'application/json' }],
5
7
  };
6
8
  }
9
+ function resourceError(uri, error) {
10
+ const errorCode = error instanceof GuardStateBusyError
11
+ ? 'GUARD_STATE_BUSY'
12
+ : error instanceof GuardStateStaleError || error instanceof DiscoveryStaleError
13
+ ? 'GUARD_STATE_STALE'
14
+ : 'GUARD_CONFIG_INVALID';
15
+ return jsonResource(uri, guardErrorPayload(errorCode, error instanceof Error ? error.message : String(error), {
16
+ configured: errorCode !== 'GUARD_CONFIG_INVALID',
17
+ outcome: 'error',
18
+ recoverable: true,
19
+ hint: 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
20
+ }));
21
+ }
7
22
  export function registerGuardResources(server) {
8
23
  server.registerResource('codapult_guard_rules', 'codapult://guard/rules', {
9
24
  title: 'Architecture Guard Rules',
@@ -11,13 +26,18 @@ export function registerGuardResources(server) {
11
26
  mimeType: 'application/json',
12
27
  }, () => {
13
28
  const root = findGuardRoot();
14
- const config = loadGuardConfig(root);
15
- return jsonResource('codapult://guard/rules', {
16
- configured: config !== undefined,
17
- rules: config?.rules ?? [],
18
- contracts: config?.contracts ?? [],
19
- project: discoverProject(root),
20
- });
29
+ try {
30
+ const config = loadGuardConfig(root);
31
+ return jsonResource('codapult://guard/rules', {
32
+ configured: config !== undefined,
33
+ rules: config?.rules ?? [],
34
+ contracts: config?.contracts ?? [],
35
+ project: discoverProject(root),
36
+ });
37
+ }
38
+ catch (error) {
39
+ return resourceError('codapult://guard/rules', error);
40
+ }
21
41
  });
22
42
  server.registerResource('codapult_guard_context', 'codapult://guard/context', {
23
43
  title: 'Guard Project Context',
@@ -25,19 +45,24 @@ export function registerGuardResources(server) {
25
45
  mimeType: 'application/json',
26
46
  }, () => {
27
47
  const root = findGuardRoot();
28
- const project = discoverProject(root);
29
- const config = loadGuardConfig(root);
30
- return jsonResource('codapult://guard/context', {
31
- version: 1,
32
- root,
33
- configured: config !== undefined,
34
- project,
35
- architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
36
- conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
37
- rules: config?.rules ?? [],
38
- contracts: config?.contracts ?? [],
39
- agent: loadGuardAgentConfig(root),
40
- });
48
+ try {
49
+ const project = discoverProject(root);
50
+ const config = loadGuardConfig(root);
51
+ return jsonResource('codapult://guard/context', {
52
+ version: 1,
53
+ root,
54
+ configured: config !== undefined,
55
+ project,
56
+ architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
57
+ conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
58
+ rules: config?.rules ?? [],
59
+ contracts: config?.contracts ?? [],
60
+ agent: loadGuardAgentConfig(root),
61
+ });
62
+ }
63
+ catch (error) {
64
+ return resourceError('codapult://guard/context', error);
65
+ }
41
66
  });
42
67
  server.registerResource('codapult_guard_architecture', 'codapult://guard/architecture', {
43
68
  title: 'Guard Architecture Map',
@@ -45,13 +70,18 @@ export function registerGuardResources(server) {
45
70
  mimeType: 'application/json',
46
71
  }, () => {
47
72
  const root = findGuardRoot();
48
- const project = discoverProject(root);
49
- return jsonResource('codapult://guard/architecture', {
50
- version: 1,
51
- root,
52
- architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
53
- conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
54
- });
73
+ try {
74
+ const project = discoverProject(root);
75
+ return jsonResource('codapult://guard/architecture', {
76
+ version: 1,
77
+ root,
78
+ architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
79
+ conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
80
+ });
81
+ }
82
+ catch (error) {
83
+ return resourceError('codapult://guard/architecture', error);
84
+ }
55
85
  });
56
86
  server.registerResource('codapult_guard_proposals', 'codapult://guard/proposals', {
57
87
  title: 'Architecture Guard Proposals',
@@ -59,6 +89,11 @@ export function registerGuardResources(server) {
59
89
  mimeType: 'application/json',
60
90
  }, () => {
61
91
  const root = findGuardRoot();
62
- return jsonResource('codapult://guard/proposals', loadGuardProposals(root) ?? { version: 1, rules: [], contracts: [], questions: [] });
92
+ try {
93
+ return jsonResource('codapult://guard/proposals', loadGuardProposals(root) ?? { version: 1, rules: [], contracts: [], questions: [] });
94
+ }
95
+ catch (error) {
96
+ return resourceError('codapult://guard/proposals', error);
97
+ }
63
98
  });
64
99
  }