@codapult/guard 0.3.0 → 0.5.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 +58 -1
- package/dist/adapters/command.d.ts +11 -8
- package/dist/adapters/command.js +4 -0
- package/dist/adapters/project-checks.d.ts +23 -20
- package/dist/adapters/project-checks.js +12 -3
- package/dist/cli/commands/guard.d.ts +54 -60
- package/dist/cli/commands/guard.js +46 -19
- package/dist/cli/index.js +24 -10
- package/dist/core/analysis/doctor.d.ts +1 -1
- package/dist/core/analysis/doctor.js +2 -3
- package/dist/core/discovery/discovery.d.ts +18 -14
- package/dist/core/discovery/discovery.js +35 -15
- package/dist/core/errors.d.ts +2 -2
- package/dist/core/guard.d.ts +124 -72
- package/dist/core/guard.js +542 -89
- package/dist/core/history/runs.d.ts +43 -0
- package/dist/core/history/runs.js +154 -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 +69 -0
- package/dist/core/policy/schemas.js +25 -0
- package/dist/core/verification/verify.d.ts +20 -16
- package/dist/core/verification/verify.js +55 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/resources.js +78 -29
- package/dist/mcp/tools/guard.js +147 -28
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -231,12 +231,37 @@ Guard state is stored in `.codapult/guard/`:
|
|
|
231
231
|
├── proposals.json evidence and approval history
|
|
232
232
|
├── baseline.json accepted pre-existing findings
|
|
233
233
|
├── agent.json AI-host completion-gate configuration
|
|
234
|
-
└── history/ project snapshots
|
|
234
|
+
└── history/ project snapshots and local verification run manifests
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
Commit policy and baseline files when the team wants shared guardrails. Treat cache artifacts as
|
|
238
238
|
disposable according to the project’s policy, and never commit secrets.
|
|
239
239
|
|
|
240
|
+
### Optional scoped budgets
|
|
241
|
+
|
|
242
|
+
Budgets are policy, not a universal style rule. Add them only for a named risk boundary such as a
|
|
243
|
+
service, route handler, or dependency-heavy module:
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{
|
|
247
|
+
"id": "service-lines",
|
|
248
|
+
"description": "Services must remain reviewable.",
|
|
249
|
+
"metric": "lines",
|
|
250
|
+
"scope": ["src/services"],
|
|
251
|
+
"limit": 300,
|
|
252
|
+
"severity": "warning",
|
|
253
|
+
"reason": "Keep service changes reviewable by one owner.",
|
|
254
|
+
"status": "active"
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Place budgets in the `budgets` array in `.codapult/guard/rules.json`. Supported metrics are
|
|
259
|
+
`lines`, `bytes`, and `imports`. Generated files, schemas, migrations, and other paths are not
|
|
260
|
+
checked unless they are explicitly included in `scope`; a wildcard such as `*` can intentionally
|
|
261
|
+
cover the project root. Invalid, absolute, parent-directory, or nonexistent scopes fail the Guard
|
|
262
|
+
gate. Changing a budget is a policy decision:
|
|
263
|
+
review the diff and update its written `reason` rather than silently increasing the limit.
|
|
264
|
+
|
|
240
265
|
## AI agents and MCP
|
|
241
266
|
|
|
242
267
|
Guard is deliberately model-agnostic. It does not send source code to a remote LLM and does not
|
|
@@ -283,6 +308,28 @@ task finished
|
|
|
283
308
|
For Cursor, Claude Code, Codex, Gemini CLI, GitHub Copilot, and generic hosts, see
|
|
284
309
|
[`docs/integrations/`](docs/integrations/).
|
|
285
310
|
|
|
311
|
+
For workflows where the authoring agent must not approve its own policy proposals, set Guard to
|
|
312
|
+
protected mode in `rules.json`:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"approval": {
|
|
317
|
+
"mode": "protected",
|
|
318
|
+
"allowMcpApproval": false,
|
|
319
|
+
"requireDistinctActor": true
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
MCP can then read and propose policy, while approval happens through the CLI or a protected CI/PR
|
|
325
|
+
process. With `requireDistinctActor`, CLI and permitted MCP approvals require `GUARD_APPROVER`,
|
|
326
|
+
and Guard rejects the same declared actor when a proposal was generated with `GUARD_PROPOSER`.
|
|
327
|
+
These variables provide declared provenance only; external branch protection or signed identity
|
|
328
|
+
remains responsible for proving who approved the change.
|
|
329
|
+
|
|
330
|
+
See [the extension map](docs/extensions.md) for the supported AI-host, CI/PR, policy-pack, tool
|
|
331
|
+
adapter, approval-governance, and observability integrations.
|
|
332
|
+
|
|
286
333
|
## CI
|
|
287
334
|
|
|
288
335
|
Copy the consumer workflow into a project that has installed and initialized Guard:
|
|
@@ -319,8 +366,18 @@ real project shapes.
|
|
|
319
366
|
Use `analyze --refresh` after a structural change. `doctor --fix-cache` removes only the disposable
|
|
320
367
|
discovery cache; it does not change rules, contracts, baseline, or source files.
|
|
321
368
|
|
|
369
|
+
Guard state is safe for concurrent CLI/MCP readers and writers: derived facts are published as an
|
|
370
|
+
atomic generation, policy updates use revisions, and short write contention is waited out. A
|
|
371
|
+
process that dies while holding the state lock is detected by its local PID and the lock is
|
|
372
|
+
recovered; use `--no-wait` when an integration needs immediate contention feedback.
|
|
373
|
+
|
|
322
374
|
Run `pnpm exec codapult-guard <command> --help` for command-specific options.
|
|
323
375
|
|
|
376
|
+
Every `verify` run also writes a local manifest under
|
|
377
|
+
`.codapult/guard/history/runs/<run-id>.json`. It records stage durations, the outcome, and the
|
|
378
|
+
policy gate that decided the result. It contains no source code or external telemetry. These
|
|
379
|
+
manifests make a failed run explainable without turning Guard into a production tracing system.
|
|
380
|
+
|
|
324
381
|
## Security and data handling
|
|
325
382
|
|
|
326
383
|
- Deterministic discovery and checks run locally.
|
|
@@ -5,17 +5,20 @@ export interface CommandResult {
|
|
|
5
5
|
exitCode: number;
|
|
6
6
|
stdout: string;
|
|
7
7
|
stderr: string;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
durationMs: number;
|
|
9
|
+
timedOut?: boolean | undefined;
|
|
10
|
+
truncated?: boolean | undefined;
|
|
10
11
|
}
|
|
11
|
-
export
|
|
12
|
-
timeout?: number;
|
|
13
|
-
env?: NodeJS.ProcessEnv;
|
|
14
|
-
}
|
|
15
|
-
export
|
|
12
|
+
export interface RunProjectCommandOptions {
|
|
13
|
+
timeout?: number | undefined;
|
|
14
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface CommandResponse {
|
|
16
17
|
content: {
|
|
17
18
|
type: 'text';
|
|
18
19
|
text: string;
|
|
19
20
|
}[];
|
|
20
21
|
isError: boolean;
|
|
21
|
-
}
|
|
22
|
+
}
|
|
23
|
+
export declare function runProjectCommand(command: string, cwd: string, options?: RunProjectCommandOptions): CommandResult;
|
|
24
|
+
export declare function commandResponse(result: CommandResult): CommandResponse;
|
package/dist/adapters/command.js
CHANGED
|
@@ -24,6 +24,7 @@ function parseCommand(command) {
|
|
|
24
24
|
return { executable: windowsExecutable, args };
|
|
25
25
|
}
|
|
26
26
|
export function runProjectCommand(command, cwd, options = {}) {
|
|
27
|
+
const startedAt = Date.now();
|
|
27
28
|
const parsed = parseCommand(command);
|
|
28
29
|
if (!parsed) {
|
|
29
30
|
return {
|
|
@@ -33,6 +34,7 @@ export function runProjectCommand(command, cwd, options = {}) {
|
|
|
33
34
|
exitCode: 2,
|
|
34
35
|
stdout: '',
|
|
35
36
|
stderr: 'Unsafe or empty project command rejected.',
|
|
37
|
+
durationMs: 0,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
try {
|
|
@@ -51,6 +53,7 @@ export function runProjectCommand(command, cwd, options = {}) {
|
|
|
51
53
|
exitCode: 0,
|
|
52
54
|
stdout: captured.value,
|
|
53
55
|
stderr: '',
|
|
56
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
54
57
|
truncated: captured.truncated,
|
|
55
58
|
};
|
|
56
59
|
}
|
|
@@ -68,6 +71,7 @@ export function runProjectCommand(command, cwd, options = {}) {
|
|
|
68
71
|
exitCode,
|
|
69
72
|
stdout: stdout.value,
|
|
70
73
|
stderr: timedOut ? `${stderr.value}\n[command timed out]` : stderr.value,
|
|
74
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
71
75
|
timedOut,
|
|
72
76
|
truncated: stdout.truncated || stderr.truncated,
|
|
73
77
|
};
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
import { type CommandResult } from './command.js';
|
|
2
|
-
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
2
|
+
import type { GuardAdapterName, GuardToolMode, ProjectCheck } from '../core/model/types.js';
|
|
3
|
+
export type { ProjectCheck } from '../core/model/types.js';
|
|
4
|
+
export type GuardAdapter = GuardAdapterName;
|
|
5
|
+
export type ExternalToolMode = GuardToolMode;
|
|
5
6
|
export type ProjectCheckResults = Partial<Record<ProjectCheck, CommandResult>>;
|
|
7
|
+
export interface ProjectChecksOptions {
|
|
8
|
+
timeout?: number | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface WorkspaceProjectChecksOptions extends ProjectChecksOptions {
|
|
11
|
+
rootResults?: ProjectCheckResults | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface ProjectAdaptersOptions {
|
|
14
|
+
mode?: Exclude<ExternalToolMode, 'off'> | undefined;
|
|
15
|
+
timeout?: number | undefined;
|
|
16
|
+
tooling?: Partial<Record<GuardAdapter, {
|
|
17
|
+
script: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
}>> | undefined;
|
|
20
|
+
}
|
|
6
21
|
export interface ProjectRuntimeDiagnostics {
|
|
7
22
|
node: string;
|
|
8
23
|
packageManager: string;
|
|
9
|
-
declaredPackageManager?: string;
|
|
10
|
-
declaredNode?: string;
|
|
24
|
+
declaredPackageManager?: string | undefined;
|
|
25
|
+
declaredNode?: string | undefined;
|
|
11
26
|
compatible: boolean;
|
|
12
27
|
issues: string[];
|
|
13
28
|
}
|
|
14
29
|
export declare function inspectProjectRuntime(root: string): ProjectRuntimeDiagnostics;
|
|
15
|
-
export declare function runProjectChecks(root: string, checks: ProjectCheck[], options?:
|
|
16
|
-
timeout?: number;
|
|
17
|
-
}): ProjectCheckResults;
|
|
30
|
+
export declare function runProjectChecks(root: string, checks: ProjectCheck[], options?: ProjectChecksOptions): ProjectCheckResults;
|
|
18
31
|
/**
|
|
19
32
|
* Runs checks for workspace packages only when the root package does not own
|
|
20
33
|
* the corresponding check. Root orchestration remains the source of truth.
|
|
@@ -22,16 +35,6 @@ export declare function runProjectChecks(root: string, checks: ProjectCheck[], o
|
|
|
22
35
|
export declare function runWorkspaceProjectChecks(root: string, packages: {
|
|
23
36
|
path: string;
|
|
24
37
|
scripts: Record<string, string>;
|
|
25
|
-
}[], checks: ProjectCheck[], options?:
|
|
26
|
-
timeout?: number;
|
|
27
|
-
rootResults?: ProjectCheckResults;
|
|
28
|
-
}): Record<string, ProjectCheckResults>;
|
|
38
|
+
}[], checks: ProjectCheck[], options?: WorkspaceProjectChecksOptions): Record<string, ProjectCheckResults>;
|
|
29
39
|
/** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
|
|
30
|
-
export declare function runProjectAdapters(root: string, options?:
|
|
31
|
-
mode?: Exclude<ExternalToolMode, 'off'>;
|
|
32
|
-
timeout?: number;
|
|
33
|
-
tooling?: Partial<Record<GuardAdapter, {
|
|
34
|
-
script: string;
|
|
35
|
-
enabled: boolean;
|
|
36
|
-
}>>;
|
|
37
|
-
}): Partial<Record<GuardAdapter, CommandResult>>;
|
|
40
|
+
export declare function runProjectAdapters(root: string, options?: ProjectAdaptersOptions): Partial<Record<GuardAdapter, CommandResult>>;
|
|
@@ -12,6 +12,7 @@ function notConfigured(check) {
|
|
|
12
12
|
exitCode: 0,
|
|
13
13
|
stdout: '',
|
|
14
14
|
stderr: `${check} script is not configured`,
|
|
15
|
+
durationMs: 0,
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
function readPackageMetadata(root) {
|
|
@@ -119,6 +120,9 @@ const adapterScriptPatterns = {
|
|
|
119
120
|
'dependency-graph': /(?:dep(?:endency)?[-:]?(?:check|graph|cruise)|madge|architecture)/i,
|
|
120
121
|
security: /(?:security|semgrep|gitleaks|snyk|trivy|audit)/i,
|
|
121
122
|
'dependency-hygiene': /(?:knip|dep(?:endency)?[-:]?(?:unused|hygiene))/i,
|
|
123
|
+
sast: /(?:semgrep|codeql|bearer|sast)/i,
|
|
124
|
+
'secret-scanning': /(?:gitleaks|trufflehog|secret[-_ ]?scan)/i,
|
|
125
|
+
'dependency-audit': /(?:npm audit|pnpm audit|yarn audit|osv|snyk|trivy|dependency[-_ ]?audit)/i,
|
|
122
126
|
};
|
|
123
127
|
/** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
|
|
124
128
|
export function runProjectAdapters(root, options = {}) {
|
|
@@ -126,6 +130,7 @@ export function runProjectAdapters(root, options = {}) {
|
|
|
126
130
|
const manager = packageManager(root, metadata.packageManager);
|
|
127
131
|
const scripts = metadata.scripts ?? {};
|
|
128
132
|
const results = {};
|
|
133
|
+
const executed = new Map();
|
|
129
134
|
for (const [adapter, pattern] of Object.entries(adapterScriptPatterns)) {
|
|
130
135
|
const configured = options.tooling?.[adapter];
|
|
131
136
|
const script = configured?.enabled
|
|
@@ -141,9 +146,13 @@ export function runProjectAdapters(root, options = {}) {
|
|
|
141
146
|
continue;
|
|
142
147
|
}
|
|
143
148
|
const command = manager === 'npm' ? `npm run ${script}` : `${manager} run ${script}`;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
const previous = executed.get(command);
|
|
150
|
+
const result = previous ??
|
|
151
|
+
runProjectCommand(command, root, {
|
|
152
|
+
timeout: options.timeout ?? 120_000,
|
|
153
|
+
});
|
|
154
|
+
executed.set(command, result);
|
|
155
|
+
results[adapter] = result;
|
|
147
156
|
}
|
|
148
157
|
return results;
|
|
149
158
|
}
|
|
@@ -1,64 +1,58 @@
|
|
|
1
1
|
import { type GuardToolMode } from '../../core/guard.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
interface GuardOutputOptions {
|
|
3
|
+
json?: boolean | undefined;
|
|
4
|
+
}
|
|
5
|
+
interface GuardInitOptions {
|
|
6
|
+
force?: boolean | undefined;
|
|
7
|
+
wait?: boolean | undefined;
|
|
8
|
+
}
|
|
9
|
+
interface GuardAnalyzeOptions {
|
|
10
|
+
refresh?: boolean | undefined;
|
|
11
|
+
wait?: boolean | undefined;
|
|
12
|
+
}
|
|
13
|
+
interface GuardVerifyOptions extends GuardOutputOptions {
|
|
14
|
+
checks?: string | undefined;
|
|
15
|
+
changed?: boolean | undefined;
|
|
16
|
+
requirement?: string | undefined;
|
|
17
|
+
tools?: GuardToolMode | undefined;
|
|
18
|
+
strict?: boolean | undefined;
|
|
19
|
+
projectChecks?: boolean | undefined;
|
|
20
|
+
}
|
|
21
|
+
interface GuardCheckOptions extends GuardOutputOptions {
|
|
22
|
+
changed?: boolean | undefined;
|
|
23
|
+
sarif?: boolean | undefined;
|
|
24
|
+
}
|
|
25
|
+
interface GuardBaselineOptions extends GuardOutputOptions {
|
|
26
|
+
all?: boolean | undefined;
|
|
27
|
+
reason?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
interface GuardApprovalOptions {
|
|
30
|
+
all?: boolean | undefined;
|
|
31
|
+
}
|
|
32
|
+
interface GuardReviewOptions {
|
|
33
|
+
maxDiffChars?: string | undefined;
|
|
34
|
+
requirement?: string | undefined;
|
|
35
|
+
base?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
export declare function guardInitCommand(options?: GuardInitOptions): void;
|
|
38
|
+
export declare function guardAnalyzeCommand(options?: GuardAnalyzeOptions): void;
|
|
39
|
+
export declare function guardProposeCommand(options?: GuardOutputOptions): void;
|
|
40
|
+
export declare function guardInstallAgentCommand(target?: string, options?: GuardOutputOptions): void;
|
|
41
|
+
export declare function guardDoctorCommand(options?: GuardOutputOptions & {
|
|
42
|
+
fixCache?: boolean | undefined;
|
|
17
43
|
}): void;
|
|
18
44
|
export declare function guardHistoryCommand(): void;
|
|
19
|
-
export declare function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export declare function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export declare function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
strict?: boolean;
|
|
32
|
-
projectChecks?: boolean;
|
|
33
|
-
}): void;
|
|
34
|
-
export declare function guardCheckCommand(options?: {
|
|
35
|
-
changed?: boolean;
|
|
36
|
-
json?: boolean;
|
|
37
|
-
sarif?: boolean;
|
|
38
|
-
}): void;
|
|
39
|
-
export declare function guardAuditCommand(options?: {
|
|
40
|
-
json?: boolean;
|
|
41
|
-
}): void;
|
|
42
|
-
export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: {
|
|
43
|
-
all?: boolean;
|
|
44
|
-
reason?: string;
|
|
45
|
-
json?: boolean;
|
|
46
|
-
}): void;
|
|
47
|
-
export declare function guardRulesApproveCommand(ids?: string, options?: {
|
|
48
|
-
all?: boolean;
|
|
49
|
-
}): void;
|
|
50
|
-
export declare function guardPolicyExplainCommand(id: string, options?: {
|
|
51
|
-
json?: boolean;
|
|
52
|
-
}): void;
|
|
53
|
-
export declare function guardContractsApproveCommand(ids?: string, options?: {
|
|
54
|
-
all?: boolean;
|
|
55
|
-
}): void;
|
|
56
|
-
export declare function guardContractsRejectCommand(ids?: string, options?: {
|
|
57
|
-
all?: boolean;
|
|
58
|
-
}): void;
|
|
59
|
-
export declare function guardReviewCommand(options?: {
|
|
60
|
-
maxDiffChars?: string;
|
|
61
|
-
requirement?: string;
|
|
62
|
-
base?: string;
|
|
63
|
-
}): void;
|
|
45
|
+
export declare function guardRunsCommand(options?: GuardOutputOptions): void;
|
|
46
|
+
export declare function guardImpactCommand(files: string[], options?: GuardOutputOptions): void;
|
|
47
|
+
export declare function guardHistoryDiffCommand(from: string, to: string, options?: GuardOutputOptions): void;
|
|
48
|
+
export declare function guardVerifyCommand(options?: GuardVerifyOptions): void;
|
|
49
|
+
export declare function guardCheckCommand(options?: GuardCheckOptions): void;
|
|
50
|
+
export declare function guardAuditCommand(options?: GuardOutputOptions): void;
|
|
51
|
+
export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: GuardBaselineOptions): void;
|
|
52
|
+
export declare function guardRulesApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
53
|
+
export declare function guardPolicyExplainCommand(id: string, options?: GuardOutputOptions): void;
|
|
54
|
+
export declare function guardContractsApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
55
|
+
export declare function guardContractsRejectCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
56
|
+
export declare function guardReviewCommand(options?: GuardReviewOptions): void;
|
|
64
57
|
export declare function guardIsInitialized(root: string): boolean;
|
|
58
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
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, clearDiscoveryCache, 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
4
|
import { guardErrorPayload } from '../../core/errors.js';
|
|
5
5
|
import { runGuardVerification, } from '../../core/verification/verify.js';
|
|
6
6
|
import { diagnoseGuard } from '../../core/analysis/doctor.js';
|
|
@@ -9,6 +9,7 @@ import { guardAgentTargets, installGuardAgentInstructions, installGuardAgentTarg
|
|
|
9
9
|
import { dim, fail, heading, info, success, warn } from '../ui.js';
|
|
10
10
|
import { guardFindingsToSarif } from '../../core/output/sarif.js';
|
|
11
11
|
import { analyzeProjectImpact } from '../../core/analysis/impact.js';
|
|
12
|
+
import { listGuardRuns, summarizeGuardRuns } from '../../core/history/runs.js';
|
|
12
13
|
function renderFindings(findings) {
|
|
13
14
|
for (const finding of findings) {
|
|
14
15
|
const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
|
|
@@ -70,7 +71,7 @@ export function guardInitCommand(options = {}) {
|
|
|
70
71
|
const root = getRoot();
|
|
71
72
|
let initialized;
|
|
72
73
|
try {
|
|
73
|
-
initialized = initializeGuard(root, { force: options.force });
|
|
74
|
+
initialized = initializeGuard(root, options.wait === false ? { force: options.force, noWait: true } : { force: options.force });
|
|
74
75
|
}
|
|
75
76
|
catch (error) {
|
|
76
77
|
if (error instanceof GuardAlreadyInitializedError) {
|
|
@@ -97,14 +98,12 @@ export function guardInitCommand(options = {}) {
|
|
|
97
98
|
info(`${config.rules.length} rule(s) generated (${proposedRules} proposed); ${report.scannedFiles} source file(s) scanned.`);
|
|
98
99
|
dim('Existing findings are baselined. New violations will be reported by `codapult-guard check`.');
|
|
99
100
|
}
|
|
100
|
-
export function guardAnalyzeCommand(
|
|
101
|
+
export function guardAnalyzeCommand(options = {}) {
|
|
101
102
|
const root = getRoot();
|
|
102
103
|
const { model: projectModel, metrics } = discoverProjectWithMetrics(root, {
|
|
103
104
|
persistCache: true,
|
|
104
105
|
});
|
|
105
|
-
|
|
106
|
-
writeGuardMemory(root, projectModel);
|
|
107
|
-
const revision = writeProjectSnapshot(root, projectModel);
|
|
106
|
+
const revision = writeProjectState(root, projectModel, options.wait === false ? { noWait: true } : undefined);
|
|
108
107
|
heading('Codapult Guard Analyze');
|
|
109
108
|
success(`Updated ${GUARD_PROJECT_FILE}, ${GUARD_ARCHITECTURE_FILE}, and ${GUARD_CONVENTIONS_FILE}`);
|
|
110
109
|
info(`${projectModel.files.length} file(s), ${projectModel.modules.length} module(s), ${projectModel.insights.cycles.length} cycle(s) recorded as project context.`);
|
|
@@ -196,6 +195,19 @@ export function guardHistoryCommand() {
|
|
|
196
195
|
info(`${snapshot.revision}: ${snapshot.files} files, ${snapshot.modules} modules, ${snapshot.cycles} cycles`);
|
|
197
196
|
}
|
|
198
197
|
}
|
|
198
|
+
export function guardRunsCommand(options = {}) {
|
|
199
|
+
const root = getRoot();
|
|
200
|
+
const result = { summary: summarizeGuardRuns(root), runs: listGuardRuns(root) };
|
|
201
|
+
if (options.json) {
|
|
202
|
+
console.log(JSON.stringify(result, null, 2));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
heading('Codapult Guard Runs');
|
|
206
|
+
info(`${result.summary.total} run(s); ${result.summary.failed} failed, ${result.summary.warnings} warning, average ${result.summary.averageDurationMs} ms`);
|
|
207
|
+
for (const run of result.runs) {
|
|
208
|
+
info(`${run.runId}: ${run.outcome}, gate=${run.gate}, ${run.durationMs} ms`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
199
211
|
export function guardImpactCommand(files, options = {}) {
|
|
200
212
|
const root = getRoot();
|
|
201
213
|
if (files.length === 0) {
|
|
@@ -326,7 +338,7 @@ export function guardCheckCommand(options = {}) {
|
|
|
326
338
|
baseline: loadBaseline(root),
|
|
327
339
|
includeArchitectureInsights: true,
|
|
328
340
|
});
|
|
329
|
-
const contractIssues =
|
|
341
|
+
const contractIssues = validateGuardPolicy(root, config);
|
|
330
342
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
331
343
|
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
332
344
|
const contractFindings = contractIssues.map((issue) => ({
|
|
@@ -392,7 +404,7 @@ export function guardAuditCommand(options = {}) {
|
|
|
392
404
|
return;
|
|
393
405
|
}
|
|
394
406
|
const report = scanGuard(root, config, { includeArchitectureInsights: true });
|
|
395
|
-
const contractIssues =
|
|
407
|
+
const contractIssues = validateGuardPolicy(root, config);
|
|
396
408
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
397
409
|
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
398
410
|
if (options.json) {
|
|
@@ -491,11 +503,16 @@ export function guardRulesApproveCommand(ids, options = {}) {
|
|
|
491
503
|
return;
|
|
492
504
|
}
|
|
493
505
|
const selectedIds = new Set(selected.map((rule) => rule.id));
|
|
494
|
-
|
|
506
|
+
const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
|
|
507
|
+
if (approvalError) {
|
|
508
|
+
fail(approvalError);
|
|
509
|
+
process.exitCode = 1;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
applyGuardProposalDecision(root, {
|
|
495
513
|
...config,
|
|
496
514
|
rules: config.rules.map((rule) => selectedIds.has(rule.id) ? { ...rule, status: 'active' } : rule),
|
|
497
|
-
});
|
|
498
|
-
recordGuardProposalDecision(root, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })));
|
|
515
|
+
}, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })), { source: 'cli' });
|
|
499
516
|
for (const rule of selected)
|
|
500
517
|
success(`Activated ${rule.id}`);
|
|
501
518
|
process.exitCode = 0;
|
|
@@ -566,15 +583,20 @@ export function guardContractsApproveCommand(ids, options = {}) {
|
|
|
566
583
|
return;
|
|
567
584
|
}
|
|
568
585
|
const selectedIds = new Set(selected.map((contract) => contract.id));
|
|
569
|
-
|
|
586
|
+
const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
|
|
587
|
+
if (approvalError) {
|
|
588
|
+
fail(approvalError);
|
|
589
|
+
process.exitCode = 1;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
applyGuardProposalDecision(root, {
|
|
570
593
|
...config,
|
|
571
594
|
contracts: (config.contracts ?? []).map((contract) => selectedIds.has(contract.id) ? { ...contract, status: 'active' } : contract),
|
|
572
|
-
})
|
|
573
|
-
recordGuardProposalDecision(root, selected.map((contract) => ({
|
|
595
|
+
}, selected.map((contract) => ({
|
|
574
596
|
id: contract.id,
|
|
575
597
|
type: 'contract',
|
|
576
598
|
decision: 'approved',
|
|
577
|
-
})));
|
|
599
|
+
})), { source: 'cli' });
|
|
578
600
|
for (const contract of selected)
|
|
579
601
|
success(`Activated ${contract.id}`);
|
|
580
602
|
process.exitCode = 0;
|
|
@@ -601,15 +623,20 @@ export function guardContractsRejectCommand(ids, options = {}) {
|
|
|
601
623
|
process.exitCode = 1;
|
|
602
624
|
return;
|
|
603
625
|
}
|
|
604
|
-
|
|
626
|
+
const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
|
|
627
|
+
if (approvalError) {
|
|
628
|
+
fail(approvalError);
|
|
629
|
+
process.exitCode = 1;
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
applyGuardProposalDecision(root, {
|
|
605
633
|
...config,
|
|
606
634
|
contracts: config.contracts ?? [],
|
|
607
|
-
})
|
|
608
|
-
recordGuardProposalDecision(root, selected.map((contract) => ({
|
|
635
|
+
}, selected.map((contract) => ({
|
|
609
636
|
id: contract.id,
|
|
610
637
|
type: 'contract',
|
|
611
638
|
decision: 'rejected',
|
|
612
|
-
})));
|
|
639
|
+
})), { source: 'cli' });
|
|
613
640
|
for (const contract of selected)
|
|
614
641
|
success(`Rejected ${contract.id}`);
|
|
615
642
|
process.exitCode = 0;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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, guardPolicyExplainCommand, 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, guardRunsCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
|
|
5
5
|
import { config } from '../core/config.js';
|
|
6
|
-
import { GuardConfigError, GuardStateBusyError } from '../core/guard.js';
|
|
6
|
+
import { GuardBaselineReasonError, GuardConfigError, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
|
|
7
7
|
import { guardErrorPayload } from '../core/errors.js';
|
|
8
8
|
const program = new Command()
|
|
9
9
|
.name(config.commandName)
|
|
@@ -16,12 +16,13 @@ const program = new Command()
|
|
|
16
16
|
// Guard is the standalone product, so its commands live at the package root:
|
|
17
17
|
// `codapult-guard init`, not `codapult-guard guard init`.
|
|
18
18
|
const guard = program;
|
|
19
|
-
guard.command('init').option('--force').action(guardInitCommand);
|
|
20
|
-
guard.command('analyze').option('--refresh').action(guardAnalyzeCommand);
|
|
19
|
+
guard.command('init').option('--force').option('--no-wait').action(guardInitCommand);
|
|
20
|
+
guard.command('analyze').option('--refresh').option('--no-wait').action(guardAnalyzeCommand);
|
|
21
21
|
guard.command('propose').option('--json').action(guardProposeCommand);
|
|
22
22
|
guard.command('install-agent [target]').option('--json').action(guardInstallAgentCommand);
|
|
23
23
|
guard.command('doctor').option('--json').option('--fix-cache').action(guardDoctorCommand);
|
|
24
24
|
guard.command('history').action(guardHistoryCommand);
|
|
25
|
+
guard.command('runs').option('--json').action(guardRunsCommand);
|
|
25
26
|
guard.command('history-diff <from> <to>').option('--json').action(guardHistoryDiffCommand);
|
|
26
27
|
guard.command('impact <files...>').option('--json').action(guardImpactCommand);
|
|
27
28
|
guard
|
|
@@ -81,15 +82,28 @@ try {
|
|
|
81
82
|
program.parse();
|
|
82
83
|
}
|
|
83
84
|
catch (error) {
|
|
84
|
-
if (error instanceof GuardConfigError ||
|
|
85
|
+
if (error instanceof GuardConfigError ||
|
|
86
|
+
error instanceof GuardBaselineReasonError ||
|
|
87
|
+
error instanceof GuardStateBusyError ||
|
|
88
|
+
error instanceof GuardStateStaleError) {
|
|
85
89
|
const isBusy = error instanceof GuardStateBusyError;
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const isStale = error instanceof GuardStateStaleError;
|
|
91
|
+
const isInvalidInput = error instanceof GuardBaselineReasonError;
|
|
92
|
+
console.error(JSON.stringify(guardErrorPayload(isBusy
|
|
93
|
+
? 'GUARD_STATE_BUSY'
|
|
94
|
+
: isStale
|
|
95
|
+
? 'GUARD_STATE_STALE'
|
|
96
|
+
: isInvalidInput
|
|
97
|
+
? 'GUARD_INVALID_INPUT'
|
|
98
|
+
: 'GUARD_CONFIG_INVALID', error.message, {
|
|
99
|
+
configured: !isBusy && !isStale,
|
|
88
100
|
outcome: 'error',
|
|
89
101
|
recoverable: true,
|
|
90
|
-
hint: isBusy
|
|
91
|
-
? '
|
|
92
|
-
:
|
|
102
|
+
hint: isBusy || isStale
|
|
103
|
+
? 'Re-read Guard state and retry the operation.'
|
|
104
|
+
: isInvalidInput
|
|
105
|
+
? 'Provide a written reason for the baseline decision.'
|
|
106
|
+
: 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
|
|
93
107
|
}), null, 2));
|
|
94
108
|
process.exitCode = 1;
|
|
95
109
|
}
|
|
@@ -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,
|
|
@@ -48,8 +48,7 @@ export function diagnoseGuard(root) {
|
|
|
48
48
|
(() => {
|
|
49
49
|
try {
|
|
50
50
|
const guardConfig = loadGuardConfig(root);
|
|
51
|
-
return
|
|
52
|
-
validateGuardContracts(root, guardConfig.contracts).length > 0);
|
|
51
|
+
return guardConfig !== undefined && validateGuardPolicy(root, guardConfig).length > 0;
|
|
53
52
|
}
|
|
54
53
|
catch {
|
|
55
54
|
return true;
|