@codapult/guard 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -1
- package/dist/adapters/command.d.ts +10 -8
- package/dist/adapters/command.js +23 -2
- package/dist/adapters/project-checks.d.ts +23 -20
- package/dist/cli/commands/guard.d.ts +53 -54
- package/dist/cli/commands/guard.js +155 -32
- package/dist/cli/index.js +41 -5
- package/dist/core/analysis/doctor.d.ts +1 -1
- package/dist/core/analysis/doctor.js +19 -6
- package/dist/core/discovery/discovery.d.ts +27 -13
- package/dist/core/discovery/discovery.js +182 -29
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/guard.d.ts +134 -69
- package/dist/core/guard.js +767 -145
- package/dist/core/history/runs.d.ts +28 -0
- package/dist/core/history/runs.js +59 -0
- package/dist/core/model/types.d.ts +10 -0
- package/dist/core/model/types.js +1 -0
- package/dist/core/policy/schemas.d.ts +81 -0
- package/dist/core/policy/schemas.js +46 -17
- package/dist/core/verification/verify.d.ts +21 -12
- package/dist/core/verification/verify.js +81 -8
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp/resources.js +64 -29
- package/dist/mcp/tools/guard.js +300 -52
- package/package.json +12 -7
|
@@ -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,
|
|
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, applyGuardProposalDecision, writeGuardProposals, validateGuardProposalApproval, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardPolicy, writeProjectState, } 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;
|
|
@@ -47,7 +70,7 @@ export function guardInitCommand(options = {}) {
|
|
|
47
70
|
const root = getRoot();
|
|
48
71
|
let initialized;
|
|
49
72
|
try {
|
|
50
|
-
initialized = initializeGuard(root, { force: options.force });
|
|
73
|
+
initialized = initializeGuard(root, options.wait === false ? { force: options.force, noWait: true } : { force: options.force });
|
|
51
74
|
}
|
|
52
75
|
catch (error) {
|
|
53
76
|
if (error instanceof GuardAlreadyInitializedError) {
|
|
@@ -74,14 +97,12 @@ 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,
|
|
81
104
|
});
|
|
82
|
-
|
|
83
|
-
writeGuardMemory(root, projectModel);
|
|
84
|
-
const revision = writeProjectSnapshot(root, projectModel);
|
|
105
|
+
const revision = writeProjectState(root, projectModel, options.wait === false ? { noWait: true } : undefined);
|
|
85
106
|
heading('Codapult Guard Analyze');
|
|
86
107
|
success(`Updated ${GUARD_PROJECT_FILE}, ${GUARD_ARCHITECTURE_FILE}, and ${GUARD_CONVENTIONS_FILE}`);
|
|
87
108
|
info(`${projectModel.files.length} file(s), ${projectModel.modules.length} module(s), ${projectModel.insights.cycles.length} cycle(s) recorded as project context.`);
|
|
@@ -91,7 +112,10 @@ export function guardAnalyzeCommand() {
|
|
|
91
112
|
export function guardProposeCommand(options = {}) {
|
|
92
113
|
const root = getRoot();
|
|
93
114
|
const model = discoverProject(root);
|
|
94
|
-
const
|
|
115
|
+
const loaded = loadConfigSafely(root, options);
|
|
116
|
+
if (loaded.invalid)
|
|
117
|
+
return;
|
|
118
|
+
const config = loaded.config ?? buildGeneratedGuardConfig(model);
|
|
95
119
|
const proposals = buildGuardProposals(model, config);
|
|
96
120
|
const previous = loadGuardProposals(root);
|
|
97
121
|
writeGuardProposals(root, {
|
|
@@ -135,9 +159,12 @@ export function guardInstallAgentCommand(target = 'generic', options = {}) {
|
|
|
135
159
|
process.exitCode = 0;
|
|
136
160
|
}
|
|
137
161
|
export function guardDoctorCommand(options = {}) {
|
|
162
|
+
if (options.fixCache)
|
|
163
|
+
clearDiscoveryCache(getRoot());
|
|
138
164
|
const report = diagnoseGuard(getRoot());
|
|
165
|
+
const result = options.fixCache ? { ...report, cacheFixed: true } : report;
|
|
139
166
|
if (options.json) {
|
|
140
|
-
console.log(JSON.stringify(
|
|
167
|
+
console.log(JSON.stringify(result, null, 2));
|
|
141
168
|
}
|
|
142
169
|
else {
|
|
143
170
|
heading('Codapult Guard Doctor');
|
|
@@ -154,7 +181,7 @@ export function guardDoctorCommand(options = {}) {
|
|
|
154
181
|
else
|
|
155
182
|
success('Guard state is healthy.');
|
|
156
183
|
}
|
|
157
|
-
process.exitCode =
|
|
184
|
+
process.exitCode = result.status === 'fail' ? 1 : 0;
|
|
158
185
|
}
|
|
159
186
|
export function guardHistoryCommand() {
|
|
160
187
|
const snapshots = listGuardSnapshots(getRoot());
|
|
@@ -174,7 +201,10 @@ export function guardImpactCommand(files, options = {}) {
|
|
|
174
201
|
process.exitCode = 1;
|
|
175
202
|
return;
|
|
176
203
|
}
|
|
177
|
-
const
|
|
204
|
+
const loaded = loadConfigSafely(root, options);
|
|
205
|
+
if (loaded.invalid)
|
|
206
|
+
return;
|
|
207
|
+
const result = analyzeProjectImpact(discoverProject(root), files, loaded.config?.contracts ?? []);
|
|
178
208
|
if (options.json) {
|
|
179
209
|
console.log(JSON.stringify({ status: 'ok', ...result }, null, 2));
|
|
180
210
|
return;
|
|
@@ -268,10 +298,20 @@ export function guardVerifyCommand(options = {}) {
|
|
|
268
298
|
}
|
|
269
299
|
export function guardCheckCommand(options = {}) {
|
|
270
300
|
const root = getRoot();
|
|
271
|
-
const
|
|
301
|
+
const loaded = loadConfigSafely(root, { ...options, machine: options.json || options.sarif });
|
|
302
|
+
if (loaded.invalid)
|
|
303
|
+
return;
|
|
304
|
+
const config = loaded.config;
|
|
272
305
|
if (!config) {
|
|
273
306
|
if (options.json) {
|
|
274
|
-
console.log(JSON.stringify({
|
|
307
|
+
console.log(JSON.stringify({
|
|
308
|
+
status: 'error',
|
|
309
|
+
outcome: 'not-configured',
|
|
310
|
+
configured: false,
|
|
311
|
+
errorCode: 'GUARD_NOT_CONFIGURED',
|
|
312
|
+
message: 'Guard is not initialized.',
|
|
313
|
+
recoverable: true,
|
|
314
|
+
}));
|
|
275
315
|
process.exitCode = 1;
|
|
276
316
|
return;
|
|
277
317
|
}
|
|
@@ -284,7 +324,7 @@ export function guardCheckCommand(options = {}) {
|
|
|
284
324
|
baseline: loadBaseline(root),
|
|
285
325
|
includeArchitectureInsights: true,
|
|
286
326
|
});
|
|
287
|
-
const contractIssues =
|
|
327
|
+
const contractIssues = validateGuardPolicy(root, config);
|
|
288
328
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
289
329
|
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
290
330
|
const contractFindings = contractIssues.map((issue) => ({
|
|
@@ -328,10 +368,20 @@ export function guardCheckCommand(options = {}) {
|
|
|
328
368
|
}
|
|
329
369
|
export function guardAuditCommand(options = {}) {
|
|
330
370
|
const root = getRoot();
|
|
331
|
-
const
|
|
371
|
+
const loaded = loadConfigSafely(root, options);
|
|
372
|
+
if (loaded.invalid)
|
|
373
|
+
return;
|
|
374
|
+
const config = loaded.config;
|
|
332
375
|
if (!config) {
|
|
333
376
|
if (options.json) {
|
|
334
|
-
console.log(JSON.stringify({
|
|
377
|
+
console.log(JSON.stringify({
|
|
378
|
+
status: 'error',
|
|
379
|
+
outcome: 'not-configured',
|
|
380
|
+
configured: false,
|
|
381
|
+
errorCode: 'GUARD_NOT_CONFIGURED',
|
|
382
|
+
message: 'Guard is not initialized.',
|
|
383
|
+
recoverable: true,
|
|
384
|
+
}));
|
|
335
385
|
process.exitCode = 1;
|
|
336
386
|
return;
|
|
337
387
|
}
|
|
@@ -340,7 +390,7 @@ export function guardAuditCommand(options = {}) {
|
|
|
340
390
|
return;
|
|
341
391
|
}
|
|
342
392
|
const report = scanGuard(root, config, { includeArchitectureInsights: true });
|
|
343
|
-
const contractIssues =
|
|
393
|
+
const contractIssues = validateGuardPolicy(root, config);
|
|
344
394
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
345
395
|
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
346
396
|
if (options.json) {
|
|
@@ -388,9 +438,12 @@ export function guardBaselineCommand(action, ids, options = {}) {
|
|
|
388
438
|
process.exitCode = 0;
|
|
389
439
|
return;
|
|
390
440
|
}
|
|
441
|
+
const loaded = options.all ? loadConfigSafely(root, options) : { invalid: false };
|
|
442
|
+
if (loaded.invalid)
|
|
443
|
+
return;
|
|
391
444
|
const selected = options.all
|
|
392
445
|
? [
|
|
393
|
-
...scanGuard(root,
|
|
446
|
+
...scanGuard(root, loaded.config ?? buildGeneratedGuardConfig(discoverProject(root)), {
|
|
394
447
|
includeArchitectureInsights: true,
|
|
395
448
|
}).findings.map((finding) => finding.fingerprint),
|
|
396
449
|
]
|
|
@@ -415,7 +468,10 @@ export function guardBaselineCommand(action, ids, options = {}) {
|
|
|
415
468
|
}
|
|
416
469
|
export function guardRulesApproveCommand(ids, options = {}) {
|
|
417
470
|
const root = getRoot();
|
|
418
|
-
const
|
|
471
|
+
const loaded = loadConfigSafely(root);
|
|
472
|
+
if (loaded.invalid)
|
|
473
|
+
return;
|
|
474
|
+
const config = loaded.config;
|
|
419
475
|
if (!config) {
|
|
420
476
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
421
477
|
process.exitCode = 1;
|
|
@@ -433,18 +489,69 @@ export function guardRulesApproveCommand(ids, options = {}) {
|
|
|
433
489
|
return;
|
|
434
490
|
}
|
|
435
491
|
const selectedIds = new Set(selected.map((rule) => rule.id));
|
|
436
|
-
|
|
492
|
+
const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
|
|
493
|
+
if (approvalError) {
|
|
494
|
+
fail(approvalError);
|
|
495
|
+
process.exitCode = 1;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
applyGuardProposalDecision(root, {
|
|
437
499
|
...config,
|
|
438
500
|
rules: config.rules.map((rule) => selectedIds.has(rule.id) ? { ...rule, status: 'active' } : rule),
|
|
439
|
-
});
|
|
440
|
-
recordGuardProposalDecision(root, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })));
|
|
501
|
+
}, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })), { source: 'cli' });
|
|
441
502
|
for (const rule of selected)
|
|
442
503
|
success(`Activated ${rule.id}`);
|
|
443
504
|
process.exitCode = 0;
|
|
444
505
|
}
|
|
506
|
+
export function guardPolicyExplainCommand(id, options = {}) {
|
|
507
|
+
const root = getRoot();
|
|
508
|
+
const loaded = loadConfigSafely(root, options);
|
|
509
|
+
if (loaded.invalid)
|
|
510
|
+
return;
|
|
511
|
+
const config = loaded.config;
|
|
512
|
+
const proposals = loadGuardProposals(root);
|
|
513
|
+
if (!config) {
|
|
514
|
+
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
515
|
+
process.exitCode = 1;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const item = [...config.rules, ...(config.contracts ?? [])].find((entry) => entry.id === id);
|
|
519
|
+
const proposed = [...(proposals?.rules ?? []), ...(proposals?.contracts ?? [])].find((entry) => entry.id === id);
|
|
520
|
+
if (!item && !proposed) {
|
|
521
|
+
fail(`Policy item not found: ${id}`);
|
|
522
|
+
process.exitCode = 1;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const result = {
|
|
526
|
+
id,
|
|
527
|
+
active: item?.status !== 'proposed' && item !== undefined,
|
|
528
|
+
definition: item ?? proposed,
|
|
529
|
+
proposal: proposed,
|
|
530
|
+
decisions: proposals?.decisions?.filter((decision) => decision.id === id) ?? [],
|
|
531
|
+
};
|
|
532
|
+
if (options.json)
|
|
533
|
+
console.log(JSON.stringify(result, null, 2));
|
|
534
|
+
else {
|
|
535
|
+
heading(`Guard Policy: ${id}`);
|
|
536
|
+
info(`Status: ${result.active ? 'active' : 'proposed'}`);
|
|
537
|
+
const definition = result.definition;
|
|
538
|
+
if (definition && 'description' in definition)
|
|
539
|
+
dim(definition.description);
|
|
540
|
+
if (definition && 'statement' in definition)
|
|
541
|
+
dim(definition.statement);
|
|
542
|
+
if ((definition?.evidence?.length ?? 0) > 0)
|
|
543
|
+
dim(`Evidence: ${definition?.evidence?.join(', ')}`);
|
|
544
|
+
if (result.decisions.length > 0)
|
|
545
|
+
dim(`Decisions: ${result.decisions.length}`);
|
|
546
|
+
}
|
|
547
|
+
process.exitCode = 0;
|
|
548
|
+
}
|
|
445
549
|
export function guardContractsApproveCommand(ids, options = {}) {
|
|
446
550
|
const root = getRoot();
|
|
447
|
-
const
|
|
551
|
+
const loaded = loadConfigSafely(root);
|
|
552
|
+
if (loaded.invalid)
|
|
553
|
+
return;
|
|
554
|
+
const config = loaded.config;
|
|
448
555
|
if (!config) {
|
|
449
556
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
450
557
|
process.exitCode = 1;
|
|
@@ -462,22 +569,30 @@ export function guardContractsApproveCommand(ids, options = {}) {
|
|
|
462
569
|
return;
|
|
463
570
|
}
|
|
464
571
|
const selectedIds = new Set(selected.map((contract) => contract.id));
|
|
465
|
-
|
|
572
|
+
const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
|
|
573
|
+
if (approvalError) {
|
|
574
|
+
fail(approvalError);
|
|
575
|
+
process.exitCode = 1;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
applyGuardProposalDecision(root, {
|
|
466
579
|
...config,
|
|
467
580
|
contracts: (config.contracts ?? []).map((contract) => selectedIds.has(contract.id) ? { ...contract, status: 'active' } : contract),
|
|
468
|
-
})
|
|
469
|
-
recordGuardProposalDecision(root, selected.map((contract) => ({
|
|
581
|
+
}, selected.map((contract) => ({
|
|
470
582
|
id: contract.id,
|
|
471
583
|
type: 'contract',
|
|
472
584
|
decision: 'approved',
|
|
473
|
-
})));
|
|
585
|
+
})), { source: 'cli' });
|
|
474
586
|
for (const contract of selected)
|
|
475
587
|
success(`Activated ${contract.id}`);
|
|
476
588
|
process.exitCode = 0;
|
|
477
589
|
}
|
|
478
590
|
export function guardContractsRejectCommand(ids, options = {}) {
|
|
479
591
|
const root = getRoot();
|
|
480
|
-
const
|
|
592
|
+
const loaded = loadConfigSafely(root);
|
|
593
|
+
if (loaded.invalid)
|
|
594
|
+
return;
|
|
595
|
+
const config = loaded.config;
|
|
481
596
|
if (!config) {
|
|
482
597
|
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
483
598
|
process.exitCode = 1;
|
|
@@ -494,15 +609,20 @@ export function guardContractsRejectCommand(ids, options = {}) {
|
|
|
494
609
|
process.exitCode = 1;
|
|
495
610
|
return;
|
|
496
611
|
}
|
|
497
|
-
|
|
612
|
+
const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
|
|
613
|
+
if (approvalError) {
|
|
614
|
+
fail(approvalError);
|
|
615
|
+
process.exitCode = 1;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
applyGuardProposalDecision(root, {
|
|
498
619
|
...config,
|
|
499
620
|
contracts: config.contracts ?? [],
|
|
500
|
-
})
|
|
501
|
-
recordGuardProposalDecision(root, selected.map((contract) => ({
|
|
621
|
+
}, selected.map((contract) => ({
|
|
502
622
|
id: contract.id,
|
|
503
623
|
type: 'contract',
|
|
504
624
|
decision: 'rejected',
|
|
505
|
-
})));
|
|
625
|
+
})), { source: 'cli' });
|
|
506
626
|
for (const contract of selected)
|
|
507
627
|
success(`Rejected ${contract.id}`);
|
|
508
628
|
process.exitCode = 0;
|
|
@@ -512,7 +632,10 @@ export function guardReviewCommand(options = {}) {
|
|
|
512
632
|
const requirement = readRequirement(root, options.requirement);
|
|
513
633
|
if (options.requirement && !requirement)
|
|
514
634
|
return;
|
|
515
|
-
const
|
|
635
|
+
const loaded = loadConfigSafely(root);
|
|
636
|
+
if (loaded.invalid)
|
|
637
|
+
return;
|
|
638
|
+
const config = loaded.config;
|
|
516
639
|
if (!config) {
|
|
517
640
|
fail(`Guard is not initialized. Run \`codapult-guard init\` first.`);
|
|
518
641
|
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 { GuardBaselineReasonError, GuardConfigError, GuardStateBusyError, GuardStateStaleError, } 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')
|
|
@@ -14,11 +16,11 @@ const program = new Command()
|
|
|
14
16
|
// Guard is the standalone product, so its commands live at the package root:
|
|
15
17
|
// `codapult-guard init`, not `codapult-guard guard init`.
|
|
16
18
|
const guard = program;
|
|
17
|
-
guard.command('init').option('--force').action(guardInitCommand);
|
|
18
|
-
guard.command('analyze').action(guardAnalyzeCommand);
|
|
19
|
+
guard.command('init').option('--force').option('--no-wait').action(guardInitCommand);
|
|
20
|
+
guard.command('analyze').option('--refresh').option('--no-wait').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,36 @@ 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 ||
|
|
85
|
+
error instanceof GuardBaselineReasonError ||
|
|
86
|
+
error instanceof GuardStateBusyError ||
|
|
87
|
+
error instanceof GuardStateStaleError) {
|
|
88
|
+
const isBusy = error instanceof GuardStateBusyError;
|
|
89
|
+
const isStale = error instanceof GuardStateStaleError;
|
|
90
|
+
const isInvalidInput = error instanceof GuardBaselineReasonError;
|
|
91
|
+
console.error(JSON.stringify(guardErrorPayload(isBusy
|
|
92
|
+
? 'GUARD_STATE_BUSY'
|
|
93
|
+
: isStale
|
|
94
|
+
? 'GUARD_STATE_STALE'
|
|
95
|
+
: isInvalidInput
|
|
96
|
+
? 'GUARD_INVALID_INPUT'
|
|
97
|
+
: 'GUARD_CONFIG_INVALID', error.message, {
|
|
98
|
+
configured: !isBusy && !isStale,
|
|
99
|
+
outcome: 'error',
|
|
100
|
+
recoverable: true,
|
|
101
|
+
hint: isBusy || isStale
|
|
102
|
+
? 'Re-read Guard state and retry the operation.'
|
|
103
|
+
: isInvalidInput
|
|
104
|
+
? 'Provide a written reason for the baseline decision.'
|
|
105
|
+
: 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
|
|
106
|
+
}), null, 2));
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -7,6 +7,6 @@ export interface GuardDoctorReport {
|
|
|
7
7
|
status: 'ok' | 'warning' | 'fail';
|
|
8
8
|
initialized: boolean;
|
|
9
9
|
items: GuardDoctorItem[];
|
|
10
|
-
recommendation?: string;
|
|
10
|
+
recommendation?: string | undefined;
|
|
11
11
|
}
|
|
12
12
|
export declare function diagnoseGuard(root: string): GuardDoctorReport;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { GUARD_AGENT_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_BASELINE_FILE, GUARD_CONVENTIONS_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_RULES_FILE, loadGuardConfig, loadProjectModel, isGuardAgentConfig, isGuardContractsFile, isGuardProposalFile,
|
|
3
|
+
import { GUARD_AGENT_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_BASELINE_FILE, GUARD_CONVENTIONS_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_RULES_FILE, loadGuardConfig, loadProjectModel, isGuardAgentConfig, isGuardContractsFile, isGuardProposalFile, validateGuardPolicy, } from '../guard.js';
|
|
4
4
|
const requiredArtifacts = [
|
|
5
5
|
GUARD_BASELINE_FILE,
|
|
6
6
|
GUARD_RULES_FILE,
|
|
@@ -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,15 @@ 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 && validateGuardPolicy(root, guardConfig).length > 0;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
})()) {
|
|
44
57
|
return { path, status: 'invalid', message: 'Guard contract scopes or references are stale.' };
|
|
45
58
|
}
|
|
46
59
|
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> | undefined;
|
|
14
15
|
dynamicImports: string[];
|
|
15
16
|
declarations: {
|
|
16
17
|
classes: number;
|
|
@@ -32,18 +33,20 @@ export interface CapabilitySignal {
|
|
|
32
33
|
export interface ProjectModel {
|
|
33
34
|
version: 1;
|
|
34
35
|
project: {
|
|
35
|
-
name?: string;
|
|
36
|
-
packageManager?: string;
|
|
36
|
+
name?: string | undefined;
|
|
37
|
+
packageManager?: string | undefined;
|
|
37
38
|
frameworks: string[];
|
|
38
39
|
scripts: Record<string, string>;
|
|
39
40
|
dependencies: Record<string, string>;
|
|
40
41
|
devDependencies: Record<string, string>;
|
|
41
42
|
workspacePackages: {
|
|
42
43
|
path: string;
|
|
43
|
-
name?: string;
|
|
44
|
+
name?: string | undefined;
|
|
44
45
|
private: boolean;
|
|
45
46
|
scripts: Record<string, string>;
|
|
46
47
|
dependencies: string[];
|
|
48
|
+
exports?: string[] | undefined;
|
|
49
|
+
projectReferences?: string[] | undefined;
|
|
47
50
|
}[];
|
|
48
51
|
};
|
|
49
52
|
files: ProjectFileRecord[];
|
|
@@ -56,7 +59,7 @@ export interface ProjectModel {
|
|
|
56
59
|
};
|
|
57
60
|
git: {
|
|
58
61
|
repository: boolean;
|
|
59
|
-
branch?: string;
|
|
62
|
+
branch?: string | undefined;
|
|
60
63
|
dirty: boolean;
|
|
61
64
|
changedFiles: string[];
|
|
62
65
|
status: string[];
|
|
@@ -122,6 +125,24 @@ export interface DiscoveryMetrics {
|
|
|
122
125
|
changedFiles: number;
|
|
123
126
|
reusedModules: number;
|
|
124
127
|
}
|
|
128
|
+
export interface DiscoveryResult {
|
|
129
|
+
model: ProjectModel;
|
|
130
|
+
metrics: DiscoveryMetrics;
|
|
131
|
+
}
|
|
132
|
+
export interface DiscoveryOptions {
|
|
133
|
+
persistCache?: boolean | undefined;
|
|
134
|
+
/** Maximum number of files included in one model. */
|
|
135
|
+
maxFiles?: number | undefined;
|
|
136
|
+
/** Maximum size of one included file in bytes. */
|
|
137
|
+
maxFileBytes?: number | undefined;
|
|
138
|
+
}
|
|
139
|
+
export declare class DiscoveryLimitError extends Error {
|
|
140
|
+
constructor(message: string);
|
|
141
|
+
}
|
|
142
|
+
export declare class DiscoveryStaleError extends Error {
|
|
143
|
+
constructor();
|
|
144
|
+
}
|
|
145
|
+
export declare function clearDiscoveryCache(root: string): void;
|
|
125
146
|
export declare function findGuardRoot(from?: string): string;
|
|
126
147
|
/** Build the internal module graph from AST resolution plus conservative fallback resolution.
|
|
127
148
|
*
|
|
@@ -130,12 +151,5 @@ export declare function findGuardRoot(from?: string): string;
|
|
|
130
151
|
* no resolved imports silently drops unresolved edges from mixed projects.
|
|
131
152
|
*/
|
|
132
153
|
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
|
-
}): {
|
|
139
|
-
model: ProjectModel;
|
|
140
|
-
metrics: DiscoveryMetrics;
|
|
141
|
-
};
|
|
154
|
+
export declare function discoverProject(root: string, options?: DiscoveryOptions): ProjectModel;
|
|
155
|
+
export declare function discoverProjectWithMetrics(root: string, options?: DiscoveryOptions): DiscoveryResult;
|