@codapult/guard 0.2.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.
@@ -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
  }
@@ -1,4 +1,5 @@
1
- import { buildArchitectureMemory, buildConventionsMemory, buildGeneratedGuardConfig, buildGuardProposals, buildGuardReviewPacket, discoverProject, discoverProjectWithMetrics, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadBaseline, loadGuardArtifact, loadGuardConfig, loadGuardAgentConfig, loadGuardProposals, getGuardProposalFreshness, loadProjectModel, writeGuardProposals, initializeGuard, GuardAlreadyInitializedError, recordGuardProposalDecision, writeGuardConfig, scanGuard, validateGuardContracts, classifyGuardOutcome, } from '../../core/guard.js';
1
+ import { buildArchitectureMemory, buildConventionsMemory, buildGeneratedGuardConfig, buildGuardProposals, buildGuardReviewPacket, discoverProject, discoverProjectWithMetrics, fingerprintProjectModel, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadBaseline, loadGuardArtifact, loadGuardConfig, loadGuardAgentConfig, loadGuardProposals, getPendingGuardProposals, getGuardProposalFreshness, loadProjectModel, writeGuardProposals, initializeGuard, GuardAlreadyInitializedError, GuardStateBusyError, GuardStateStaleError, applyGuardProposalDecision, validateGuardProposalApproval, writeProjectState, scanGuard, validateGuardPolicy, classifyGuardOutcome, } from '../../core/guard.js';
2
+ import { guardErrorPayload } from '../../core/errors.js';
2
3
  import { analyzeProjectImpact } from '../../core/analysis/impact.js';
3
4
  import { z } from 'zod';
4
5
  import { runGuardVerification } from '../../core/verification/verify.js';
@@ -12,7 +13,208 @@ function jsonToolResult(value, isError = false) {
12
13
  ...(isError ? { isError: true } : {}),
13
14
  };
14
15
  }
16
+ function notConfiguredToolResult() {
17
+ return jsonToolResult({
18
+ status: 'error',
19
+ outcome: 'not-configured',
20
+ configured: false,
21
+ errorCode: 'GUARD_NOT_CONFIGURED',
22
+ message: 'Guard is not initialized.',
23
+ recoverable: true,
24
+ }, true);
25
+ }
26
+ function loadConfigSafely(root) {
27
+ try {
28
+ return { config: loadGuardConfig(root) };
29
+ }
30
+ catch (error) {
31
+ return {
32
+ error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
33
+ configured: false,
34
+ outcome: 'error',
35
+ recoverable: true,
36
+ hint: 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
37
+ }), true),
38
+ };
39
+ }
40
+ }
41
+ function loadBaselineSafely(root) {
42
+ try {
43
+ return { baseline: loadBaseline(root) };
44
+ }
45
+ catch (error) {
46
+ return {
47
+ error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
48
+ configured: true,
49
+ outcome: 'error',
50
+ recoverable: true,
51
+ hint: 'Repair baseline.json, then run codapult-guard doctor.',
52
+ }), true),
53
+ };
54
+ }
55
+ }
56
+ function stateErrorResult(error) {
57
+ const isBusy = error instanceof GuardStateBusyError;
58
+ const isStale = error instanceof GuardStateStaleError;
59
+ if (!isBusy && !isStale)
60
+ return undefined;
61
+ return jsonToolResult(guardErrorPayload(isBusy ? 'GUARD_STATE_BUSY' : 'GUARD_STATE_STALE', String(error), {
62
+ configured: true,
63
+ outcome: 'error',
64
+ recoverable: true,
65
+ hint: 'Re-read Guard state and retry the operation.',
66
+ }), true);
67
+ }
68
+ function loadAgentSafely(root) {
69
+ try {
70
+ return { config: loadGuardAgentConfig(root) };
71
+ }
72
+ catch (error) {
73
+ return {
74
+ error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
75
+ configured: false,
76
+ outcome: 'error',
77
+ recoverable: true,
78
+ hint: 'Repair agent.json, then run codapult-guard doctor.',
79
+ }), true),
80
+ };
81
+ }
82
+ }
83
+ function loadProposalsSafely(root) {
84
+ try {
85
+ return { proposals: loadGuardProposals(root) };
86
+ }
87
+ catch (error) {
88
+ return {
89
+ error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
90
+ configured: false,
91
+ outcome: 'error',
92
+ recoverable: true,
93
+ hint: 'Repair proposals.json, then run codapult-guard doctor.',
94
+ }), true),
95
+ };
96
+ }
97
+ }
15
98
  export function registerGuardTools(server) {
99
+ server.registerTool('codapult_guard_analyze', {
100
+ title: 'Refresh Guard Model',
101
+ description: 'Refresh persisted project facts and snapshots without changing rules, contracts, or baseline.',
102
+ inputSchema: {
103
+ root: rootSchema,
104
+ confirm: z
105
+ .boolean()
106
+ .default(false)
107
+ .describe('Required before writing refreshed Guard state.'),
108
+ wait_ms: z.number().int().min(0).max(120_000).default(30_000),
109
+ no_wait: z.boolean().default(false),
110
+ },
111
+ }, ({ root: requestedRoot, confirm, wait_ms, no_wait }) => {
112
+ const root = getGuardRoot(requestedRoot);
113
+ if (!confirm)
114
+ return jsonToolResult({ status: 'needs-confirmation', action: 'analyze' });
115
+ const discovery = discoverProjectWithMetrics(root, { persistCache: true });
116
+ let revision;
117
+ try {
118
+ revision = writeProjectState(root, discovery.model, {
119
+ waitMs: wait_ms,
120
+ noWait: no_wait,
121
+ });
122
+ }
123
+ catch (error) {
124
+ return (stateErrorResult(error) ??
125
+ jsonToolResult({ status: 'fail', message: String(error) }, true));
126
+ }
127
+ return jsonToolResult({
128
+ status: 'ok',
129
+ action: 'analyze',
130
+ revision,
131
+ discovery: discovery.metrics,
132
+ });
133
+ });
134
+ server.registerTool('codapult_guard_next_action', {
135
+ title: 'Guard Next Action',
136
+ description: 'Return the next bounded action for an AI coding-agent loop without modifying project files.',
137
+ inputSchema: { root: rootSchema },
138
+ }, ({ root: requestedRoot }) => {
139
+ const root = getGuardRoot(requestedRoot);
140
+ const loaded = loadConfigSafely(root);
141
+ if (loaded.error)
142
+ return loaded.error;
143
+ const config = loaded.config;
144
+ if (!config) {
145
+ return jsonToolResult({
146
+ status: 'needs-setup',
147
+ action: 'init',
148
+ reason: 'Guard policy is not initialized.',
149
+ });
150
+ }
151
+ const project = discoverProject(root);
152
+ const persisted = loadProjectModel(root);
153
+ if (persisted &&
154
+ 'project' in persisted &&
155
+ fingerprintProjectModel(persisted) !== fingerprintProjectModel(project)) {
156
+ return jsonToolResult({
157
+ status: 'needs-refresh',
158
+ action: 'analyze',
159
+ reason: 'Persisted Guard project facts are stale after repository changes.',
160
+ canRetry: false,
161
+ });
162
+ }
163
+ const loadedProposals = loadProposalsSafely(root);
164
+ if ('error' in loadedProposals)
165
+ return loadedProposals.error;
166
+ const proposals = loadedProposals.proposals;
167
+ const proposedCount = getPendingGuardProposals(proposals).length;
168
+ if (proposals && getGuardProposalFreshness(project, proposals) === 'stale') {
169
+ return jsonToolResult({
170
+ status: 'needs-decision',
171
+ action: 'propose',
172
+ reason: 'Guard proposals are stale after project changes.',
173
+ canRetry: false,
174
+ });
175
+ }
176
+ if (proposedCount > 0) {
177
+ return jsonToolResult({
178
+ status: 'needs-decision',
179
+ action: 'policy-review',
180
+ reason: `${proposedCount} proposed policy item(s) await explicit approval or rejection.`,
181
+ canRetry: false,
182
+ });
183
+ }
184
+ const loadedBaseline = loadBaselineSafely(root);
185
+ if (loadedBaseline.error)
186
+ return loadedBaseline.error;
187
+ const report = scanGuard(root, config, {
188
+ changedOnly: true,
189
+ baseline: loadedBaseline.baseline,
190
+ includeArchitectureInsights: true,
191
+ });
192
+ const contractIssues = validateGuardPolicy(root, config);
193
+ if (report.findings.some((finding) => finding.severity === 'error')) {
194
+ return jsonToolResult({
195
+ status: 'needs-repair',
196
+ action: 'repair',
197
+ reason: 'Active architecture findings remain in changed files.',
198
+ findings: report.findings,
199
+ canRetry: true,
200
+ });
201
+ }
202
+ if (contractIssues.length > 0) {
203
+ return jsonToolResult({
204
+ status: 'needs-decision',
205
+ action: 'policy-repair',
206
+ reason: 'Guard contracts are stale or invalid.',
207
+ contractIssues,
208
+ canRetry: false,
209
+ });
210
+ }
211
+ return jsonToolResult({
212
+ status: 'ready-for-verification',
213
+ action: 'verify',
214
+ reason: 'No changed-file architecture regressions remain.',
215
+ canRetry: true,
216
+ });
217
+ });
16
218
  server.registerTool('codapult_guard_context', {
17
219
  title: 'Guard Context',
18
220
  description: 'Read the discovered project model, architecture insights, and local rules used by Codapult Guard. Works before init by discovering the project on demand.',
@@ -25,8 +227,14 @@ export function registerGuardTools(server) {
25
227
  },
26
228
  }, ({ root: requestedRoot, refresh }) => {
27
229
  const root = getGuardRoot(requestedRoot);
28
- const config = loadGuardConfig(root);
29
- const agentConfig = loadGuardAgentConfig(root);
230
+ const loaded = loadConfigSafely(root);
231
+ if (loaded.error)
232
+ return loaded.error;
233
+ const config = loaded.config;
234
+ const agent = loadAgentSafely(root);
235
+ if ('error' in agent)
236
+ return agent.error;
237
+ const agentConfig = agent.config;
30
238
  const discovery = refresh
31
239
  ? discoverProjectWithMetrics(root, { persistCache: true })
32
240
  : { model: loadProjectModel(root) ?? discoverProject(root), metrics: undefined };
@@ -73,9 +281,15 @@ export function registerGuardTools(server) {
73
281
  if (persist && !confirm)
74
282
  return jsonToolResult({ status: 'needs-confirmation', persist: true });
75
283
  const project = discoverProject(root);
76
- const config = loadGuardConfig(root) ?? buildGeneratedGuardConfig(project);
284
+ const loaded = loadConfigSafely(root);
285
+ if (loaded.error)
286
+ return loaded.error;
287
+ const config = loaded.config ?? buildGeneratedGuardConfig(project);
77
288
  const proposals = buildGuardProposals(project, config);
78
- const previous = loadGuardProposals(root);
289
+ const loadedPrevious = loadProposalsSafely(root);
290
+ if ('error' in loadedPrevious)
291
+ return loadedPrevious.error;
292
+ const previous = loadedPrevious.proposals;
79
293
  if (persist) {
80
294
  writeGuardProposals(root, {
81
295
  ...proposals,
@@ -108,16 +322,21 @@ export function registerGuardTools(server) {
108
322
  .boolean()
109
323
  .default(false)
110
324
  .describe('Replace existing Guard state; requires confirm.'),
325
+ wait_ms: z.number().int().min(0).max(120_000).default(30_000),
326
+ no_wait: z.boolean().default(false),
111
327
  },
112
- }, ({ root: requestedRoot, confirm, force }) => {
328
+ }, ({ root: requestedRoot, confirm, force, wait_ms, no_wait }) => {
113
329
  const root = getGuardRoot(requestedRoot);
114
330
  if (!confirm)
115
331
  return jsonToolResult({ status: 'needs-confirmation', root, force });
116
332
  try {
117
- const result = initializeGuard(root, { force });
333
+ const result = initializeGuard(root, { force, waitMs: wait_ms, noWait: no_wait });
118
334
  return jsonToolResult({ status: 'ok', root, report: result.report });
119
335
  }
120
336
  catch (error) {
337
+ const stateError = stateErrorResult(error);
338
+ if (stateError)
339
+ return stateError;
121
340
  const status = error instanceof GuardAlreadyInitializedError ? 'already-initialized' : 'fail';
122
341
  return jsonToolResult({ status, root, message: error instanceof Error ? error.message : String(error) }, true);
123
342
  }
@@ -133,7 +352,10 @@ export function registerGuardTools(server) {
133
352
  },
134
353
  }, ({ root: requestedRoot, ids, decision, confirm }) => {
135
354
  const root = getGuardRoot(requestedRoot);
136
- const proposals = loadGuardProposals(root);
355
+ const loadedProposals = loadProposalsSafely(root);
356
+ if ('error' in loadedProposals)
357
+ return loadedProposals.error;
358
+ const proposals = loadedProposals.proposals;
137
359
  if (!proposals)
138
360
  return jsonToolResult({ status: 'not-configured', message: 'No proposals found.' }, true);
139
361
  const project = discoverProject(root);
@@ -144,10 +366,28 @@ export function registerGuardTools(server) {
144
366
  const missing = ids.filter((id) => !selected.some((item) => item.id === id));
145
367
  if (missing.length > 0)
146
368
  return jsonToolResult({ status: 'invalid', missing }, true);
369
+ const loadedConfig = loadConfigSafely(root);
370
+ if (loadedConfig.error)
371
+ return loadedConfig.error;
372
+ const approval = loadedConfig.config?.approval;
373
+ if (decision === 'approved' && approval?.mode === 'protected' && !approval.allowMcpApproval) {
374
+ return jsonToolResult({
375
+ status: 'approval-required',
376
+ message: 'Protected Guard policy requires approval outside the MCP agent surface.',
377
+ source: 'mcp',
378
+ ids,
379
+ }, true);
380
+ }
381
+ if (decision === 'approved') {
382
+ const approvalError = validateGuardProposalApproval(approval, proposals);
383
+ if (approvalError) {
384
+ return jsonToolResult({ status: 'approval-required', message: approvalError, source: 'mcp', ids }, true);
385
+ }
386
+ }
147
387
  if (!confirm)
148
388
  return jsonToolResult({ status: 'needs-confirmation', decision, ids });
149
389
  if (decision === 'approved') {
150
- const config = loadGuardConfig(root) ?? { version: 1, rules: [], contracts: [] };
390
+ const config = loadedConfig.config ?? { version: 1, rules: [], contracts: [] };
151
391
  const nextRules = [...config.rules];
152
392
  const nextContracts = [...(config.contracts ?? [])];
153
393
  for (const item of selected) {
@@ -168,13 +408,19 @@ export function registerGuardTools(server) {
168
408
  nextRules.push(active);
169
409
  }
170
410
  }
171
- writeGuardConfig(root, { version: 1, rules: nextRules, contracts: nextContracts });
411
+ applyGuardProposalDecision(root, { version: 1, rules: nextRules, contracts: nextContracts }, selected.map((item) => ({
412
+ id: item.id,
413
+ type: 'statement' in item ? 'contract' : 'rule',
414
+ decision,
415
+ })), { source: 'mcp' });
416
+ }
417
+ else {
418
+ applyGuardProposalDecision(root, loadedConfig.config ?? { version: 1, rules: [], contracts: [] }, selected.map((item) => ({
419
+ id: item.id,
420
+ type: 'statement' in item ? 'contract' : 'rule',
421
+ decision,
422
+ })), { source: 'mcp' });
172
423
  }
173
- recordGuardProposalDecision(root, selected.map((item) => ({
174
- id: item.id,
175
- type: 'statement' in item ? 'contract' : 'rule',
176
- decision,
177
- })));
178
424
  return jsonToolResult({ status: 'ok', root, decision, ids });
179
425
  });
180
426
  server.registerTool('codapult_guard_verify', {
@@ -201,6 +447,9 @@ export function registerGuardTools(server) {
201
447
  },
202
448
  }, ({ root: requestedRoot, checks, changed_only, requirement, tools, strict, project_checks, iteration, }) => {
203
449
  const root = getGuardRoot(requestedRoot);
450
+ const agent = loadAgentSafely(root);
451
+ if ('error' in agent)
452
+ return agent.error;
204
453
  const result = runGuardVerification(root, {
205
454
  checks,
206
455
  changedOnly: changed_only,
@@ -209,7 +458,7 @@ export function registerGuardTools(server) {
209
458
  strict,
210
459
  projectChecks: project_checks,
211
460
  });
212
- const completionGate = loadGuardAgentConfig(root).completionGate;
461
+ const completionGate = agent.config.completionGate;
213
462
  return jsonToolResult({
214
463
  ...result,
215
464
  completionGate: {
@@ -225,20 +474,15 @@ export function registerGuardTools(server) {
225
474
  inputSchema: { root: rootSchema },
226
475
  }, ({ root: requestedRoot }) => {
227
476
  const root = getGuardRoot(requestedRoot);
228
- const config = loadGuardConfig(root);
477
+ const loaded = loadConfigSafely(root);
478
+ if (loaded.error)
479
+ return loaded.error;
480
+ const config = loaded.config;
229
481
  if (!config) {
230
- return {
231
- content: [
232
- {
233
- type: 'text',
234
- text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
235
- },
236
- ],
237
- isError: true,
238
- };
482
+ return notConfiguredToolResult();
239
483
  }
240
484
  const report = scanGuard(root, config, { includeArchitectureInsights: true });
241
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
485
+ const contractIssues = validateGuardPolicy(root, config);
242
486
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
243
487
  const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
244
488
  const status = errors > 0 || contractIssues.length > 0 ? 'fail' : warnings > 0 ? 'warning' : 'ok';
@@ -271,19 +515,19 @@ export function registerGuardTools(server) {
271
515
  },
272
516
  }, ({ root: requestedRoot, changed_only, max_diff_chars, requirement, base }) => {
273
517
  const root = getGuardRoot(requestedRoot);
274
- const config = loadGuardConfig(root);
518
+ const loaded = loadConfigSafely(root);
519
+ if (loaded.error)
520
+ return loaded.error;
521
+ const config = loaded.config;
275
522
  if (!config) {
276
- return {
277
- content: [
278
- {
279
- type: 'text',
280
- text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
281
- },
282
- ],
283
- isError: true,
284
- };
523
+ return notConfiguredToolResult();
285
524
  }
286
- const packet = buildGuardReviewPacket(root, config, changed_only ? loadBaseline(root) : new Set(), max_diff_chars, changed_only, requirement, base);
525
+ const loadedBaseline = changed_only
526
+ ? loadBaselineSafely(root)
527
+ : { baseline: new Set() };
528
+ if (loadedBaseline.error)
529
+ return loadedBaseline.error;
530
+ const packet = buildGuardReviewPacket(root, config, loadedBaseline.baseline ?? new Set(), max_diff_chars, changed_only, requirement, base);
287
531
  return jsonToolResult({ ...packet, status: packet.diffError ? 'fail' : 'needs-review' }, Boolean(packet.diffError));
288
532
  });
289
533
  server.registerTool('codapult_guard_check', {
@@ -292,24 +536,22 @@ export function registerGuardTools(server) {
292
536
  inputSchema: { root: rootSchema, changed_only: z.boolean().optional() },
293
537
  }, ({ root: requestedRoot, changed_only }) => {
294
538
  const root = getGuardRoot(requestedRoot);
295
- const config = loadGuardConfig(root);
539
+ const loaded = loadConfigSafely(root);
540
+ if (loaded.error)
541
+ return loaded.error;
542
+ const config = loaded.config;
296
543
  if (!config) {
297
- return {
298
- content: [
299
- {
300
- type: 'text',
301
- text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
302
- },
303
- ],
304
- isError: true,
305
- };
544
+ return notConfiguredToolResult();
306
545
  }
546
+ const loadedBaseline = loadBaselineSafely(root);
547
+ if (loadedBaseline.error)
548
+ return loadedBaseline.error;
307
549
  const report = scanGuard(root, config, {
308
550
  changedOnly: changed_only,
309
- baseline: loadBaseline(root),
551
+ baseline: loadedBaseline.baseline,
310
552
  includeArchitectureInsights: true,
311
553
  });
312
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
554
+ const contractIssues = validateGuardPolicy(root, config);
313
555
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
314
556
  return jsonToolResult({
315
557
  status: errors > 0 || contractIssues.length > 0 ? 'fail' : 'ok',
@@ -331,7 +573,10 @@ export function registerGuardTools(server) {
331
573
  }, ({ root: requestedRoot, files }) => {
332
574
  const root = getGuardRoot(requestedRoot);
333
575
  const model = discoverProject(root);
334
- const impact = analyzeProjectImpact(model, files, loadGuardConfig(root)?.contracts ?? []);
576
+ const loaded = loadConfigSafely(root);
577
+ if (loaded.error)
578
+ return loaded.error;
579
+ const impact = analyzeProjectImpact(model, files, loaded.config?.contracts ?? []);
335
580
  return jsonToolResult({
336
581
  status: 'ok',
337
582
  ...impact,
@@ -346,7 +591,10 @@ export function registerGuardTools(server) {
346
591
  },
347
592
  }, ({ root: requestedRoot, id }) => {
348
593
  const root = getGuardRoot(requestedRoot);
349
- const config = loadGuardConfig(root);
594
+ const loaded = loadConfigSafely(root);
595
+ if (loaded.error)
596
+ return loaded.error;
597
+ const config = loaded.config;
350
598
  const item = [...(config?.rules ?? []), ...(config?.contracts ?? [])].find((candidate) => candidate.id === id);
351
599
  if (!item)
352
600
  return jsonToolResult({ status: 'not-found', id }, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codapult/guard",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",
@@ -60,7 +60,12 @@
60
60
  "test:guard:fixtures": "pnpm build && node scripts/guard-fixture-smoke.mjs",
61
61
  "test:guard:fixtures:regression": "pnpm build && node scripts/guard-fixture-regression.mjs",
62
62
  "test:guard:fixtures:native": "pnpm build && node scripts/guard-fixture-native.mjs",
63
+ "test:guard:fixtures:adversarial": "pnpm build && node scripts/guard-fixture-adversarial.mjs",
64
+ "test:guard:fixtures:all": "pnpm build && node scripts/guard-fixture-suite.mjs",
65
+ "test:guard:fixtures:full": "pnpm build && node scripts/guard-fixture-suite.mjs --all",
66
+ "test:guard:concurrent": "pnpm build && node scripts/guard-concurrent-state.mjs",
63
67
  "test:guard:pr": "pnpm build && node scripts/guard-pr-review.mjs",
68
+ "test:guard:benchmark": "pnpm build && node scripts/guard-benchmark.mjs",
64
69
  "test:guard:golden": "pnpm build && node scripts/guard-golden-demo.mjs",
65
70
  "test:watch": "vitest",
66
71
  "typecheck": "tsc --noEmit",
@@ -99,19 +104,19 @@
99
104
  },
100
105
  "devDependencies": {
101
106
  "@js-toolkit/config-utils": "^1.0.1",
102
- "@js-toolkit/eslint-config": "^1.3.0",
107
+ "@js-toolkit/eslint-config": "^1.3.2",
103
108
  "@js-toolkit/prettier-config": "^1.0.1",
104
109
  "@js-toolkit/tsconfig": "^1.0.1",
105
110
  "@release-it/conventional-changelog": "^12.0.2",
106
- "@types/node": "^26.6.1",
111
+ "@types/node": "^26.6.2",
107
112
  "@typescript/native": "npm:typescript@^7.0.2",
108
113
  "@vitest/coverage-v8": "^5.0.1",
109
- "eslint": "^10.10.0",
114
+ "eslint": "^10.11.0",
110
115
  "eslint-config-prettier": "^10.1.8",
111
116
  "eslint-plugin-prettier": "^5.5.6",
112
- "prettier": "^3.9.7",
113
- "release-it": "^21.0.3",
114
- "typescript-eslint": "^8.70.0",
117
+ "prettier": "^3.9.8",
118
+ "release-it": "^21.1.0",
119
+ "typescript-eslint": "^8.70.1",
115
120
  "vitest": "^5.0.1"
116
121
  }
117
122
  }