@codapult/guard 0.1.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/adapters/agents/agent-integration.d.ts +9 -0
- package/dist/adapters/agents/agent-integration.js +62 -0
- package/dist/adapters/command.d.ts +21 -0
- package/dist/adapters/command.js +60 -0
- package/dist/adapters/project-checks.d.ts +37 -0
- package/dist/adapters/project-checks.js +149 -0
- package/dist/cli/commands/guard.d.ts +55 -0
- package/dist/cli/commands/guard.js +510 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +75 -0
- package/dist/cli/ui.d.ts +6 -0
- package/dist/cli/ui.js +7 -0
- package/dist/commands/guard.d.ts +2 -0
- package/dist/commands/guard.js +2 -0
- package/dist/core/analysis/doctor.d.ts +12 -0
- package/dist/core/analysis/doctor.js +85 -0
- package/dist/core/analysis/packs.d.ts +21 -0
- package/dist/core/analysis/packs.js +251 -0
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +6 -0
- package/dist/core/discovery/discovery.d.ts +134 -0
- package/dist/core/discovery/discovery.js +816 -0
- package/dist/core/guard.d.ts +189 -0
- package/dist/core/guard.js +936 -0
- package/dist/core/history/history.d.ts +24 -0
- package/dist/core/history/history.js +65 -0
- package/dist/core/output/sarif.d.ts +35 -0
- package/dist/core/output/sarif.js +29 -0
- package/dist/core/policy/schemas.d.ts +272 -0
- package/dist/core/policy/schemas.js +72 -0
- package/dist/core/verification/verify.d.ts +29 -0
- package/dist/core/verification/verify.js +77 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/mcp/prompts.d.ts +2 -0
- package/dist/mcp/prompts.js +54 -0
- package/dist/mcp/resources.d.ts +2 -0
- package/dist/mcp/resources.js +64 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +17 -0
- package/dist/mcp/tools/guard.d.ts +2 -0
- package/dist/mcp/tools/guard.js +375 -0
- package/package.json +117 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type GuardToolMode } from '../../core/guard.js';
|
|
2
|
+
export declare function guardInitCommand(options?: {
|
|
3
|
+
force?: boolean;
|
|
4
|
+
}): void;
|
|
5
|
+
export declare function guardAnalyzeCommand(): void;
|
|
6
|
+
export declare function guardProposeCommand(options?: {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}): void;
|
|
9
|
+
export declare function guardInstallAgentCommand(target?: string, options?: {
|
|
10
|
+
json?: boolean;
|
|
11
|
+
}): void;
|
|
12
|
+
export declare function guardDoctorCommand(options?: {
|
|
13
|
+
json?: boolean;
|
|
14
|
+
}): void;
|
|
15
|
+
export declare function guardHistoryCommand(): void;
|
|
16
|
+
export declare function guardHistoryDiffCommand(from: string, to: string, options?: {
|
|
17
|
+
json?: boolean;
|
|
18
|
+
}): void;
|
|
19
|
+
export declare function guardVerifyCommand(options?: {
|
|
20
|
+
checks?: string;
|
|
21
|
+
changed?: boolean;
|
|
22
|
+
json?: boolean;
|
|
23
|
+
requirement?: string;
|
|
24
|
+
tools?: GuardToolMode;
|
|
25
|
+
strict?: boolean;
|
|
26
|
+
projectChecks?: boolean;
|
|
27
|
+
}): void;
|
|
28
|
+
export declare function guardCheckCommand(options?: {
|
|
29
|
+
changed?: boolean;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
sarif?: boolean;
|
|
32
|
+
}): void;
|
|
33
|
+
export declare function guardAuditCommand(options?: {
|
|
34
|
+
json?: boolean;
|
|
35
|
+
}): void;
|
|
36
|
+
export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: {
|
|
37
|
+
all?: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
}): void;
|
|
41
|
+
export declare function guardRulesApproveCommand(ids?: string, options?: {
|
|
42
|
+
all?: boolean;
|
|
43
|
+
}): void;
|
|
44
|
+
export declare function guardContractsApproveCommand(ids?: string, options?: {
|
|
45
|
+
all?: boolean;
|
|
46
|
+
}): void;
|
|
47
|
+
export declare function guardContractsRejectCommand(ids?: string, options?: {
|
|
48
|
+
all?: boolean;
|
|
49
|
+
}): void;
|
|
50
|
+
export declare function guardReviewCommand(options?: {
|
|
51
|
+
maxDiffChars?: string;
|
|
52
|
+
requirement?: string;
|
|
53
|
+
base?: string;
|
|
54
|
+
}): void;
|
|
55
|
+
export declare function guardIsInitialized(root: string): boolean;
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
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';
|
|
4
|
+
import { runGuardVerification, } from '../../core/verification/verify.js';
|
|
5
|
+
import { diagnoseGuard } from '../../core/analysis/doctor.js';
|
|
6
|
+
import { diffGuardSnapshots, listGuardSnapshots } from '../../core/history/history.js';
|
|
7
|
+
import { guardAgentTargets, installGuardAgentInstructions, installGuardAgentTargets, } from '../../adapters/agents/agent-integration.js';
|
|
8
|
+
import { dim, fail, heading, info, success, warn } from '../ui.js';
|
|
9
|
+
import { guardFindingsToSarif } from '../../core/output/sarif.js';
|
|
10
|
+
function renderFindings(findings) {
|
|
11
|
+
for (const finding of findings) {
|
|
12
|
+
const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
|
|
13
|
+
printer(`${finding.file}:${finding.line} [${finding.ruleId}] ${finding.message}`);
|
|
14
|
+
dim(` import: ${finding.importPath}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getRoot() {
|
|
18
|
+
return findGuardRoot();
|
|
19
|
+
}
|
|
20
|
+
function readRequirement(root, file) {
|
|
21
|
+
if (!file)
|
|
22
|
+
return undefined;
|
|
23
|
+
const path = resolve(root, file);
|
|
24
|
+
const relativePath = relative(root, path);
|
|
25
|
+
if (relativePath.split(/[\\/]/).includes('..')) {
|
|
26
|
+
fail(`--requirement ${file}: file must be inside the project root`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const value = readFileSync(path, 'utf8').trim();
|
|
32
|
+
if (!value)
|
|
33
|
+
throw new Error('file is empty');
|
|
34
|
+
if (value.length > 20_000)
|
|
35
|
+
throw new Error('file exceeds the 20,000 character limit');
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : 'cannot read file';
|
|
40
|
+
fail(`--requirement ${file}: ${message}`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function guardInitCommand(options = {}) {
|
|
46
|
+
const root = getRoot();
|
|
47
|
+
let initialized;
|
|
48
|
+
try {
|
|
49
|
+
initialized = initializeGuard(root, { force: options.force });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof GuardAlreadyInitializedError) {
|
|
53
|
+
fail(`${error.message} Use \`codapult-guard init --force\` to replace it.`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
fail(error instanceof Error ? error.message : 'Guard initialization failed.');
|
|
57
|
+
}
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const { config, report } = initialized;
|
|
62
|
+
heading('Codapult Guard Init');
|
|
63
|
+
success(`Created ${GUARD_RULES_FILE}`);
|
|
64
|
+
success(`Created ${GUARD_ARCHITECTURE_FILE}`);
|
|
65
|
+
success(`Created ${GUARD_CONVENTIONS_FILE}`);
|
|
66
|
+
success(`Created ${GUARD_BASELINE_FILE}`);
|
|
67
|
+
success(`Created ${GUARD_BASELINE_META_FILE}`);
|
|
68
|
+
success(`Created ${GUARD_PROJECT_FILE}`);
|
|
69
|
+
success(`Created ${GUARD_AGENT_FILE}`);
|
|
70
|
+
success(`Created ${GUARD_CONTRACTS_FILE}`);
|
|
71
|
+
success(`Created ${GUARD_PROPOSALS_FILE}`);
|
|
72
|
+
const proposedRules = config.rules.filter((rule) => rule.status === 'proposed').length;
|
|
73
|
+
info(`${config.rules.length} rule(s) generated (${proposedRules} proposed); ${report.scannedFiles} source file(s) scanned.`);
|
|
74
|
+
dim('Existing findings are baselined. New violations will be reported by `codapult-guard check`.');
|
|
75
|
+
}
|
|
76
|
+
export function guardAnalyzeCommand() {
|
|
77
|
+
const root = getRoot();
|
|
78
|
+
const { model: projectModel, metrics } = discoverProjectWithMetrics(root, {
|
|
79
|
+
persistCache: true,
|
|
80
|
+
});
|
|
81
|
+
writeProjectModel(root, projectModel);
|
|
82
|
+
writeGuardMemory(root, projectModel);
|
|
83
|
+
const revision = writeProjectSnapshot(root, projectModel);
|
|
84
|
+
heading('Codapult Guard Analyze');
|
|
85
|
+
success(`Updated ${GUARD_PROJECT_FILE}, ${GUARD_ARCHITECTURE_FILE}, and ${GUARD_CONVENTIONS_FILE}`);
|
|
86
|
+
info(`${projectModel.files.length} file(s), ${projectModel.modules.length} module(s), ${projectModel.insights.cycles.length} cycle(s) recorded as project context.`);
|
|
87
|
+
dim(`Discovery: ${metrics.durationMs} ms; cache ${metrics.cacheHit ? 'hit' : metrics.cacheAvailable ? 'available' : 'cold'}; ${metrics.modules} module(s), ${metrics.changedFiles} changed file(s).`);
|
|
88
|
+
dim(`Snapshot: ${GUARD_HISTORY_DIR}/${revision}.json`);
|
|
89
|
+
}
|
|
90
|
+
export function guardProposeCommand(options = {}) {
|
|
91
|
+
const root = getRoot();
|
|
92
|
+
const model = discoverProject(root);
|
|
93
|
+
const config = loadGuardConfig(root) ?? buildGeneratedGuardConfig(model);
|
|
94
|
+
const proposals = buildGuardProposals(model, config);
|
|
95
|
+
const previous = loadGuardProposals(root);
|
|
96
|
+
writeGuardProposals(root, {
|
|
97
|
+
...proposals,
|
|
98
|
+
...(previous?.decisions ? { decisions: previous.decisions } : {}),
|
|
99
|
+
});
|
|
100
|
+
const result = {
|
|
101
|
+
...proposals,
|
|
102
|
+
freshness: getGuardProposalFreshness(model, proposals),
|
|
103
|
+
instructions: 'Give this proposal packet to the host AI for evidence-based contract refinement. Do not activate proposals without review.',
|
|
104
|
+
};
|
|
105
|
+
if (options.json)
|
|
106
|
+
console.log(JSON.stringify(result, null, 2));
|
|
107
|
+
else {
|
|
108
|
+
heading('Codapult Guard Proposals');
|
|
109
|
+
info(`${proposals.rules.length} rule proposal(s), ${proposals.contracts.length} contract proposal(s).`);
|
|
110
|
+
for (const question of proposals.questions)
|
|
111
|
+
warn(question);
|
|
112
|
+
dim(`Saved ${GUARD_PROPOSALS_FILE}`);
|
|
113
|
+
}
|
|
114
|
+
process.exitCode = 0;
|
|
115
|
+
}
|
|
116
|
+
export function guardInstallAgentCommand(target = 'generic', options = {}) {
|
|
117
|
+
const normalized = target.toLowerCase();
|
|
118
|
+
const targets = normalized === 'all' ? guardAgentTargets : [normalized];
|
|
119
|
+
if (!targets.every((item) => guardAgentTargets.includes(item))) {
|
|
120
|
+
fail(`Unknown agent target. Choose one of: ${guardAgentTargets.join(', ')}, all.`);
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const results = targets.length === 1
|
|
125
|
+
? [installGuardAgentInstructions(getRoot(), targets.at(0) ?? 'generic')]
|
|
126
|
+
: installGuardAgentTargets(getRoot(), targets);
|
|
127
|
+
if (options.json)
|
|
128
|
+
console.log(JSON.stringify(results, null, 2));
|
|
129
|
+
else {
|
|
130
|
+
heading('Codapult Guard Agent Integration');
|
|
131
|
+
for (const result of results)
|
|
132
|
+
success(`${result.action} ${result.path}`);
|
|
133
|
+
}
|
|
134
|
+
process.exitCode = 0;
|
|
135
|
+
}
|
|
136
|
+
export function guardDoctorCommand(options = {}) {
|
|
137
|
+
const report = diagnoseGuard(getRoot());
|
|
138
|
+
if (options.json) {
|
|
139
|
+
console.log(JSON.stringify(report, null, 2));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
heading('Codapult Guard Doctor');
|
|
143
|
+
for (const item of report.items) {
|
|
144
|
+
if (item.status === 'ok')
|
|
145
|
+
success(`${item.path}: ok`);
|
|
146
|
+
else if (item.status === 'missing')
|
|
147
|
+
warn(`${item.path}: missing`);
|
|
148
|
+
else
|
|
149
|
+
fail(`${item.path}: invalid`);
|
|
150
|
+
}
|
|
151
|
+
if (report.recommendation)
|
|
152
|
+
dim(report.recommendation);
|
|
153
|
+
else
|
|
154
|
+
success('Guard state is healthy.');
|
|
155
|
+
}
|
|
156
|
+
process.exitCode = report.status === 'fail' ? 1 : 0;
|
|
157
|
+
}
|
|
158
|
+
export function guardHistoryCommand() {
|
|
159
|
+
const snapshots = listGuardSnapshots(getRoot());
|
|
160
|
+
heading('Codapult Guard History');
|
|
161
|
+
if (snapshots.length === 0) {
|
|
162
|
+
info('No Guard snapshots found. Run `codapult-guard analyze`.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const snapshot of snapshots) {
|
|
166
|
+
info(`${snapshot.revision}: ${snapshot.files} files, ${snapshot.modules} modules, ${snapshot.cycles} cycles`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function guardHistoryDiffCommand(from, to, options = {}) {
|
|
170
|
+
const diff = diffGuardSnapshots(getRoot(), from, to);
|
|
171
|
+
if (!diff) {
|
|
172
|
+
fail(`Could not load snapshots '${from}' and '${to}'.`);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (options.json)
|
|
177
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
178
|
+
else {
|
|
179
|
+
heading(`Guard Snapshot Diff: ${from} → ${to}`);
|
|
180
|
+
info(`Added files: ${diff.addedFiles.length}`);
|
|
181
|
+
info(`Removed files: ${diff.removedFiles.length}`);
|
|
182
|
+
info(`Added dependencies: ${diff.addedDependencies.join(', ') || 'none'}`);
|
|
183
|
+
info(`Removed dependencies: ${diff.removedDependencies.join(', ') || 'none'}`);
|
|
184
|
+
info(`Capabilities: +${diff.addedCapabilities.join(', ') || 'none'} / -${diff.removedCapabilities.join(', ') || 'none'}`);
|
|
185
|
+
info(`Cycles: ${diff.cycles.from} → ${diff.cycles.to}`);
|
|
186
|
+
}
|
|
187
|
+
process.exitCode = 0;
|
|
188
|
+
}
|
|
189
|
+
export function guardVerifyCommand(options = {}) {
|
|
190
|
+
const root = getRoot();
|
|
191
|
+
const requirement = readRequirement(root, options.requirement);
|
|
192
|
+
if (options.requirement && !requirement)
|
|
193
|
+
return;
|
|
194
|
+
const checks = options.checks
|
|
195
|
+
?.split(',')
|
|
196
|
+
.map((check) => check.trim())
|
|
197
|
+
.filter((check) => ['lint', 'typecheck', 'test', 'build'].includes(check));
|
|
198
|
+
const result = runGuardVerification(root, {
|
|
199
|
+
checks,
|
|
200
|
+
changedOnly: options.changed,
|
|
201
|
+
requirement,
|
|
202
|
+
tools: options.tools,
|
|
203
|
+
strict: options.strict,
|
|
204
|
+
projectChecks: options.projectChecks,
|
|
205
|
+
});
|
|
206
|
+
if (options.json) {
|
|
207
|
+
console.log(JSON.stringify(result, null, 2));
|
|
208
|
+
process.exitCode = result.status === 'ok' ? 0 : 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
heading('Codapult Guard Verify');
|
|
212
|
+
if (result.status === 'not-configured') {
|
|
213
|
+
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const [name, check] of Object.entries(result.checks)) {
|
|
218
|
+
if (check.status === 'passed')
|
|
219
|
+
success(`${name}: passed`);
|
|
220
|
+
else if (check.status === 'not-configured')
|
|
221
|
+
dim(`${name}: not configured`);
|
|
222
|
+
else
|
|
223
|
+
fail(`${name}: failed (exit ${check.exitCode})`);
|
|
224
|
+
}
|
|
225
|
+
for (const [name, check] of Object.entries(result.adapters ?? {})) {
|
|
226
|
+
if (check.status === 'passed')
|
|
227
|
+
success(`${name}: passed`);
|
|
228
|
+
else if (check.status === 'not-configured')
|
|
229
|
+
dim(`${name}: not configured`);
|
|
230
|
+
else
|
|
231
|
+
fail(`${name}: failed (exit ${check.exitCode})`);
|
|
232
|
+
}
|
|
233
|
+
const findings = result.architecture?.findings ?? [];
|
|
234
|
+
if (findings.length === 0)
|
|
235
|
+
success('architecture: no new regressions');
|
|
236
|
+
else
|
|
237
|
+
renderFindings(findings);
|
|
238
|
+
for (const issue of result.contractIssues)
|
|
239
|
+
warn(`[${issue.contractId}] ${issue.message}`);
|
|
240
|
+
if (result.contractIssues.length > 0)
|
|
241
|
+
fail(`contracts: ${result.contractIssues.length} invalid`);
|
|
242
|
+
dim(`requirements: ${result.requirement.message}`);
|
|
243
|
+
process.exitCode = result.status === 'ok' ? 0 : 1;
|
|
244
|
+
}
|
|
245
|
+
export function guardCheckCommand(options = {}) {
|
|
246
|
+
const root = getRoot();
|
|
247
|
+
const config = loadGuardConfig(root);
|
|
248
|
+
if (!config) {
|
|
249
|
+
if (options.json) {
|
|
250
|
+
console.log(JSON.stringify({ configured: false, error: 'Guard is not initialized' }));
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
fail(`Guard is not initialized. Run \`codapult-guard init\` first.`);
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const report = scanGuard(root, config, {
|
|
259
|
+
changedOnly: options.changed,
|
|
260
|
+
baseline: loadBaseline(root),
|
|
261
|
+
includeArchitectureInsights: true,
|
|
262
|
+
});
|
|
263
|
+
const contractIssues = validateGuardContracts(root, config.contracts ?? []);
|
|
264
|
+
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
265
|
+
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
266
|
+
const contractFindings = contractIssues.map((issue) => ({
|
|
267
|
+
ruleId: `contract:${issue.contractId}`,
|
|
268
|
+
severity: 'error',
|
|
269
|
+
file: issue.value,
|
|
270
|
+
line: 1,
|
|
271
|
+
importPath: 'contract',
|
|
272
|
+
message: issue.message,
|
|
273
|
+
fingerprint: `contract-invalid|${issue.contractId}|${issue.field}|${issue.value}`,
|
|
274
|
+
}));
|
|
275
|
+
if (options.sarif) {
|
|
276
|
+
console.log(JSON.stringify(guardFindingsToSarif([...report.findings, ...contractFindings]), null, 2));
|
|
277
|
+
process.exitCode = errors > 0 || contractIssues.length > 0 ? 1 : 0;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (options.json) {
|
|
281
|
+
console.log(JSON.stringify({
|
|
282
|
+
status: errors > 0 || contractIssues.length > 0 ? 'fail' : warnings > 0 ? 'warning' : 'ok',
|
|
283
|
+
outcome: classifyGuardOutcome({ errors: errors + contractIssues.length, warnings }),
|
|
284
|
+
errors,
|
|
285
|
+
warnings,
|
|
286
|
+
contractIssues,
|
|
287
|
+
report,
|
|
288
|
+
}, null, 2));
|
|
289
|
+
process.exitCode = errors > 0 || contractIssues.length > 0 ? 1 : 0;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
heading(`Codapult Guard${options.changed ? ' (changed files)' : ''}`);
|
|
293
|
+
dim(`Scanned ${report.scannedFiles} source file(s); suppressed ${report.suppressed} baseline finding(s).`);
|
|
294
|
+
renderFindings(report.findings);
|
|
295
|
+
for (const issue of contractIssues)
|
|
296
|
+
warn(`[${issue.contractId}] ${issue.message}`);
|
|
297
|
+
if (errors > 0 || contractIssues.length > 0)
|
|
298
|
+
fail(`${errors + contractIssues.length} issue(s) found`);
|
|
299
|
+
else if (warnings > 0)
|
|
300
|
+
warn(`${warnings} warning(s) found`);
|
|
301
|
+
else
|
|
302
|
+
success('No new guard violations found.');
|
|
303
|
+
process.exitCode = errors > 0 || contractIssues.length > 0 ? 1 : 0;
|
|
304
|
+
}
|
|
305
|
+
export function guardAuditCommand(options = {}) {
|
|
306
|
+
const root = getRoot();
|
|
307
|
+
const config = loadGuardConfig(root);
|
|
308
|
+
if (!config) {
|
|
309
|
+
if (options.json) {
|
|
310
|
+
console.log(JSON.stringify({ configured: false, error: 'Guard is not initialized' }));
|
|
311
|
+
process.exitCode = 1;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
fail(`Guard is not initialized. Run \`codapult-guard init\` first.`);
|
|
315
|
+
process.exitCode = 1;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const report = scanGuard(root, config, { includeArchitectureInsights: true });
|
|
319
|
+
const contractIssues = validateGuardContracts(root, config.contracts ?? []);
|
|
320
|
+
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
321
|
+
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
322
|
+
if (options.json) {
|
|
323
|
+
console.log(JSON.stringify({
|
|
324
|
+
status: errors > 0 || contractIssues.length > 0 ? 'fail' : warnings > 0 ? 'warning' : 'ok',
|
|
325
|
+
errors,
|
|
326
|
+
warnings,
|
|
327
|
+
outcome: classifyGuardOutcome({
|
|
328
|
+
errors: errors + contractIssues.length,
|
|
329
|
+
warnings,
|
|
330
|
+
}),
|
|
331
|
+
contractIssues,
|
|
332
|
+
report,
|
|
333
|
+
}, null, 2));
|
|
334
|
+
process.exitCode = errors > 0 || contractIssues.length > 0 ? 1 : 0;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
heading('Codapult Guard Audit');
|
|
338
|
+
dim(`Scanned ${report.scannedFiles} source file(s); baseline is ignored for this full audit.`);
|
|
339
|
+
renderFindings(report.findings);
|
|
340
|
+
for (const issue of contractIssues)
|
|
341
|
+
warn(`[${issue.contractId}] ${issue.message}`);
|
|
342
|
+
if (errors > 0)
|
|
343
|
+
fail(`${errors} error(s) found`);
|
|
344
|
+
else if (warnings > 0 || contractIssues.length > 0) {
|
|
345
|
+
warn(`${warnings + contractIssues.length} warning(s) found`);
|
|
346
|
+
}
|
|
347
|
+
else
|
|
348
|
+
success('No Guard issues found.');
|
|
349
|
+
process.exitCode = errors > 0 || contractIssues.length > 0 ? 1 : 0;
|
|
350
|
+
}
|
|
351
|
+
export function guardBaselineCommand(action, ids, options = {}) {
|
|
352
|
+
const root = getRoot();
|
|
353
|
+
const baseline = loadBaseline(root);
|
|
354
|
+
if (action === 'list') {
|
|
355
|
+
const result = { count: baseline.size, fingerprints: [...baseline].sort() };
|
|
356
|
+
if (options.json)
|
|
357
|
+
console.log(JSON.stringify(result, null, 2));
|
|
358
|
+
else {
|
|
359
|
+
heading('Codapult Guard Baseline');
|
|
360
|
+
info(`${result.count} fingerprint(s)`);
|
|
361
|
+
for (const fingerprint of result.fingerprints)
|
|
362
|
+
dim(fingerprint);
|
|
363
|
+
}
|
|
364
|
+
process.exitCode = 0;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const selected = options.all
|
|
368
|
+
? [
|
|
369
|
+
...scanGuard(root, loadGuardConfig(root) ?? buildGeneratedGuardConfig(discoverProject(root)), {
|
|
370
|
+
includeArchitectureInsights: true,
|
|
371
|
+
}).findings.map((finding) => finding.fingerprint),
|
|
372
|
+
]
|
|
373
|
+
: (ids ?? '')
|
|
374
|
+
.split(',')
|
|
375
|
+
.map((id) => id.trim())
|
|
376
|
+
.filter(Boolean);
|
|
377
|
+
if (selected.length === 0) {
|
|
378
|
+
fail(`Specify fingerprints or use --all to ${action} findings.`);
|
|
379
|
+
process.exitCode = 1;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const next = updateBaseline(root, action === 'accept'
|
|
383
|
+
? { add: selected, reason: options.reason }
|
|
384
|
+
: { remove: selected, reason: options.reason });
|
|
385
|
+
const result = { action, changed: selected.length, count: next.size, fingerprints: selected };
|
|
386
|
+
if (options.json)
|
|
387
|
+
console.log(JSON.stringify(result, null, 2));
|
|
388
|
+
else
|
|
389
|
+
success(`${action === 'accept' ? 'Accepted' : 'Removed'} ${selected.length} baseline fingerprint(s).`);
|
|
390
|
+
process.exitCode = 0;
|
|
391
|
+
}
|
|
392
|
+
export function guardRulesApproveCommand(ids, options = {}) {
|
|
393
|
+
const root = getRoot();
|
|
394
|
+
const config = loadGuardConfig(root);
|
|
395
|
+
if (!config) {
|
|
396
|
+
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const requested = new Set(ids
|
|
401
|
+
?.split(',')
|
|
402
|
+
.map((id) => id.trim())
|
|
403
|
+
.filter(Boolean));
|
|
404
|
+
const proposed = config.rules.filter((rule) => rule.status === 'proposed');
|
|
405
|
+
const selected = proposed.filter((rule) => options.all || requested.has(rule.id));
|
|
406
|
+
if (!options.all && selected.length === 0) {
|
|
407
|
+
fail('Specify proposed rule IDs or use `codapult-guard rules approve --all`.');
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const selectedIds = new Set(selected.map((rule) => rule.id));
|
|
412
|
+
writeGuardConfig(root, {
|
|
413
|
+
...config,
|
|
414
|
+
rules: config.rules.map((rule) => selectedIds.has(rule.id) ? { ...rule, status: 'active' } : rule),
|
|
415
|
+
});
|
|
416
|
+
recordGuardProposalDecision(root, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })));
|
|
417
|
+
for (const rule of selected)
|
|
418
|
+
success(`Activated ${rule.id}`);
|
|
419
|
+
process.exitCode = 0;
|
|
420
|
+
}
|
|
421
|
+
export function guardContractsApproveCommand(ids, options = {}) {
|
|
422
|
+
const root = getRoot();
|
|
423
|
+
const config = loadGuardConfig(root);
|
|
424
|
+
if (!config) {
|
|
425
|
+
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const requested = new Set(ids
|
|
430
|
+
?.split(',')
|
|
431
|
+
.map((id) => id.trim())
|
|
432
|
+
.filter(Boolean));
|
|
433
|
+
const proposed = (config.contracts ?? []).filter((contract) => contract.status === 'proposed');
|
|
434
|
+
const selected = proposed.filter((contract) => options.all || requested.has(contract.id));
|
|
435
|
+
if (!options.all && selected.length === 0) {
|
|
436
|
+
fail('Specify proposed contract IDs or use `codapult-guard contracts approve --all`.');
|
|
437
|
+
process.exitCode = 1;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const selectedIds = new Set(selected.map((contract) => contract.id));
|
|
441
|
+
writeGuardConfig(root, {
|
|
442
|
+
...config,
|
|
443
|
+
contracts: (config.contracts ?? []).map((contract) => selectedIds.has(contract.id) ? { ...contract, status: 'active' } : contract),
|
|
444
|
+
});
|
|
445
|
+
recordGuardProposalDecision(root, selected.map((contract) => ({
|
|
446
|
+
id: contract.id,
|
|
447
|
+
type: 'contract',
|
|
448
|
+
decision: 'approved',
|
|
449
|
+
})));
|
|
450
|
+
for (const contract of selected)
|
|
451
|
+
success(`Activated ${contract.id}`);
|
|
452
|
+
process.exitCode = 0;
|
|
453
|
+
}
|
|
454
|
+
export function guardContractsRejectCommand(ids, options = {}) {
|
|
455
|
+
const root = getRoot();
|
|
456
|
+
const config = loadGuardConfig(root);
|
|
457
|
+
if (!config) {
|
|
458
|
+
fail('Guard is not initialized. Run `codapult-guard init` first.');
|
|
459
|
+
process.exitCode = 1;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const requested = new Set(ids
|
|
463
|
+
?.split(',')
|
|
464
|
+
.map((id) => id.trim())
|
|
465
|
+
.filter(Boolean));
|
|
466
|
+
const proposed = (config.contracts ?? []).filter((contract) => contract.status === 'proposed');
|
|
467
|
+
const selected = proposed.filter((contract) => options.all || requested.has(contract.id));
|
|
468
|
+
if (!options.all && selected.length === 0) {
|
|
469
|
+
fail('Specify proposed contract IDs or use `codapult-guard contracts reject --all`.');
|
|
470
|
+
process.exitCode = 1;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
writeGuardConfig(root, {
|
|
474
|
+
...config,
|
|
475
|
+
contracts: config.contracts ?? [],
|
|
476
|
+
});
|
|
477
|
+
recordGuardProposalDecision(root, selected.map((contract) => ({
|
|
478
|
+
id: contract.id,
|
|
479
|
+
type: 'contract',
|
|
480
|
+
decision: 'rejected',
|
|
481
|
+
})));
|
|
482
|
+
for (const contract of selected)
|
|
483
|
+
success(`Rejected ${contract.id}`);
|
|
484
|
+
process.exitCode = 0;
|
|
485
|
+
}
|
|
486
|
+
export function guardReviewCommand(options = {}) {
|
|
487
|
+
const root = getRoot();
|
|
488
|
+
const requirement = readRequirement(root, options.requirement);
|
|
489
|
+
if (options.requirement && !requirement)
|
|
490
|
+
return;
|
|
491
|
+
const config = loadGuardConfig(root);
|
|
492
|
+
if (!config) {
|
|
493
|
+
fail(`Guard is not initialized. Run \`codapult-guard init\` first.`);
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const maxDiffChars = options.maxDiffChars ? Number(options.maxDiffChars) : 120_000;
|
|
498
|
+
if (!Number.isInteger(maxDiffChars) || maxDiffChars < 1) {
|
|
499
|
+
fail('--max-diff-chars must be a positive integer.');
|
|
500
|
+
process.exitCode = 1;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const packet = buildGuardReviewPacket(root, config, loadBaseline(root), maxDiffChars, true, requirement, options.base);
|
|
504
|
+
console.log(JSON.stringify(packet, null, 2));
|
|
505
|
+
if (packet.diffError)
|
|
506
|
+
process.exitCode = 1;
|
|
507
|
+
}
|
|
508
|
+
export function guardIsInitialized(root) {
|
|
509
|
+
return existsSync(resolve(root, GUARD_RULES_FILE));
|
|
510
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
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';
|
|
5
|
+
import { config } from '../core/config.js';
|
|
6
|
+
const program = new Command()
|
|
7
|
+
.name(config.commandName)
|
|
8
|
+
.description('Local-first architecture guardrails for JavaScript and TypeScript projects')
|
|
9
|
+
.configureHelp({
|
|
10
|
+
styleTitle: (str) => pc.bold(pc.cyan(str)),
|
|
11
|
+
styleCommandText: (str) => pc.yellow(str),
|
|
12
|
+
styleOptionText: (str) => pc.green(str),
|
|
13
|
+
});
|
|
14
|
+
// Guard is the standalone product, so its commands live at the package root:
|
|
15
|
+
// `codapult-guard init`, not `codapult-guard guard init`.
|
|
16
|
+
const guard = program;
|
|
17
|
+
guard.command('init').option('--force').action(guardInitCommand);
|
|
18
|
+
guard.command('analyze').action(guardAnalyzeCommand);
|
|
19
|
+
guard.command('propose').option('--json').action(guardProposeCommand);
|
|
20
|
+
guard.command('install-agent [target]').option('--json').action(guardInstallAgentCommand);
|
|
21
|
+
guard.command('doctor').option('--json').action(guardDoctorCommand);
|
|
22
|
+
guard.command('history').action(guardHistoryCommand);
|
|
23
|
+
guard.command('history-diff <from> <to>').option('--json').action(guardHistoryDiffCommand);
|
|
24
|
+
guard
|
|
25
|
+
.command('check')
|
|
26
|
+
.option('--changed')
|
|
27
|
+
.option('--json')
|
|
28
|
+
.option('--sarif')
|
|
29
|
+
.action(guardCheckCommand);
|
|
30
|
+
guard.command('audit').option('--json').action(guardAuditCommand);
|
|
31
|
+
guard
|
|
32
|
+
.command('verify')
|
|
33
|
+
.option('--checks <list>')
|
|
34
|
+
.option('--tools <mode>', 'external tools: auto, on, or off', 'auto')
|
|
35
|
+
.option('--strict')
|
|
36
|
+
.option('--no-project-checks')
|
|
37
|
+
.option('--requirement <file>')
|
|
38
|
+
.option('--changed')
|
|
39
|
+
.option('--json')
|
|
40
|
+
.action(guardVerifyCommand);
|
|
41
|
+
guard
|
|
42
|
+
.command('review')
|
|
43
|
+
.option('--max-diff-chars <number>', undefined, '120000')
|
|
44
|
+
.option('--base <ref>')
|
|
45
|
+
.option('--requirement <file>')
|
|
46
|
+
.action(guardReviewCommand);
|
|
47
|
+
const rules = guard.command('rules');
|
|
48
|
+
rules.command('approve [ids]').option('--all').action(guardRulesApproveCommand);
|
|
49
|
+
const contracts = guard.command('contracts');
|
|
50
|
+
contracts.command('approve [ids]').option('--all').action(guardContractsApproveCommand);
|
|
51
|
+
contracts.command('reject [ids]').option('--all').action(guardContractsRejectCommand);
|
|
52
|
+
const baseline = guard.command('baseline');
|
|
53
|
+
baseline
|
|
54
|
+
.command('list')
|
|
55
|
+
.option('--json')
|
|
56
|
+
.action((options) => guardBaselineCommand('list', undefined, options));
|
|
57
|
+
baseline
|
|
58
|
+
.command('accept [ids]')
|
|
59
|
+
.option('--all')
|
|
60
|
+
.option('--reason <text>')
|
|
61
|
+
.option('--json')
|
|
62
|
+
.action((ids, options) => guardBaselineCommand('accept', ids, options));
|
|
63
|
+
baseline
|
|
64
|
+
.command('remove [ids]')
|
|
65
|
+
.option('--all')
|
|
66
|
+
.option('--reason <text>')
|
|
67
|
+
.option('--json')
|
|
68
|
+
.action((ids, options) => guardBaselineCommand('remove', ids, options));
|
|
69
|
+
program
|
|
70
|
+
.command('mcp-server')
|
|
71
|
+
.description('start the Guard MCP server over stdio')
|
|
72
|
+
.action(async () => {
|
|
73
|
+
await import('../mcp/server.js');
|
|
74
|
+
});
|
|
75
|
+
program.parse();
|
package/dist/cli/ui.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const heading: (value: string) => void;
|
|
2
|
+
export declare const success: (value: string) => void;
|
|
3
|
+
export declare const info: (value: string) => void;
|
|
4
|
+
export declare const warn: (value: string) => void;
|
|
5
|
+
export declare const fail: (value: string) => void;
|
|
6
|
+
export declare const dim: (value: string) => void;
|
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export const heading = (value) => console.log(`\n${pc.bold(pc.cyan(value))}\n`);
|
|
3
|
+
export const success = (value) => console.log(pc.green(`✓ ${value}`));
|
|
4
|
+
export const info = (value) => console.log(pc.cyan(`ℹ ${value}`));
|
|
5
|
+
export const warn = (value) => console.warn(pc.yellow(`⚠ ${value}`));
|
|
6
|
+
export const fail = (value) => console.error(pc.red(`✗ ${value}`));
|
|
7
|
+
export const dim = (value) => console.log(pc.dim(value));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface GuardDoctorItem {
|
|
2
|
+
path: string;
|
|
3
|
+
status: 'ok' | 'missing' | 'invalid';
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GuardDoctorReport {
|
|
7
|
+
status: 'ok' | 'warning' | 'fail';
|
|
8
|
+
initialized: boolean;
|
|
9
|
+
items: GuardDoctorItem[];
|
|
10
|
+
recommendation?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function diagnoseGuard(root: string): GuardDoctorReport;
|