@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.
@@ -1,4 +1,4 @@
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, recordGuardProposalDecision, writeGuardConfig, writeGuardMemory, writeProjectModel, writeProjectSnapshot, 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
2
  import { guardErrorPayload } from '../../core/errors.js';
3
3
  import { analyzeProjectImpact } from '../../core/analysis/impact.js';
4
4
  import { z } from 'zod';
@@ -38,6 +38,63 @@ function loadConfigSafely(root) {
38
38
  };
39
39
  }
40
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
+ }
41
98
  export function registerGuardTools(server) {
42
99
  server.registerTool('codapult_guard_analyze', {
43
100
  title: 'Refresh Guard Model',
@@ -48,15 +105,25 @@ export function registerGuardTools(server) {
48
105
  .boolean()
49
106
  .default(false)
50
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),
51
110
  },
52
- }, ({ root: requestedRoot, confirm }) => {
111
+ }, ({ root: requestedRoot, confirm, wait_ms, no_wait }) => {
53
112
  const root = getGuardRoot(requestedRoot);
54
113
  if (!confirm)
55
114
  return jsonToolResult({ status: 'needs-confirmation', action: 'analyze' });
56
115
  const discovery = discoverProjectWithMetrics(root, { persistCache: true });
57
- writeProjectModel(root, discovery.model);
58
- writeGuardMemory(root, discovery.model);
59
- const revision = writeProjectSnapshot(root, discovery.model);
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
+ }
60
127
  return jsonToolResult({
61
128
  status: 'ok',
62
129
  action: 'analyze',
@@ -93,7 +160,10 @@ export function registerGuardTools(server) {
93
160
  canRetry: false,
94
161
  });
95
162
  }
96
- const proposals = loadGuardProposals(root);
163
+ const loadedProposals = loadProposalsSafely(root);
164
+ if ('error' in loadedProposals)
165
+ return loadedProposals.error;
166
+ const proposals = loadedProposals.proposals;
97
167
  const proposedCount = getPendingGuardProposals(proposals).length;
98
168
  if (proposals && getGuardProposalFreshness(project, proposals) === 'stale') {
99
169
  return jsonToolResult({
@@ -111,12 +181,15 @@ export function registerGuardTools(server) {
111
181
  canRetry: false,
112
182
  });
113
183
  }
184
+ const loadedBaseline = loadBaselineSafely(root);
185
+ if (loadedBaseline.error)
186
+ return loadedBaseline.error;
114
187
  const report = scanGuard(root, config, {
115
188
  changedOnly: true,
116
- baseline: loadBaseline(root),
189
+ baseline: loadedBaseline.baseline,
117
190
  includeArchitectureInsights: true,
118
191
  });
119
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
192
+ const contractIssues = validateGuardPolicy(root, config);
120
193
  if (report.findings.some((finding) => finding.severity === 'error')) {
121
194
  return jsonToolResult({
122
195
  status: 'needs-repair',
@@ -158,7 +231,10 @@ export function registerGuardTools(server) {
158
231
  if (loaded.error)
159
232
  return loaded.error;
160
233
  const config = loaded.config;
161
- const agentConfig = loadGuardAgentConfig(root);
234
+ const agent = loadAgentSafely(root);
235
+ if ('error' in agent)
236
+ return agent.error;
237
+ const agentConfig = agent.config;
162
238
  const discovery = refresh
163
239
  ? discoverProjectWithMetrics(root, { persistCache: true })
164
240
  : { model: loadProjectModel(root) ?? discoverProject(root), metrics: undefined };
@@ -210,7 +286,10 @@ export function registerGuardTools(server) {
210
286
  return loaded.error;
211
287
  const config = loaded.config ?? buildGeneratedGuardConfig(project);
212
288
  const proposals = buildGuardProposals(project, config);
213
- const previous = loadGuardProposals(root);
289
+ const loadedPrevious = loadProposalsSafely(root);
290
+ if ('error' in loadedPrevious)
291
+ return loadedPrevious.error;
292
+ const previous = loadedPrevious.proposals;
214
293
  if (persist) {
215
294
  writeGuardProposals(root, {
216
295
  ...proposals,
@@ -243,16 +322,21 @@ export function registerGuardTools(server) {
243
322
  .boolean()
244
323
  .default(false)
245
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),
246
327
  },
247
- }, ({ root: requestedRoot, confirm, force }) => {
328
+ }, ({ root: requestedRoot, confirm, force, wait_ms, no_wait }) => {
248
329
  const root = getGuardRoot(requestedRoot);
249
330
  if (!confirm)
250
331
  return jsonToolResult({ status: 'needs-confirmation', root, force });
251
332
  try {
252
- const result = initializeGuard(root, { force });
333
+ const result = initializeGuard(root, { force, waitMs: wait_ms, noWait: no_wait });
253
334
  return jsonToolResult({ status: 'ok', root, report: result.report });
254
335
  }
255
336
  catch (error) {
337
+ const stateError = stateErrorResult(error);
338
+ if (stateError)
339
+ return stateError;
256
340
  const status = error instanceof GuardAlreadyInitializedError ? 'already-initialized' : 'fail';
257
341
  return jsonToolResult({ status, root, message: error instanceof Error ? error.message : String(error) }, true);
258
342
  }
@@ -268,7 +352,10 @@ export function registerGuardTools(server) {
268
352
  },
269
353
  }, ({ root: requestedRoot, ids, decision, confirm }) => {
270
354
  const root = getGuardRoot(requestedRoot);
271
- const proposals = loadGuardProposals(root);
355
+ const loadedProposals = loadProposalsSafely(root);
356
+ if ('error' in loadedProposals)
357
+ return loadedProposals.error;
358
+ const proposals = loadedProposals.proposals;
272
359
  if (!proposals)
273
360
  return jsonToolResult({ status: 'not-configured', message: 'No proposals found.' }, true);
274
361
  const project = discoverProject(root);
@@ -279,13 +366,28 @@ export function registerGuardTools(server) {
279
366
  const missing = ids.filter((id) => !selected.some((item) => item.id === id));
280
367
  if (missing.length > 0)
281
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
+ }
282
387
  if (!confirm)
283
388
  return jsonToolResult({ status: 'needs-confirmation', decision, ids });
284
389
  if (decision === 'approved') {
285
- const loaded = loadConfigSafely(root);
286
- if (loaded.error)
287
- return loaded.error;
288
- const config = loaded.config ?? { version: 1, rules: [], contracts: [] };
390
+ const config = loadedConfig.config ?? { version: 1, rules: [], contracts: [] };
289
391
  const nextRules = [...config.rules];
290
392
  const nextContracts = [...(config.contracts ?? [])];
291
393
  for (const item of selected) {
@@ -306,13 +408,19 @@ export function registerGuardTools(server) {
306
408
  nextRules.push(active);
307
409
  }
308
410
  }
309
- 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' });
310
423
  }
311
- recordGuardProposalDecision(root, selected.map((item) => ({
312
- id: item.id,
313
- type: 'statement' in item ? 'contract' : 'rule',
314
- decision,
315
- })));
316
424
  return jsonToolResult({ status: 'ok', root, decision, ids });
317
425
  });
318
426
  server.registerTool('codapult_guard_verify', {
@@ -339,6 +447,9 @@ export function registerGuardTools(server) {
339
447
  },
340
448
  }, ({ root: requestedRoot, checks, changed_only, requirement, tools, strict, project_checks, iteration, }) => {
341
449
  const root = getGuardRoot(requestedRoot);
450
+ const agent = loadAgentSafely(root);
451
+ if ('error' in agent)
452
+ return agent.error;
342
453
  const result = runGuardVerification(root, {
343
454
  checks,
344
455
  changedOnly: changed_only,
@@ -347,7 +458,7 @@ export function registerGuardTools(server) {
347
458
  strict,
348
459
  projectChecks: project_checks,
349
460
  });
350
- const completionGate = loadGuardAgentConfig(root).completionGate;
461
+ const completionGate = agent.config.completionGate;
351
462
  return jsonToolResult({
352
463
  ...result,
353
464
  completionGate: {
@@ -371,7 +482,7 @@ export function registerGuardTools(server) {
371
482
  return notConfiguredToolResult();
372
483
  }
373
484
  const report = scanGuard(root, config, { includeArchitectureInsights: true });
374
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
485
+ const contractIssues = validateGuardPolicy(root, config);
375
486
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
376
487
  const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
377
488
  const status = errors > 0 || contractIssues.length > 0 ? 'fail' : warnings > 0 ? 'warning' : 'ok';
@@ -411,7 +522,12 @@ export function registerGuardTools(server) {
411
522
  if (!config) {
412
523
  return notConfiguredToolResult();
413
524
  }
414
- 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);
415
531
  return jsonToolResult({ ...packet, status: packet.diffError ? 'fail' : 'needs-review' }, Boolean(packet.diffError));
416
532
  });
417
533
  server.registerTool('codapult_guard_check', {
@@ -427,12 +543,15 @@ export function registerGuardTools(server) {
427
543
  if (!config) {
428
544
  return notConfiguredToolResult();
429
545
  }
546
+ const loadedBaseline = loadBaselineSafely(root);
547
+ if (loadedBaseline.error)
548
+ return loadedBaseline.error;
430
549
  const report = scanGuard(root, config, {
431
550
  changedOnly: changed_only,
432
- baseline: loadBaseline(root),
551
+ baseline: loadedBaseline.baseline,
433
552
  includeArchitectureInsights: true,
434
553
  });
435
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
554
+ const contractIssues = validateGuardPolicy(root, config);
436
555
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
437
556
  return jsonToolResult({
438
557
  status: errors > 0 || contractIssues.length > 0 ? 'fail' : 'ok',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codapult/guard",
3
- "version": "0.3.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",
@@ -104,19 +104,19 @@
104
104
  },
105
105
  "devDependencies": {
106
106
  "@js-toolkit/config-utils": "^1.0.1",
107
- "@js-toolkit/eslint-config": "^1.3.0",
107
+ "@js-toolkit/eslint-config": "^1.3.2",
108
108
  "@js-toolkit/prettier-config": "^1.0.1",
109
109
  "@js-toolkit/tsconfig": "^1.0.1",
110
110
  "@release-it/conventional-changelog": "^12.0.2",
111
- "@types/node": "^26.6.1",
111
+ "@types/node": "^26.6.2",
112
112
  "@typescript/native": "npm:typescript@^7.0.2",
113
113
  "@vitest/coverage-v8": "^5.0.1",
114
- "eslint": "^10.10.0",
114
+ "eslint": "^10.11.0",
115
115
  "eslint-config-prettier": "^10.1.8",
116
116
  "eslint-plugin-prettier": "^5.5.6",
117
- "prettier": "^3.9.7",
118
- "release-it": "^21.0.3",
119
- "typescript-eslint": "^8.70.0",
117
+ "prettier": "^3.9.8",
118
+ "release-it": "^21.1.0",
119
+ "typescript-eslint": "^8.70.1",
120
120
  "vitest": "^5.0.1"
121
121
  }
122
122
  }