@codapult/guard 0.1.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 +95 -15
- package/dist/adapters/command.js +23 -2
- package/dist/cli/commands/guard.d.ts +10 -1
- package/dist/cli/commands/guard.js +148 -14
- package/dist/cli/index.js +28 -4
- package/dist/core/analysis/doctor.js +19 -5
- package/dist/core/analysis/impact.d.ts +22 -0
- package/dist/core/analysis/impact.js +73 -0
- package/dist/core/discovery/discovery.d.ts +23 -6
- package/dist/core/discovery/discovery.js +166 -26
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/guard.d.ts +26 -5
- package/dist/core/guard.js +262 -43
- package/dist/core/history/history.d.ts +6 -0
- package/dist/core/history/history.js +26 -2
- 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 +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/tools/guard.js +167 -47
- package/package.json +17 -12
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/codapult/codapult-guard/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/codapult/codapult-guard/actions/workflows/guard-fixtures.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@codapult/guard)
|
|
5
6
|
[](LICENSE)
|
|
6
7
|
[](package.json)
|
|
7
8
|
[](https://www.typescriptlang.org/)
|
|
@@ -11,8 +12,43 @@
|
|
|
11
12
|
`@codapult/guard` is a local-first, model-agnostic architecture guard for JavaScript and
|
|
12
13
|
TypeScript projects.
|
|
13
14
|
|
|
14
|
-
It learns what already exists, records the project’s architectural memory, and
|
|
15
|
-
changes from introducing regressions.
|
|
15
|
+
It learns what already exists, records the project’s architectural memory, and can protect future
|
|
16
|
+
changes from introducing regressions covered by the project’s approved policy.
|
|
17
|
+
|
|
18
|
+
### See the value in one change
|
|
19
|
+
|
|
20
|
+
Suppose a SaaS project has an established boundary:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
Client → Server Action → Service → Repository → Database
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
An AI agent can still produce code that is valid TypeScript but crosses that boundary:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Client → Database
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
After the project has explicitly approved a matching import rule or deterministic contract, Guard
|
|
33
|
+
checks the changed files and reports matching violations deterministically:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
Codapult Guard (changed files)
|
|
37
|
+
Scanned 1 source file(s); suppressed 12 baseline finding(s).
|
|
38
|
+
|
|
39
|
+
src/app/settings/page.tsx:1 [client-no-persistence-import]
|
|
40
|
+
Client modules should not import persistence-layer modules.
|
|
41
|
+
import: @/lib/db/client
|
|
42
|
+
|
|
43
|
+
✗ 1 issue(s) found
|
|
44
|
+
Exit code: 1
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The file path, rule ID, message, and counts depend on the project. This is the actual CLI output
|
|
48
|
+
shape: Guard does not infer that every `Client → Database` edge is forbidden, and it does not
|
|
49
|
+
activate proposals silently. The team approves the relevant policy first; then existing findings
|
|
50
|
+
can be baselined while newly matching violations fail the gate and give the agent bounded evidence
|
|
51
|
+
to repair.
|
|
16
52
|
|
|
17
53
|
> **Guard does not tell every project to use the same architecture.**
|
|
18
54
|
> It discovers the architecture that is already there, then lets the team decide what becomes policy.
|
|
@@ -132,6 +168,10 @@ Guard does not assume a fixed `UI → actions → services → repositories →
|
|
|
132
168
|
It can discover that shape when the project exhibits it, but observed patterns become enforceable
|
|
133
169
|
only after explicit approval.
|
|
134
170
|
|
|
171
|
+
Policy paths are validated as project-relative paths. Guard rejects traversal, absolute paths, and
|
|
172
|
+
symlinks escaping the project root; import-boundary checks include imports, re-exports, and literal
|
|
173
|
+
dynamic imports.
|
|
174
|
+
|
|
135
175
|
## What Guard discovers
|
|
136
176
|
|
|
137
177
|
The model is framework-aware without being framework-dependent:
|
|
@@ -150,6 +190,15 @@ The model is framework-aware without being framework-dependent:
|
|
|
150
190
|
Capabilities are evidence, not requirements. A Vite app, Express service, Hono project, Node
|
|
151
191
|
package, monorepo, or Next.js SaaS can all use the same Guard core.
|
|
152
192
|
|
|
193
|
+
## What Guard is — and is not
|
|
194
|
+
|
|
195
|
+
Guard is an architecture control plane for AI-assisted development. It protects project-specific
|
|
196
|
+
boundaries and change impact using local facts, approved policy, and deterministic verification.
|
|
197
|
+
|
|
198
|
+
Guard is not a replacement for ESLint, TypeScript, tests, SAST, or a general-purpose PR bot. Those
|
|
199
|
+
tools answer different questions; Guard connects their results with the architectural memory that
|
|
200
|
+
an AI coding agent needs before and after changing a repository.
|
|
201
|
+
|
|
153
202
|
## The normal loop
|
|
154
203
|
|
|
155
204
|
```text
|
|
@@ -164,6 +213,12 @@ init once → edit → check --changed → review → verify → commit / merge
|
|
|
164
213
|
contract validation.
|
|
165
214
|
5. `audit` inspects the complete current state, including findings accepted by the baseline.
|
|
166
215
|
|
|
216
|
+
For a focused change explanation, use `codapult-guard impact <file...>` or the equivalent
|
|
217
|
+
`codapult_guard_impact` MCP tool. It reports both directions of the dependency graph: what the
|
|
218
|
+
changed module uses and which transitive callers may be affected. `review` includes the same
|
|
219
|
+
impact packet plus typed Git changes (`added`, `modified`, `deleted`, and `renamed`) for the AI
|
|
220
|
+
host.
|
|
221
|
+
|
|
167
222
|
Guard state is stored in `.codapult/guard/`:
|
|
168
223
|
|
|
169
224
|
```text
|
|
@@ -194,10 +249,30 @@ Start the standalone MCP server over stdio:
|
|
|
194
249
|
pnpm exec codapult-guard mcp-server
|
|
195
250
|
```
|
|
196
251
|
|
|
252
|
+
Register that command in the MCP client from the project root. For example, Cursor can use
|
|
253
|
+
`.cursor/mcp.json`:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"mcpServers": {
|
|
258
|
+
"codapult-guard": {
|
|
259
|
+
"command": "pnpm",
|
|
260
|
+
"args": ["exec", "codapult-guard", "mcp-server"],
|
|
261
|
+
"cwd": "."
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For clients that accept a generic stdio server, use the same command and set its working
|
|
268
|
+
directory to the project root. Guard reads local files only; no API key or model provider is
|
|
269
|
+
required. Host-specific examples are in [`docs/integrations/`](docs/integrations/).
|
|
270
|
+
|
|
197
271
|
Recommended agent loop:
|
|
198
272
|
|
|
199
273
|
```text
|
|
200
274
|
task finished
|
|
275
|
+
→ codapult_guard_next_action
|
|
201
276
|
→ codapult_guard_context
|
|
202
277
|
→ codapult_guard_review(requirement, diff)
|
|
203
278
|
→ codapult_guard_verify
|
|
@@ -225,19 +300,24 @@ real project shapes.
|
|
|
225
300
|
|
|
226
301
|
## CLI surface
|
|
227
302
|
|
|
228
|
-
| Command | Purpose
|
|
229
|
-
| -------------------------- |
|
|
230
|
-
| `init` | Create project memory and the initial baseline.
|
|
231
|
-
| `analyze` | Refresh facts without changing policy or baseline.
|
|
232
|
-
| `propose` | Generate evidence-based rule and contract proposals.
|
|
233
|
-
| `check --changed` | Enforce active policy on changed and untracked files.
|
|
234
|
-
| `audit` | Scan the complete current project, including baseline findings.
|
|
235
|
-
| `review` | Create a bounded semantic-review packet for an AI host.
|
|
236
|
-
| `verify` | Run the configured completion gate.
|
|
237
|
-
| `doctor` | Diagnose invalid or missing Guard artifacts.
|
|
238
|
-
| `history` / `history-diff` | Inspect project model evolution.
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
303
|
+
| Command | Purpose |
|
|
304
|
+
| -------------------------- | ------------------------------------------------------------------------------------------- |
|
|
305
|
+
| `init` | Create project memory and the initial baseline. |
|
|
306
|
+
| `analyze` | Refresh facts without changing policy or baseline. |
|
|
307
|
+
| `propose` | Generate evidence-based rule and contract proposals. |
|
|
308
|
+
| `check --changed` | Enforce active policy on changed and untracked files. |
|
|
309
|
+
| `audit` | Scan the complete current project, including baseline findings. |
|
|
310
|
+
| `review` | Create a bounded semantic-review packet for an AI host. |
|
|
311
|
+
| `verify` | Run the configured completion gate. |
|
|
312
|
+
| `doctor` | Diagnose invalid or missing Guard artifacts. |
|
|
313
|
+
| `history` / `history-diff` | Inspect project model, module graph, and architecture-edge evolution. |
|
|
314
|
+
| `impact <files...>` | Explain dependencies, transitive dependents, capabilities, and contracts affected by files. |
|
|
315
|
+
| `policy explain <id>` | Explain a policy item, its evidence, and approval history. |
|
|
316
|
+
| `rules` / `contracts` | Approve or reject proposed policy. |
|
|
317
|
+
| `baseline` | Review or intentionally accept existing findings. |
|
|
318
|
+
|
|
319
|
+
Use `analyze --refresh` after a structural change. `doctor --fix-cache` removes only the disposable
|
|
320
|
+
discovery cache; it does not change rules, contracts, baseline, or source files.
|
|
241
321
|
|
|
242
322
|
Run `pnpm exec codapult-guard <command> --help` for command-specific options.
|
|
243
323
|
|
package/dist/adapters/command.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
const MAX_OUTPUT_CHARS = 20_000;
|
|
3
3
|
const MAX_BUFFER_BYTES = 2_000_000;
|
|
4
4
|
function redactOutput(value) {
|
|
@@ -13,9 +13,30 @@ function captureOutput(value) {
|
|
|
13
13
|
? { value: `${redacted.slice(0, MAX_OUTPUT_CHARS)}\n[output truncated]`, truncated: true }
|
|
14
14
|
: { value: redacted, truncated: false };
|
|
15
15
|
}
|
|
16
|
+
function parseCommand(command) {
|
|
17
|
+
const parts = command.trim().split(/\s+/).filter(Boolean);
|
|
18
|
+
if (parts.length === 0 || parts.some((part) => /[;&|<>`$()]/.test(part)))
|
|
19
|
+
return undefined;
|
|
20
|
+
const [executable, ...args] = parts;
|
|
21
|
+
const windowsExecutable = process.platform === 'win32' && /^(?:npm|npx|pnpm|yarn|bun)$/.test(executable)
|
|
22
|
+
? `${executable}.cmd`
|
|
23
|
+
: executable;
|
|
24
|
+
return { executable: windowsExecutable, args };
|
|
25
|
+
}
|
|
16
26
|
export function runProjectCommand(command, cwd, options = {}) {
|
|
27
|
+
const parsed = parseCommand(command);
|
|
28
|
+
if (!parsed) {
|
|
29
|
+
return {
|
|
30
|
+
command,
|
|
31
|
+
status: 'failed',
|
|
32
|
+
passed: false,
|
|
33
|
+
exitCode: 2,
|
|
34
|
+
stdout: '',
|
|
35
|
+
stderr: 'Unsafe or empty project command rejected.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
17
38
|
try {
|
|
18
|
-
const stdout =
|
|
39
|
+
const stdout = execFileSync(parsed.executable, parsed.args, {
|
|
19
40
|
cwd,
|
|
20
41
|
env: { ...process.env, ...options.env },
|
|
21
42
|
stdio: 'pipe',
|
|
@@ -2,7 +2,9 @@ import { type GuardToolMode } from '../../core/guard.js';
|
|
|
2
2
|
export declare function guardInitCommand(options?: {
|
|
3
3
|
force?: boolean;
|
|
4
4
|
}): void;
|
|
5
|
-
export declare function guardAnalyzeCommand(
|
|
5
|
+
export declare function guardAnalyzeCommand(_options?: {
|
|
6
|
+
refresh?: boolean;
|
|
7
|
+
}): void;
|
|
6
8
|
export declare function guardProposeCommand(options?: {
|
|
7
9
|
json?: boolean;
|
|
8
10
|
}): void;
|
|
@@ -11,8 +13,12 @@ export declare function guardInstallAgentCommand(target?: string, options?: {
|
|
|
11
13
|
}): void;
|
|
12
14
|
export declare function guardDoctorCommand(options?: {
|
|
13
15
|
json?: boolean;
|
|
16
|
+
fixCache?: boolean;
|
|
14
17
|
}): void;
|
|
15
18
|
export declare function guardHistoryCommand(): void;
|
|
19
|
+
export declare function guardImpactCommand(files: string[], options?: {
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}): void;
|
|
16
22
|
export declare function guardHistoryDiffCommand(from: string, to: string, options?: {
|
|
17
23
|
json?: boolean;
|
|
18
24
|
}): void;
|
|
@@ -41,6 +47,9 @@ export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove
|
|
|
41
47
|
export declare function guardRulesApproveCommand(ids?: string, options?: {
|
|
42
48
|
all?: boolean;
|
|
43
49
|
}): void;
|
|
50
|
+
export declare function guardPolicyExplainCommand(id: string, options?: {
|
|
51
|
+
json?: boolean;
|
|
52
|
+
}): void;
|
|
44
53
|
export declare function guardContractsApproveCommand(ids?: string, options?: {
|
|
45
54
|
all?: boolean;
|
|
46
55
|
}): void;
|
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { relative, resolve } from 'node:path';
|
|
3
|
-
import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, writeGuardConfig, writeGuardProposals, recordGuardProposalDecision, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardContracts, writeProjectModel, writeGuardMemory, writeProjectSnapshot, } from '../../core/guard.js';
|
|
3
|
+
import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, writeGuardConfig, writeGuardProposals, recordGuardProposalDecision, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardContracts, writeProjectModel, writeGuardMemory, writeProjectSnapshot, } from '../../core/guard.js';
|
|
4
|
+
import { guardErrorPayload } from '../../core/errors.js';
|
|
4
5
|
import { runGuardVerification, } from '../../core/verification/verify.js';
|
|
5
6
|
import { diagnoseGuard } from '../../core/analysis/doctor.js';
|
|
6
7
|
import { diffGuardSnapshots, listGuardSnapshots } from '../../core/history/history.js';
|
|
7
8
|
import { guardAgentTargets, installGuardAgentInstructions, installGuardAgentTargets, } from '../../adapters/agents/agent-integration.js';
|
|
8
9
|
import { dim, fail, heading, info, success, warn } from '../ui.js';
|
|
9
10
|
import { guardFindingsToSarif } from '../../core/output/sarif.js';
|
|
11
|
+
import { analyzeProjectImpact } from '../../core/analysis/impact.js';
|
|
10
12
|
function renderFindings(findings) {
|
|
11
13
|
for (const finding of findings) {
|
|
12
14
|
const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
|
|
13
15
|
printer(`${finding.file}:${finding.line} [${finding.ruleId}] ${finding.message}`);
|
|
14
16
|
dim(` import: ${finding.importPath}`);
|
|
17
|
+
if (finding.resolvedPath)
|
|
18
|
+
dim(` resolved: ${finding.resolvedPath}`);
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
function getRoot() {
|
|
18
22
|
return findGuardRoot();
|
|
19
23
|
}
|
|
24
|
+
function loadConfigSafely(root, options = {}) {
|
|
25
|
+
try {
|
|
26
|
+
return { config: loadGuardConfig(root), invalid: false };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : 'Guard configuration is invalid.';
|
|
30
|
+
const payload = guardErrorPayload('GUARD_CONFIG_INVALID', message, {
|
|
31
|
+
configured: false,
|
|
32
|
+
outcome: 'error',
|
|
33
|
+
recoverable: true,
|
|
34
|
+
hint: 'Repair or remove the invalid Guard artifact, then run `codapult-guard doctor`.',
|
|
35
|
+
});
|
|
36
|
+
if (options.json || options.machine)
|
|
37
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
38
|
+
else
|
|
39
|
+
fail(`${payload.message} Run \`codapult-guard doctor\` for details.`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return { invalid: true };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
20
44
|
function readRequirement(root, file) {
|
|
21
45
|
if (!file)
|
|
22
46
|
return undefined;
|
|
@@ -73,7 +97,7 @@ export function guardInitCommand(options = {}) {
|
|
|
73
97
|
info(`${config.rules.length} rule(s) generated (${proposedRules} proposed); ${report.scannedFiles} source file(s) scanned.`);
|
|
74
98
|
dim('Existing findings are baselined. New violations will be reported by `codapult-guard check`.');
|
|
75
99
|
}
|
|
76
|
-
export function guardAnalyzeCommand() {
|
|
100
|
+
export function guardAnalyzeCommand(_options = {}) {
|
|
77
101
|
const root = getRoot();
|
|
78
102
|
const { model: projectModel, metrics } = discoverProjectWithMetrics(root, {
|
|
79
103
|
persistCache: true,
|
|
@@ -90,7 +114,10 @@ export function guardAnalyzeCommand() {
|
|
|
90
114
|
export function guardProposeCommand(options = {}) {
|
|
91
115
|
const root = getRoot();
|
|
92
116
|
const model = discoverProject(root);
|
|
93
|
-
const
|
|
117
|
+
const loaded = loadConfigSafely(root, options);
|
|
118
|
+
if (loaded.invalid)
|
|
119
|
+
return;
|
|
120
|
+
const config = loaded.config ?? buildGeneratedGuardConfig(model);
|
|
94
121
|
const proposals = buildGuardProposals(model, config);
|
|
95
122
|
const previous = loadGuardProposals(root);
|
|
96
123
|
writeGuardProposals(root, {
|
|
@@ -134,9 +161,12 @@ export function guardInstallAgentCommand(target = 'generic', options = {}) {
|
|
|
134
161
|
process.exitCode = 0;
|
|
135
162
|
}
|
|
136
163
|
export function guardDoctorCommand(options = {}) {
|
|
164
|
+
if (options.fixCache)
|
|
165
|
+
clearDiscoveryCache(getRoot());
|
|
137
166
|
const report = diagnoseGuard(getRoot());
|
|
167
|
+
const result = options.fixCache ? { ...report, cacheFixed: true } : report;
|
|
138
168
|
if (options.json) {
|
|
139
|
-
console.log(JSON.stringify(
|
|
169
|
+
console.log(JSON.stringify(result, null, 2));
|
|
140
170
|
}
|
|
141
171
|
else {
|
|
142
172
|
heading('Codapult Guard Doctor');
|
|
@@ -153,7 +183,7 @@ export function guardDoctorCommand(options = {}) {
|
|
|
153
183
|
else
|
|
154
184
|
success('Guard state is healthy.');
|
|
155
185
|
}
|
|
156
|
-
process.exitCode =
|
|
186
|
+
process.exitCode = result.status === 'fail' ? 1 : 0;
|
|
157
187
|
}
|
|
158
188
|
export function guardHistoryCommand() {
|
|
159
189
|
const snapshots = listGuardSnapshots(getRoot());
|
|
@@ -166,6 +196,29 @@ export function guardHistoryCommand() {
|
|
|
166
196
|
info(`${snapshot.revision}: ${snapshot.files} files, ${snapshot.modules} modules, ${snapshot.cycles} cycles`);
|
|
167
197
|
}
|
|
168
198
|
}
|
|
199
|
+
export function guardImpactCommand(files, options = {}) {
|
|
200
|
+
const root = getRoot();
|
|
201
|
+
if (files.length === 0) {
|
|
202
|
+
fail('Specify at least one project-relative file.');
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const loaded = loadConfigSafely(root, options);
|
|
207
|
+
if (loaded.invalid)
|
|
208
|
+
return;
|
|
209
|
+
const result = analyzeProjectImpact(discoverProject(root), files, loaded.config?.contracts ?? []);
|
|
210
|
+
if (options.json) {
|
|
211
|
+
console.log(JSON.stringify({ status: 'ok', ...result }, null, 2));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
heading('Codapult Guard Impact');
|
|
215
|
+
info(`Requested: ${result.requestedFiles.join(', ')}`);
|
|
216
|
+
info(`Direct modules: ${result.directModules.join(', ') || 'none'}`);
|
|
217
|
+
info(`Dependencies: ${result.dependencies.join(', ') || 'none'}`);
|
|
218
|
+
info(`Transitive dependents: ${result.dependents.join(', ') || 'none'}`);
|
|
219
|
+
info(`Capabilities: ${result.capabilities.join(', ') || 'none'}`);
|
|
220
|
+
info(`Relevant contracts: ${result.relevantContracts.map((contract) => contract.id).join(', ') || 'none'}`);
|
|
221
|
+
}
|
|
169
222
|
export function guardHistoryDiffCommand(from, to, options = {}) {
|
|
170
223
|
const diff = diffGuardSnapshots(getRoot(), from, to);
|
|
171
224
|
if (!diff) {
|
|
@@ -181,6 +234,9 @@ export function guardHistoryDiffCommand(from, to, options = {}) {
|
|
|
181
234
|
info(`Removed files: ${diff.removedFiles.length}`);
|
|
182
235
|
info(`Added dependencies: ${diff.addedDependencies.join(', ') || 'none'}`);
|
|
183
236
|
info(`Removed dependencies: ${diff.removedDependencies.join(', ') || 'none'}`);
|
|
237
|
+
info(`Modules: +${diff.addedModules.length} / -${diff.removedModules.length}`);
|
|
238
|
+
info(`Dependency edges: +${diff.addedDependencyEdges.length} / -${diff.removedDependencyEdges.length}`);
|
|
239
|
+
info(`Layer edges: +${diff.addedLayerEdges.length} / -${diff.removedLayerEdges.length}`);
|
|
184
240
|
info(`Capabilities: +${diff.addedCapabilities.join(', ') || 'none'} / -${diff.removedCapabilities.join(', ') || 'none'}`);
|
|
185
241
|
info(`Cycles: ${diff.cycles.from} → ${diff.cycles.to}`);
|
|
186
242
|
}
|
|
@@ -244,10 +300,20 @@ export function guardVerifyCommand(options = {}) {
|
|
|
244
300
|
}
|
|
245
301
|
export function guardCheckCommand(options = {}) {
|
|
246
302
|
const root = getRoot();
|
|
247
|
-
const
|
|
303
|
+
const loaded = loadConfigSafely(root, { ...options, machine: options.json || options.sarif });
|
|
304
|
+
if (loaded.invalid)
|
|
305
|
+
return;
|
|
306
|
+
const config = loaded.config;
|
|
248
307
|
if (!config) {
|
|
249
308
|
if (options.json) {
|
|
250
|
-
console.log(JSON.stringify({
|
|
309
|
+
console.log(JSON.stringify({
|
|
310
|
+
status: 'error',
|
|
311
|
+
outcome: 'not-configured',
|
|
312
|
+
configured: false,
|
|
313
|
+
errorCode: 'GUARD_NOT_CONFIGURED',
|
|
314
|
+
message: 'Guard is not initialized.',
|
|
315
|
+
recoverable: true,
|
|
316
|
+
}));
|
|
251
317
|
process.exitCode = 1;
|
|
252
318
|
return;
|
|
253
319
|
}
|
|
@@ -304,10 +370,20 @@ export function guardCheckCommand(options = {}) {
|
|
|
304
370
|
}
|
|
305
371
|
export function guardAuditCommand(options = {}) {
|
|
306
372
|
const root = getRoot();
|
|
307
|
-
const
|
|
373
|
+
const loaded = loadConfigSafely(root, options);
|
|
374
|
+
if (loaded.invalid)
|
|
375
|
+
return;
|
|
376
|
+
const config = loaded.config;
|
|
308
377
|
if (!config) {
|
|
309
378
|
if (options.json) {
|
|
310
|
-
console.log(JSON.stringify({
|
|
379
|
+
console.log(JSON.stringify({
|
|
380
|
+
status: 'error',
|
|
381
|
+
outcome: 'not-configured',
|
|
382
|
+
configured: false,
|
|
383
|
+
errorCode: 'GUARD_NOT_CONFIGURED',
|
|
384
|
+
message: 'Guard is not initialized.',
|
|
385
|
+
recoverable: true,
|
|
386
|
+
}));
|
|
311
387
|
process.exitCode = 1;
|
|
312
388
|
return;
|
|
313
389
|
}
|
|
@@ -364,9 +440,12 @@ export function guardBaselineCommand(action, ids, options = {}) {
|
|
|
364
440
|
process.exitCode = 0;
|
|
365
441
|
return;
|
|
366
442
|
}
|
|
443
|
+
const loaded = options.all ? loadConfigSafely(root, options) : { invalid: false };
|
|
444
|
+
if (loaded.invalid)
|
|
445
|
+
return;
|
|
367
446
|
const selected = options.all
|
|
368
447
|
? [
|
|
369
|
-
...scanGuard(root,
|
|
448
|
+
...scanGuard(root, loaded.config ?? buildGeneratedGuardConfig(discoverProject(root)), {
|
|
370
449
|
includeArchitectureInsights: true,
|
|
371
450
|
}).findings.map((finding) => finding.fingerprint),
|
|
372
451
|
]
|
|
@@ -391,7 +470,10 @@ export function guardBaselineCommand(action, ids, options = {}) {
|
|
|
391
470
|
}
|
|
392
471
|
export function guardRulesApproveCommand(ids, options = {}) {
|
|
393
472
|
const root = getRoot();
|
|
394
|
-
const
|
|
473
|
+
const loaded = loadConfigSafely(root);
|
|
474
|
+
if (loaded.invalid)
|
|
475
|
+
return;
|
|
476
|
+
const config = loaded.config;
|
|
395
477
|
if (!config) {
|
|
396
478
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
397
479
|
process.exitCode = 1;
|
|
@@ -418,9 +500,55 @@ export function guardRulesApproveCommand(ids, options = {}) {
|
|
|
418
500
|
success(`Activated ${rule.id}`);
|
|
419
501
|
process.exitCode = 0;
|
|
420
502
|
}
|
|
503
|
+
export function guardPolicyExplainCommand(id, options = {}) {
|
|
504
|
+
const root = getRoot();
|
|
505
|
+
const loaded = loadConfigSafely(root, options);
|
|
506
|
+
if (loaded.invalid)
|
|
507
|
+
return;
|
|
508
|
+
const config = loaded.config;
|
|
509
|
+
const proposals = loadGuardProposals(root);
|
|
510
|
+
if (!config) {
|
|
511
|
+
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
512
|
+
process.exitCode = 1;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const item = [...config.rules, ...(config.contracts ?? [])].find((entry) => entry.id === id);
|
|
516
|
+
const proposed = [...(proposals?.rules ?? []), ...(proposals?.contracts ?? [])].find((entry) => entry.id === id);
|
|
517
|
+
if (!item && !proposed) {
|
|
518
|
+
fail(`Policy item not found: ${id}`);
|
|
519
|
+
process.exitCode = 1;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const result = {
|
|
523
|
+
id,
|
|
524
|
+
active: item?.status !== 'proposed' && item !== undefined,
|
|
525
|
+
definition: item ?? proposed,
|
|
526
|
+
proposal: proposed,
|
|
527
|
+
decisions: proposals?.decisions?.filter((decision) => decision.id === id) ?? [],
|
|
528
|
+
};
|
|
529
|
+
if (options.json)
|
|
530
|
+
console.log(JSON.stringify(result, null, 2));
|
|
531
|
+
else {
|
|
532
|
+
heading(`Guard Policy: ${id}`);
|
|
533
|
+
info(`Status: ${result.active ? 'active' : 'proposed'}`);
|
|
534
|
+
const definition = result.definition;
|
|
535
|
+
if (definition && 'description' in definition)
|
|
536
|
+
dim(definition.description);
|
|
537
|
+
if (definition && 'statement' in definition)
|
|
538
|
+
dim(definition.statement);
|
|
539
|
+
if ((definition?.evidence?.length ?? 0) > 0)
|
|
540
|
+
dim(`Evidence: ${definition?.evidence?.join(', ')}`);
|
|
541
|
+
if (result.decisions.length > 0)
|
|
542
|
+
dim(`Decisions: ${result.decisions.length}`);
|
|
543
|
+
}
|
|
544
|
+
process.exitCode = 0;
|
|
545
|
+
}
|
|
421
546
|
export function guardContractsApproveCommand(ids, options = {}) {
|
|
422
547
|
const root = getRoot();
|
|
423
|
-
const
|
|
548
|
+
const loaded = loadConfigSafely(root);
|
|
549
|
+
if (loaded.invalid)
|
|
550
|
+
return;
|
|
551
|
+
const config = loaded.config;
|
|
424
552
|
if (!config) {
|
|
425
553
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
426
554
|
process.exitCode = 1;
|
|
@@ -453,7 +581,10 @@ export function guardContractsApproveCommand(ids, options = {}) {
|
|
|
453
581
|
}
|
|
454
582
|
export function guardContractsRejectCommand(ids, options = {}) {
|
|
455
583
|
const root = getRoot();
|
|
456
|
-
const
|
|
584
|
+
const loaded = loadConfigSafely(root);
|
|
585
|
+
if (loaded.invalid)
|
|
586
|
+
return;
|
|
587
|
+
const config = loaded.config;
|
|
457
588
|
if (!config) {
|
|
458
589
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
459
590
|
process.exitCode = 1;
|
|
@@ -488,7 +619,10 @@ export function guardReviewCommand(options = {}) {
|
|
|
488
619
|
const requirement = readRequirement(root, options.requirement);
|
|
489
620
|
if (options.requirement && !requirement)
|
|
490
621
|
return;
|
|
491
|
-
const
|
|
622
|
+
const loaded = loadConfigSafely(root);
|
|
623
|
+
if (loaded.invalid)
|
|
624
|
+
return;
|
|
625
|
+
const config = loaded.config;
|
|
492
626
|
if (!config) {
|
|
493
627
|
fail(`Guard is not initialized. Run \`codapult-guard init\` first.`);
|
|
494
628
|
process.exitCode = 1;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardInitCommand, guardInstallAgentCommand, guardProposeCommand, guardReviewCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
|
|
4
|
+
import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
|
|
5
5
|
import { config } from '../core/config.js';
|
|
6
|
+
import { GuardConfigError, GuardStateBusyError } from '../core/guard.js';
|
|
7
|
+
import { guardErrorPayload } from '../core/errors.js';
|
|
6
8
|
const program = new Command()
|
|
7
9
|
.name(config.commandName)
|
|
8
10
|
.description('Local-first architecture guardrails for JavaScript and TypeScript projects')
|
|
@@ -15,12 +17,13 @@ const program = new Command()
|
|
|
15
17
|
// `codapult-guard init`, not `codapult-guard guard init`.
|
|
16
18
|
const guard = program;
|
|
17
19
|
guard.command('init').option('--force').action(guardInitCommand);
|
|
18
|
-
guard.command('analyze').action(guardAnalyzeCommand);
|
|
20
|
+
guard.command('analyze').option('--refresh').action(guardAnalyzeCommand);
|
|
19
21
|
guard.command('propose').option('--json').action(guardProposeCommand);
|
|
20
22
|
guard.command('install-agent [target]').option('--json').action(guardInstallAgentCommand);
|
|
21
|
-
guard.command('doctor').option('--json').action(guardDoctorCommand);
|
|
23
|
+
guard.command('doctor').option('--json').option('--fix-cache').action(guardDoctorCommand);
|
|
22
24
|
guard.command('history').action(guardHistoryCommand);
|
|
23
25
|
guard.command('history-diff <from> <to>').option('--json').action(guardHistoryDiffCommand);
|
|
26
|
+
guard.command('impact <files...>').option('--json').action(guardImpactCommand);
|
|
24
27
|
guard
|
|
25
28
|
.command('check')
|
|
26
29
|
.option('--changed')
|
|
@@ -46,6 +49,8 @@ guard
|
|
|
46
49
|
.action(guardReviewCommand);
|
|
47
50
|
const rules = guard.command('rules');
|
|
48
51
|
rules.command('approve [ids]').option('--all').action(guardRulesApproveCommand);
|
|
52
|
+
const policy = guard.command('policy');
|
|
53
|
+
policy.command('explain <id>').option('--json').action(guardPolicyExplainCommand);
|
|
49
54
|
const contracts = guard.command('contracts');
|
|
50
55
|
contracts.command('approve [ids]').option('--all').action(guardContractsApproveCommand);
|
|
51
56
|
contracts.command('reject [ids]').option('--all').action(guardContractsRejectCommand);
|
|
@@ -72,4 +77,23 @@ program
|
|
|
72
77
|
.action(async () => {
|
|
73
78
|
await import('../mcp/server.js');
|
|
74
79
|
});
|
|
75
|
-
|
|
80
|
+
try {
|
|
81
|
+
program.parse();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof GuardConfigError || error instanceof GuardStateBusyError) {
|
|
85
|
+
const isBusy = error instanceof GuardStateBusyError;
|
|
86
|
+
console.error(JSON.stringify(guardErrorPayload(isBusy ? 'GUARD_STATE_BUSY' : 'GUARD_CONFIG_INVALID', error.message, {
|
|
87
|
+
configured: !isBusy,
|
|
88
|
+
outcome: 'error',
|
|
89
|
+
recoverable: true,
|
|
90
|
+
hint: isBusy
|
|
91
|
+
? 'Retry after the other Guard process finishes.'
|
|
92
|
+
: 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
|
|
93
|
+
}), null, 2));
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -22,12 +22,18 @@ function parseJson(root, path) {
|
|
|
22
22
|
export function diagnoseGuard(root) {
|
|
23
23
|
const initialized = existsSync(resolve(root, GUARD_BASELINE_FILE));
|
|
24
24
|
const items = requiredArtifacts.map((path) => {
|
|
25
|
-
const guardConfig = path === GUARD_CONTRACTS_FILE ? loadGuardConfig(root) : undefined;
|
|
26
25
|
if (!existsSync(resolve(root, path))) {
|
|
27
26
|
return { path, status: 'missing', message: 'Artifact is missing.' };
|
|
28
27
|
}
|
|
29
|
-
if (path === GUARD_RULES_FILE
|
|
30
|
-
|
|
28
|
+
if (path === GUARD_RULES_FILE) {
|
|
29
|
+
try {
|
|
30
|
+
if (!loadGuardConfig(root)) {
|
|
31
|
+
return { path, status: 'invalid', message: 'Guard rules are invalid or unsupported.' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { path, status: 'invalid', message: 'Guard rules are invalid or unsupported.' };
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
if (path === GUARD_PROJECT_FILE && !loadProjectModel(root)) {
|
|
33
39
|
return { path, status: 'invalid', message: 'Project model is invalid or unsupported.' };
|
|
@@ -39,8 +45,16 @@ export function diagnoseGuard(root) {
|
|
|
39
45
|
return { path, status: 'invalid', message: 'Guard contracts are invalid or unsupported.' };
|
|
40
46
|
}
|
|
41
47
|
if (path === GUARD_CONTRACTS_FILE &&
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
(() => {
|
|
49
|
+
try {
|
|
50
|
+
const guardConfig = loadGuardConfig(root);
|
|
51
|
+
return (guardConfig !== undefined &&
|
|
52
|
+
validateGuardContracts(root, guardConfig.contracts).length > 0);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
})()) {
|
|
44
58
|
return { path, status: 'invalid', message: 'Guard contract scopes or references are stale.' };
|
|
45
59
|
}
|
|
46
60
|
if (path === GUARD_PROPOSALS_FILE && !isGuardProposalFile(parseJson(root, path))) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GuardContract } from '../guard.js';
|
|
2
|
+
import { type ProjectModel } from '../discovery/discovery.js';
|
|
3
|
+
export interface GuardImpactAnalysis {
|
|
4
|
+
requestedFiles: string[];
|
|
5
|
+
directModules: string[];
|
|
6
|
+
dependencies: string[];
|
|
7
|
+
dependents: string[];
|
|
8
|
+
affectedModules: string[];
|
|
9
|
+
impactPaths: ProjectModel['insights']['impactPaths'];
|
|
10
|
+
capabilities: string[];
|
|
11
|
+
relevantContracts: GuardContract[];
|
|
12
|
+
dependencyEdges: ProjectModel['insights']['dependencyEdges'];
|
|
13
|
+
layerEdges: ProjectModel['insights']['layerEdges'];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Computes the blast radius of a change from the discovered graph.
|
|
17
|
+
*
|
|
18
|
+
* The graph direction is importer -> imported module. Therefore `dependencies` are what the
|
|
19
|
+
* changed code relies on, while `dependents` are callers that can regress when the changed
|
|
20
|
+
* code changes. Both directions matter for an AI review packet.
|
|
21
|
+
*/
|
|
22
|
+
export declare function analyzeProjectImpact(model: ProjectModel, files: Iterable<string>, contracts?: GuardContract[]): GuardImpactAnalysis;
|