@codapult/guard 0.2.0 → 0.3.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,8 +1,8 @@
1
1
  import { runProjectAdapters, runProjectChecks, runWorkspaceProjectChecks, inspectProjectRuntime, } from '../../adapters/project-checks.js';
2
2
  import { loadBaseline, loadGuardAgentConfig, loadGuardConfig, scanGuard, classifyGuardOutcome, validateGuardContracts, } from '../guard.js';
3
+ import { analyzeProjectImpact } from '../analysis/impact.js';
3
4
  import { discoverProject } from '../discovery/discovery.js';
4
5
  export function runGuardVerification(root, options = {}) {
5
- const config = loadGuardConfig(root);
6
6
  const requirement = {
7
7
  status: 'delegated-to-review',
8
8
  provided: Boolean(options.requirement?.trim()),
@@ -11,10 +11,31 @@ export function runGuardVerification(root, options = {}) {
11
11
  : 'Requirement satisfaction is evaluated by guard review using the task and diff.',
12
12
  };
13
13
  const runtime = inspectProjectRuntime(root);
14
+ let config;
15
+ try {
16
+ config = loadGuardConfig(root);
17
+ }
18
+ catch (error) {
19
+ return {
20
+ status: 'fail',
21
+ outcome: 'fail',
22
+ errorCode: 'GUARD_CONFIG_INVALID',
23
+ recoverable: true,
24
+ checks: {},
25
+ workspaceChecks: {},
26
+ adapters: {},
27
+ runtime,
28
+ configError: error instanceof Error ? error.message : String(error),
29
+ contractIssues: [],
30
+ requirement,
31
+ };
32
+ }
14
33
  if (!config)
15
34
  return {
16
35
  status: 'not-configured',
17
36
  outcome: 'not-configured',
37
+ errorCode: 'GUARD_NOT_CONFIGURED',
38
+ recoverable: true,
18
39
  checks: {},
19
40
  workspaceChecks: {},
20
41
  adapters: {},
@@ -27,6 +48,9 @@ export function runGuardVerification(root, options = {}) {
27
48
  const checks = projectChecks ? (options.checks ?? agentConfig.completionGate.checks) : [];
28
49
  const results = runProjectChecks(root, checks, { timeout: options.timeout });
29
50
  const workspaceModel = discoverProject(root);
51
+ const impact = analyzeProjectImpact(workspaceModel, options.changedOnly === false
52
+ ? workspaceModel.modules.map((module) => module.path)
53
+ : workspaceModel.git.changedFiles, config.contracts ?? []);
30
54
  const workspaceChecks = runWorkspaceProjectChecks(root, workspaceModel.project.workspacePackages ?? [], checks, {
31
55
  timeout: options.timeout,
32
56
  rootResults: results,
@@ -71,6 +95,7 @@ export function runGuardVerification(root, options = {}) {
71
95
  adapters,
72
96
  runtime,
73
97
  architecture,
98
+ impact,
74
99
  contractIssues,
75
100
  requirement,
76
101
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Public, framework-agnostic Guard API. */
2
2
  export * from './core/guard.js';
3
+ export * from './core/errors.js';
3
4
  export * from './core/discovery/discovery.js';
4
5
  export { runGuardVerification } from './core/verification/verify.js';
5
6
  export type { GuardVerificationCheck, GuardVerificationResult, } from './core/verification/verify.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Public, framework-agnostic Guard API. */
2
2
  export * from './core/guard.js';
3
+ export * from './core/errors.js';
3
4
  export * from './core/discovery/discovery.js';
4
5
  export { runGuardVerification } from './core/verification/verify.js';
5
6
  export * from './core/history/history.js';
@@ -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, recordGuardProposalDecision, writeGuardConfig, writeGuardMemory, writeProjectModel, writeProjectSnapshot, scanGuard, validateGuardContracts, 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,135 @@ 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
+ }
15
41
  export function registerGuardTools(server) {
42
+ server.registerTool('codapult_guard_analyze', {
43
+ title: 'Refresh Guard Model',
44
+ description: 'Refresh persisted project facts and snapshots without changing rules, contracts, or baseline.',
45
+ inputSchema: {
46
+ root: rootSchema,
47
+ confirm: z
48
+ .boolean()
49
+ .default(false)
50
+ .describe('Required before writing refreshed Guard state.'),
51
+ },
52
+ }, ({ root: requestedRoot, confirm }) => {
53
+ const root = getGuardRoot(requestedRoot);
54
+ if (!confirm)
55
+ return jsonToolResult({ status: 'needs-confirmation', action: 'analyze' });
56
+ const discovery = discoverProjectWithMetrics(root, { persistCache: true });
57
+ writeProjectModel(root, discovery.model);
58
+ writeGuardMemory(root, discovery.model);
59
+ const revision = writeProjectSnapshot(root, discovery.model);
60
+ return jsonToolResult({
61
+ status: 'ok',
62
+ action: 'analyze',
63
+ revision,
64
+ discovery: discovery.metrics,
65
+ });
66
+ });
67
+ server.registerTool('codapult_guard_next_action', {
68
+ title: 'Guard Next Action',
69
+ description: 'Return the next bounded action for an AI coding-agent loop without modifying project files.',
70
+ inputSchema: { root: rootSchema },
71
+ }, ({ root: requestedRoot }) => {
72
+ const root = getGuardRoot(requestedRoot);
73
+ const loaded = loadConfigSafely(root);
74
+ if (loaded.error)
75
+ return loaded.error;
76
+ const config = loaded.config;
77
+ if (!config) {
78
+ return jsonToolResult({
79
+ status: 'needs-setup',
80
+ action: 'init',
81
+ reason: 'Guard policy is not initialized.',
82
+ });
83
+ }
84
+ const project = discoverProject(root);
85
+ const persisted = loadProjectModel(root);
86
+ if (persisted &&
87
+ 'project' in persisted &&
88
+ fingerprintProjectModel(persisted) !== fingerprintProjectModel(project)) {
89
+ return jsonToolResult({
90
+ status: 'needs-refresh',
91
+ action: 'analyze',
92
+ reason: 'Persisted Guard project facts are stale after repository changes.',
93
+ canRetry: false,
94
+ });
95
+ }
96
+ const proposals = loadGuardProposals(root);
97
+ const proposedCount = getPendingGuardProposals(proposals).length;
98
+ if (proposals && getGuardProposalFreshness(project, proposals) === 'stale') {
99
+ return jsonToolResult({
100
+ status: 'needs-decision',
101
+ action: 'propose',
102
+ reason: 'Guard proposals are stale after project changes.',
103
+ canRetry: false,
104
+ });
105
+ }
106
+ if (proposedCount > 0) {
107
+ return jsonToolResult({
108
+ status: 'needs-decision',
109
+ action: 'policy-review',
110
+ reason: `${proposedCount} proposed policy item(s) await explicit approval or rejection.`,
111
+ canRetry: false,
112
+ });
113
+ }
114
+ const report = scanGuard(root, config, {
115
+ changedOnly: true,
116
+ baseline: loadBaseline(root),
117
+ includeArchitectureInsights: true,
118
+ });
119
+ const contractIssues = validateGuardContracts(root, config.contracts ?? []);
120
+ if (report.findings.some((finding) => finding.severity === 'error')) {
121
+ return jsonToolResult({
122
+ status: 'needs-repair',
123
+ action: 'repair',
124
+ reason: 'Active architecture findings remain in changed files.',
125
+ findings: report.findings,
126
+ canRetry: true,
127
+ });
128
+ }
129
+ if (contractIssues.length > 0) {
130
+ return jsonToolResult({
131
+ status: 'needs-decision',
132
+ action: 'policy-repair',
133
+ reason: 'Guard contracts are stale or invalid.',
134
+ contractIssues,
135
+ canRetry: false,
136
+ });
137
+ }
138
+ return jsonToolResult({
139
+ status: 'ready-for-verification',
140
+ action: 'verify',
141
+ reason: 'No changed-file architecture regressions remain.',
142
+ canRetry: true,
143
+ });
144
+ });
16
145
  server.registerTool('codapult_guard_context', {
17
146
  title: 'Guard Context',
18
147
  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,7 +154,10 @@ export function registerGuardTools(server) {
25
154
  },
26
155
  }, ({ root: requestedRoot, refresh }) => {
27
156
  const root = getGuardRoot(requestedRoot);
28
- const config = loadGuardConfig(root);
157
+ const loaded = loadConfigSafely(root);
158
+ if (loaded.error)
159
+ return loaded.error;
160
+ const config = loaded.config;
29
161
  const agentConfig = loadGuardAgentConfig(root);
30
162
  const discovery = refresh
31
163
  ? discoverProjectWithMetrics(root, { persistCache: true })
@@ -73,7 +205,10 @@ export function registerGuardTools(server) {
73
205
  if (persist && !confirm)
74
206
  return jsonToolResult({ status: 'needs-confirmation', persist: true });
75
207
  const project = discoverProject(root);
76
- const config = loadGuardConfig(root) ?? buildGeneratedGuardConfig(project);
208
+ const loaded = loadConfigSafely(root);
209
+ if (loaded.error)
210
+ return loaded.error;
211
+ const config = loaded.config ?? buildGeneratedGuardConfig(project);
77
212
  const proposals = buildGuardProposals(project, config);
78
213
  const previous = loadGuardProposals(root);
79
214
  if (persist) {
@@ -147,7 +282,10 @@ export function registerGuardTools(server) {
147
282
  if (!confirm)
148
283
  return jsonToolResult({ status: 'needs-confirmation', decision, ids });
149
284
  if (decision === 'approved') {
150
- const config = loadGuardConfig(root) ?? { version: 1, rules: [], contracts: [] };
285
+ const loaded = loadConfigSafely(root);
286
+ if (loaded.error)
287
+ return loaded.error;
288
+ const config = loaded.config ?? { version: 1, rules: [], contracts: [] };
151
289
  const nextRules = [...config.rules];
152
290
  const nextContracts = [...(config.contracts ?? [])];
153
291
  for (const item of selected) {
@@ -225,17 +363,12 @@ export function registerGuardTools(server) {
225
363
  inputSchema: { root: rootSchema },
226
364
  }, ({ root: requestedRoot }) => {
227
365
  const root = getGuardRoot(requestedRoot);
228
- const config = loadGuardConfig(root);
366
+ const loaded = loadConfigSafely(root);
367
+ if (loaded.error)
368
+ return loaded.error;
369
+ const config = loaded.config;
229
370
  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
- };
371
+ return notConfiguredToolResult();
239
372
  }
240
373
  const report = scanGuard(root, config, { includeArchitectureInsights: true });
241
374
  const contractIssues = validateGuardContracts(root, config.contracts ?? []);
@@ -271,17 +404,12 @@ export function registerGuardTools(server) {
271
404
  },
272
405
  }, ({ root: requestedRoot, changed_only, max_diff_chars, requirement, base }) => {
273
406
  const root = getGuardRoot(requestedRoot);
274
- const config = loadGuardConfig(root);
407
+ const loaded = loadConfigSafely(root);
408
+ if (loaded.error)
409
+ return loaded.error;
410
+ const config = loaded.config;
275
411
  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
- };
412
+ return notConfiguredToolResult();
285
413
  }
286
414
  const packet = buildGuardReviewPacket(root, config, changed_only ? loadBaseline(root) : new Set(), max_diff_chars, changed_only, requirement, base);
287
415
  return jsonToolResult({ ...packet, status: packet.diffError ? 'fail' : 'needs-review' }, Boolean(packet.diffError));
@@ -292,17 +420,12 @@ export function registerGuardTools(server) {
292
420
  inputSchema: { root: rootSchema, changed_only: z.boolean().optional() },
293
421
  }, ({ root: requestedRoot, changed_only }) => {
294
422
  const root = getGuardRoot(requestedRoot);
295
- const config = loadGuardConfig(root);
423
+ const loaded = loadConfigSafely(root);
424
+ if (loaded.error)
425
+ return loaded.error;
426
+ const config = loaded.config;
296
427
  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
- };
428
+ return notConfiguredToolResult();
306
429
  }
307
430
  const report = scanGuard(root, config, {
308
431
  changedOnly: changed_only,
@@ -331,7 +454,10 @@ export function registerGuardTools(server) {
331
454
  }, ({ root: requestedRoot, files }) => {
332
455
  const root = getGuardRoot(requestedRoot);
333
456
  const model = discoverProject(root);
334
- const impact = analyzeProjectImpact(model, files, loadGuardConfig(root)?.contracts ?? []);
457
+ const loaded = loadConfigSafely(root);
458
+ if (loaded.error)
459
+ return loaded.error;
460
+ const impact = analyzeProjectImpact(model, files, loaded.config?.contracts ?? []);
335
461
  return jsonToolResult({
336
462
  status: 'ok',
337
463
  ...impact,
@@ -346,7 +472,10 @@ export function registerGuardTools(server) {
346
472
  },
347
473
  }, ({ root: requestedRoot, id }) => {
348
474
  const root = getGuardRoot(requestedRoot);
349
- const config = loadGuardConfig(root);
475
+ const loaded = loadConfigSafely(root);
476
+ if (loaded.error)
477
+ return loaded.error;
478
+ const config = loaded.config;
350
479
  const item = [...(config?.rules ?? []), ...(config?.contracts ?? [])].find((candidate) => candidate.id === id);
351
480
  if (!item)
352
481
  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.3.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",