@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.
- package/README.md +18 -0
- package/dist/adapters/command.js +23 -2
- package/dist/cli/commands/guard.d.ts +7 -1
- package/dist/cli/commands/guard.js +125 -15
- package/dist/cli/index.js +27 -4
- package/dist/core/analysis/doctor.js +19 -5
- package/dist/core/discovery/discovery.d.ts +16 -6
- package/dist/core/discovery/discovery.js +150 -17
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/guard.d.ts +18 -5
- package/dist/core/guard.js +226 -57
- package/dist/core/policy/schemas.d.ts +12 -0
- package/dist/core/policy/schemas.js +21 -17
- package/dist/core/verification/verify.d.ts +5 -0
- package/dist/core/verification/verify.js +26 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/tools/guard.js +165 -36
- package/package.json +6 -1
|
@@ -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';
|
package/dist/mcp/tools/guard.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|