@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
package/README.md
CHANGED
|
@@ -168,6 +168,10 @@ Guard does not assume a fixed `UI → actions → services → repositories →
|
|
|
168
168
|
It can discover that shape when the project exhibits it, but observed patterns become enforceable
|
|
169
169
|
only after explicit approval.
|
|
170
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
|
+
|
|
171
175
|
## What Guard discovers
|
|
172
176
|
|
|
173
177
|
The model is framework-aware without being framework-dependent:
|
|
@@ -186,6 +190,15 @@ The model is framework-aware without being framework-dependent:
|
|
|
186
190
|
Capabilities are evidence, not requirements. A Vite app, Express service, Hono project, Node
|
|
187
191
|
package, monorepo, or Next.js SaaS can all use the same Guard core.
|
|
188
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
|
+
|
|
189
202
|
## The normal loop
|
|
190
203
|
|
|
191
204
|
```text
|
|
@@ -259,6 +272,7 @@ Recommended agent loop:
|
|
|
259
272
|
|
|
260
273
|
```text
|
|
261
274
|
task finished
|
|
275
|
+
→ codapult_guard_next_action
|
|
262
276
|
→ codapult_guard_context
|
|
263
277
|
→ codapult_guard_review(requirement, diff)
|
|
264
278
|
→ codapult_guard_verify
|
|
@@ -298,9 +312,13 @@ real project shapes.
|
|
|
298
312
|
| `doctor` | Diagnose invalid or missing Guard artifacts. |
|
|
299
313
|
| `history` / `history-diff` | Inspect project model, module graph, and architecture-edge evolution. |
|
|
300
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. |
|
|
301
316
|
| `rules` / `contracts` | Approve or reject proposed policy. |
|
|
302
317
|
| `baseline` | Review or intentionally accept existing findings. |
|
|
303
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.
|
|
321
|
+
|
|
304
322
|
Run `pnpm exec codapult-guard <command> --help` for command-specific options.
|
|
305
323
|
|
|
306
324
|
## Security and data handling
|
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,6 +13,7 @@ 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;
|
|
16
19
|
export declare function guardImpactCommand(files: string[], options?: {
|
|
@@ -44,6 +47,9 @@ export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove
|
|
|
44
47
|
export declare function guardRulesApproveCommand(ids?: string, options?: {
|
|
45
48
|
all?: boolean;
|
|
46
49
|
}): void;
|
|
50
|
+
export declare function guardPolicyExplainCommand(id: string, options?: {
|
|
51
|
+
json?: boolean;
|
|
52
|
+
}): void;
|
|
47
53
|
export declare function guardContractsApproveCommand(ids?: string, options?: {
|
|
48
54
|
all?: boolean;
|
|
49
55
|
}): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
@@ -13,11 +14,33 @@ function renderFindings(findings) {
|
|
|
13
14
|
const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
|
|
14
15
|
printer(`${finding.file}:${finding.line} [${finding.ruleId}] ${finding.message}`);
|
|
15
16
|
dim(` import: ${finding.importPath}`);
|
|
17
|
+
if (finding.resolvedPath)
|
|
18
|
+
dim(` resolved: ${finding.resolvedPath}`);
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
function getRoot() {
|
|
19
22
|
return findGuardRoot();
|
|
20
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
|
+
}
|
|
21
44
|
function readRequirement(root, file) {
|
|
22
45
|
if (!file)
|
|
23
46
|
return undefined;
|
|
@@ -74,7 +97,7 @@ export function guardInitCommand(options = {}) {
|
|
|
74
97
|
info(`${config.rules.length} rule(s) generated (${proposedRules} proposed); ${report.scannedFiles} source file(s) scanned.`);
|
|
75
98
|
dim('Existing findings are baselined. New violations will be reported by `codapult-guard check`.');
|
|
76
99
|
}
|
|
77
|
-
export function guardAnalyzeCommand() {
|
|
100
|
+
export function guardAnalyzeCommand(_options = {}) {
|
|
78
101
|
const root = getRoot();
|
|
79
102
|
const { model: projectModel, metrics } = discoverProjectWithMetrics(root, {
|
|
80
103
|
persistCache: true,
|
|
@@ -91,7 +114,10 @@ export function guardAnalyzeCommand() {
|
|
|
91
114
|
export function guardProposeCommand(options = {}) {
|
|
92
115
|
const root = getRoot();
|
|
93
116
|
const model = discoverProject(root);
|
|
94
|
-
const
|
|
117
|
+
const loaded = loadConfigSafely(root, options);
|
|
118
|
+
if (loaded.invalid)
|
|
119
|
+
return;
|
|
120
|
+
const config = loaded.config ?? buildGeneratedGuardConfig(model);
|
|
95
121
|
const proposals = buildGuardProposals(model, config);
|
|
96
122
|
const previous = loadGuardProposals(root);
|
|
97
123
|
writeGuardProposals(root, {
|
|
@@ -135,9 +161,12 @@ export function guardInstallAgentCommand(target = 'generic', options = {}) {
|
|
|
135
161
|
process.exitCode = 0;
|
|
136
162
|
}
|
|
137
163
|
export function guardDoctorCommand(options = {}) {
|
|
164
|
+
if (options.fixCache)
|
|
165
|
+
clearDiscoveryCache(getRoot());
|
|
138
166
|
const report = diagnoseGuard(getRoot());
|
|
167
|
+
const result = options.fixCache ? { ...report, cacheFixed: true } : report;
|
|
139
168
|
if (options.json) {
|
|
140
|
-
console.log(JSON.stringify(
|
|
169
|
+
console.log(JSON.stringify(result, null, 2));
|
|
141
170
|
}
|
|
142
171
|
else {
|
|
143
172
|
heading('Codapult Guard Doctor');
|
|
@@ -154,7 +183,7 @@ export function guardDoctorCommand(options = {}) {
|
|
|
154
183
|
else
|
|
155
184
|
success('Guard state is healthy.');
|
|
156
185
|
}
|
|
157
|
-
process.exitCode =
|
|
186
|
+
process.exitCode = result.status === 'fail' ? 1 : 0;
|
|
158
187
|
}
|
|
159
188
|
export function guardHistoryCommand() {
|
|
160
189
|
const snapshots = listGuardSnapshots(getRoot());
|
|
@@ -174,7 +203,10 @@ export function guardImpactCommand(files, options = {}) {
|
|
|
174
203
|
process.exitCode = 1;
|
|
175
204
|
return;
|
|
176
205
|
}
|
|
177
|
-
const
|
|
206
|
+
const loaded = loadConfigSafely(root, options);
|
|
207
|
+
if (loaded.invalid)
|
|
208
|
+
return;
|
|
209
|
+
const result = analyzeProjectImpact(discoverProject(root), files, loaded.config?.contracts ?? []);
|
|
178
210
|
if (options.json) {
|
|
179
211
|
console.log(JSON.stringify({ status: 'ok', ...result }, null, 2));
|
|
180
212
|
return;
|
|
@@ -268,10 +300,20 @@ export function guardVerifyCommand(options = {}) {
|
|
|
268
300
|
}
|
|
269
301
|
export function guardCheckCommand(options = {}) {
|
|
270
302
|
const root = getRoot();
|
|
271
|
-
const
|
|
303
|
+
const loaded = loadConfigSafely(root, { ...options, machine: options.json || options.sarif });
|
|
304
|
+
if (loaded.invalid)
|
|
305
|
+
return;
|
|
306
|
+
const config = loaded.config;
|
|
272
307
|
if (!config) {
|
|
273
308
|
if (options.json) {
|
|
274
|
-
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
|
+
}));
|
|
275
317
|
process.exitCode = 1;
|
|
276
318
|
return;
|
|
277
319
|
}
|
|
@@ -328,10 +370,20 @@ export function guardCheckCommand(options = {}) {
|
|
|
328
370
|
}
|
|
329
371
|
export function guardAuditCommand(options = {}) {
|
|
330
372
|
const root = getRoot();
|
|
331
|
-
const
|
|
373
|
+
const loaded = loadConfigSafely(root, options);
|
|
374
|
+
if (loaded.invalid)
|
|
375
|
+
return;
|
|
376
|
+
const config = loaded.config;
|
|
332
377
|
if (!config) {
|
|
333
378
|
if (options.json) {
|
|
334
|
-
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
|
+
}));
|
|
335
387
|
process.exitCode = 1;
|
|
336
388
|
return;
|
|
337
389
|
}
|
|
@@ -388,9 +440,12 @@ export function guardBaselineCommand(action, ids, options = {}) {
|
|
|
388
440
|
process.exitCode = 0;
|
|
389
441
|
return;
|
|
390
442
|
}
|
|
443
|
+
const loaded = options.all ? loadConfigSafely(root, options) : { invalid: false };
|
|
444
|
+
if (loaded.invalid)
|
|
445
|
+
return;
|
|
391
446
|
const selected = options.all
|
|
392
447
|
? [
|
|
393
|
-
...scanGuard(root,
|
|
448
|
+
...scanGuard(root, loaded.config ?? buildGeneratedGuardConfig(discoverProject(root)), {
|
|
394
449
|
includeArchitectureInsights: true,
|
|
395
450
|
}).findings.map((finding) => finding.fingerprint),
|
|
396
451
|
]
|
|
@@ -415,7 +470,10 @@ export function guardBaselineCommand(action, ids, options = {}) {
|
|
|
415
470
|
}
|
|
416
471
|
export function guardRulesApproveCommand(ids, options = {}) {
|
|
417
472
|
const root = getRoot();
|
|
418
|
-
const
|
|
473
|
+
const loaded = loadConfigSafely(root);
|
|
474
|
+
if (loaded.invalid)
|
|
475
|
+
return;
|
|
476
|
+
const config = loaded.config;
|
|
419
477
|
if (!config) {
|
|
420
478
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
421
479
|
process.exitCode = 1;
|
|
@@ -442,9 +500,55 @@ export function guardRulesApproveCommand(ids, options = {}) {
|
|
|
442
500
|
success(`Activated ${rule.id}`);
|
|
443
501
|
process.exitCode = 0;
|
|
444
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
|
+
}
|
|
445
546
|
export function guardContractsApproveCommand(ids, options = {}) {
|
|
446
547
|
const root = getRoot();
|
|
447
|
-
const
|
|
548
|
+
const loaded = loadConfigSafely(root);
|
|
549
|
+
if (loaded.invalid)
|
|
550
|
+
return;
|
|
551
|
+
const config = loaded.config;
|
|
448
552
|
if (!config) {
|
|
449
553
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
450
554
|
process.exitCode = 1;
|
|
@@ -477,7 +581,10 @@ export function guardContractsApproveCommand(ids, options = {}) {
|
|
|
477
581
|
}
|
|
478
582
|
export function guardContractsRejectCommand(ids, options = {}) {
|
|
479
583
|
const root = getRoot();
|
|
480
|
-
const
|
|
584
|
+
const loaded = loadConfigSafely(root);
|
|
585
|
+
if (loaded.invalid)
|
|
586
|
+
return;
|
|
587
|
+
const config = loaded.config;
|
|
481
588
|
if (!config) {
|
|
482
589
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
483
590
|
process.exitCode = 1;
|
|
@@ -512,7 +619,10 @@ export function guardReviewCommand(options = {}) {
|
|
|
512
619
|
const requirement = readRequirement(root, options.requirement);
|
|
513
620
|
if (options.requirement && !requirement)
|
|
514
621
|
return;
|
|
515
|
-
const
|
|
622
|
+
const loaded = loadConfigSafely(root);
|
|
623
|
+
if (loaded.invalid)
|
|
624
|
+
return;
|
|
625
|
+
const config = loaded.config;
|
|
516
626
|
if (!config) {
|
|
517
627
|
fail(`Guard is not initialized. Run \`codapult-guard init\` first.`);
|
|
518
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, guardImpactCommand, 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,10 +17,10 @@ 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);
|
|
24
26
|
guard.command('impact <files...>').option('--json').action(guardImpactCommand);
|
|
@@ -47,6 +49,8 @@ guard
|
|
|
47
49
|
.action(guardReviewCommand);
|
|
48
50
|
const rules = guard.command('rules');
|
|
49
51
|
rules.command('approve [ids]').option('--all').action(guardRulesApproveCommand);
|
|
52
|
+
const policy = guard.command('policy');
|
|
53
|
+
policy.command('explain <id>').option('--json').action(guardPolicyExplainCommand);
|
|
50
54
|
const contracts = guard.command('contracts');
|
|
51
55
|
contracts.command('approve [ids]').option('--all').action(guardContractsApproveCommand);
|
|
52
56
|
contracts.command('reject [ids]').option('--all').action(guardContractsRejectCommand);
|
|
@@ -73,4 +77,23 @@ program
|
|
|
73
77
|
.action(async () => {
|
|
74
78
|
await import('../mcp/server.js');
|
|
75
79
|
});
|
|
76
|
-
|
|
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))) {
|
|
@@ -11,6 +11,7 @@ export interface ModuleRecord {
|
|
|
11
11
|
exports: string[];
|
|
12
12
|
calls: string[];
|
|
13
13
|
resolvedImports: string[];
|
|
14
|
+
resolvedImportMap?: Record<string, string>;
|
|
14
15
|
dynamicImports: string[];
|
|
15
16
|
declarations: {
|
|
16
17
|
classes: number;
|
|
@@ -44,6 +45,8 @@ export interface ProjectModel {
|
|
|
44
45
|
private: boolean;
|
|
45
46
|
scripts: Record<string, string>;
|
|
46
47
|
dependencies: string[];
|
|
48
|
+
exports?: string[];
|
|
49
|
+
projectReferences?: string[];
|
|
47
50
|
}[];
|
|
48
51
|
};
|
|
49
52
|
files: ProjectFileRecord[];
|
|
@@ -122,6 +125,17 @@ export interface DiscoveryMetrics {
|
|
|
122
125
|
changedFiles: number;
|
|
123
126
|
reusedModules: number;
|
|
124
127
|
}
|
|
128
|
+
export interface DiscoveryOptions {
|
|
129
|
+
persistCache?: boolean;
|
|
130
|
+
/** Maximum number of files included in one model. */
|
|
131
|
+
maxFiles?: number;
|
|
132
|
+
/** Maximum size of one included file in bytes. */
|
|
133
|
+
maxFileBytes?: number;
|
|
134
|
+
}
|
|
135
|
+
export declare class DiscoveryLimitError extends Error {
|
|
136
|
+
constructor(message: string);
|
|
137
|
+
}
|
|
138
|
+
export declare function clearDiscoveryCache(root: string): void;
|
|
125
139
|
export declare function findGuardRoot(from?: string): string;
|
|
126
140
|
/** Build the internal module graph from AST resolution plus conservative fallback resolution.
|
|
127
141
|
*
|
|
@@ -130,12 +144,8 @@ export declare function findGuardRoot(from?: string): string;
|
|
|
130
144
|
* no resolved imports silently drops unresolved edges from mixed projects.
|
|
131
145
|
*/
|
|
132
146
|
export declare function buildModuleTargetGraph(modules: ModuleRecord[]): Map<string, string[]>;
|
|
133
|
-
export declare function discoverProject(root: string, options?:
|
|
134
|
-
|
|
135
|
-
}): ProjectModel;
|
|
136
|
-
export declare function discoverProjectWithMetrics(root: string, options?: {
|
|
137
|
-
persistCache?: boolean;
|
|
138
|
-
}): {
|
|
147
|
+
export declare function discoverProject(root: string, options?: DiscoveryOptions): ProjectModel;
|
|
148
|
+
export declare function discoverProjectWithMetrics(root: string, options?: DiscoveryOptions): {
|
|
139
149
|
model: ProjectModel;
|
|
140
150
|
metrics: DiscoveryMetrics;
|
|
141
151
|
};
|