@codapult/guard 0.3.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 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 for comparison
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,25 @@ 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
+
286
330
  ## CI
287
331
 
288
332
  Copy the consumer workflow into a project that has installed and initialized Guard:
@@ -319,8 +363,18 @@ real project shapes.
319
363
  Use `analyze --refresh` after a structural change. `doctor --fix-cache` removes only the disposable
320
364
  discovery cache; it does not change rules, contracts, baseline, or source files.
321
365
 
366
+ Guard state is safe for concurrent CLI/MCP readers and writers: derived facts are published as an
367
+ atomic generation, policy updates use revisions, and short write contention is waited out. A
368
+ process that dies while holding the state lock is detected by its local PID and the lock is
369
+ recovered; use `--no-wait` when an integration needs immediate contention feedback.
370
+
322
371
  Run `pnpm exec codapult-guard <command> --help` for command-specific options.
323
372
 
373
+ Every `verify` run also writes a local manifest under
374
+ `.codapult/guard/history/runs/<run-id>.json`. It records stage durations, the outcome, and the
375
+ policy gate that decided the result. It contains no source code or external telemetry. These
376
+ manifests make a failed run explainable without turning Guard into a production tracing system.
377
+
324
378
  ## Security and data handling
325
379
 
326
380
  - Deterministic discovery and checks run locally.
@@ -5,17 +5,19 @@ export interface CommandResult {
5
5
  exitCode: number;
6
6
  stdout: string;
7
7
  stderr: string;
8
- timedOut?: boolean;
9
- truncated?: boolean;
8
+ timedOut?: boolean | undefined;
9
+ truncated?: boolean | undefined;
10
10
  }
11
- export declare function runProjectCommand(command: string, cwd: string, options?: {
12
- timeout?: number;
13
- env?: NodeJS.ProcessEnv;
14
- }): CommandResult;
15
- export declare function commandResponse(result: CommandResult): {
11
+ export interface RunProjectCommandOptions {
12
+ timeout?: number | undefined;
13
+ env?: NodeJS.ProcessEnv | undefined;
14
+ }
15
+ export interface CommandResponse {
16
16
  content: {
17
17
  type: 'text';
18
18
  text: string;
19
19
  }[];
20
20
  isError: boolean;
21
- };
21
+ }
22
+ export declare function runProjectCommand(command: string, cwd: string, options?: RunProjectCommandOptions): CommandResult;
23
+ export declare function commandResponse(result: CommandResult): CommandResponse;
@@ -1,20 +1,33 @@
1
1
  import { type CommandResult } from './command.js';
2
- export type ProjectCheck = 'lint' | 'typecheck' | 'test' | 'build';
3
- export type GuardAdapter = 'dependency-graph' | 'security' | 'dependency-hygiene';
4
- export type ExternalToolMode = 'auto' | 'on' | 'off';
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>>;
@@ -1,64 +1,57 @@
1
1
  import { type GuardToolMode } from '../../core/guard.js';
2
- export declare function guardInitCommand(options?: {
3
- force?: boolean;
4
- }): void;
5
- export declare function guardAnalyzeCommand(_options?: {
6
- refresh?: boolean;
7
- }): void;
8
- export declare function guardProposeCommand(options?: {
9
- json?: boolean;
10
- }): void;
11
- export declare function guardInstallAgentCommand(target?: string, options?: {
12
- json?: boolean;
13
- }): void;
14
- export declare function guardDoctorCommand(options?: {
15
- json?: boolean;
16
- fixCache?: boolean;
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 guardImpactCommand(files: string[], options?: {
20
- json?: boolean;
21
- }): void;
22
- export declare function guardHistoryDiffCommand(from: string, to: string, options?: {
23
- json?: boolean;
24
- }): void;
25
- export declare function guardVerifyCommand(options?: {
26
- checks?: string;
27
- changed?: boolean;
28
- json?: boolean;
29
- requirement?: string;
30
- tools?: GuardToolMode;
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 guardImpactCommand(files: string[], options?: GuardOutputOptions): void;
46
+ export declare function guardHistoryDiffCommand(from: string, to: string, options?: GuardOutputOptions): void;
47
+ export declare function guardVerifyCommand(options?: GuardVerifyOptions): void;
48
+ export declare function guardCheckCommand(options?: GuardCheckOptions): void;
49
+ export declare function guardAuditCommand(options?: GuardOutputOptions): void;
50
+ export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: GuardBaselineOptions): void;
51
+ export declare function guardRulesApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
52
+ export declare function guardPolicyExplainCommand(id: string, options?: GuardOutputOptions): void;
53
+ export declare function guardContractsApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
54
+ export declare function guardContractsRejectCommand(ids?: string, options?: GuardApprovalOptions): void;
55
+ export declare function guardReviewCommand(options?: GuardReviewOptions): void;
64
56
  export declare function guardIsInitialized(root: string): boolean;
57
+ 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, writeGuardConfig, writeGuardProposals, recordGuardProposalDecision, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardContracts, writeProjectModel, writeGuardMemory, writeProjectSnapshot, } from '../../core/guard.js';
3
+ import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, 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';
@@ -70,7 +70,7 @@ export function guardInitCommand(options = {}) {
70
70
  const root = getRoot();
71
71
  let initialized;
72
72
  try {
73
- initialized = initializeGuard(root, { force: options.force });
73
+ initialized = initializeGuard(root, options.wait === false ? { force: options.force, noWait: true } : { force: options.force });
74
74
  }
75
75
  catch (error) {
76
76
  if (error instanceof GuardAlreadyInitializedError) {
@@ -97,14 +97,12 @@ export function guardInitCommand(options = {}) {
97
97
  info(`${config.rules.length} rule(s) generated (${proposedRules} proposed); ${report.scannedFiles} source file(s) scanned.`);
98
98
  dim('Existing findings are baselined. New violations will be reported by `codapult-guard check`.');
99
99
  }
100
- export function guardAnalyzeCommand(_options = {}) {
100
+ export function guardAnalyzeCommand(options = {}) {
101
101
  const root = getRoot();
102
102
  const { model: projectModel, metrics } = discoverProjectWithMetrics(root, {
103
103
  persistCache: true,
104
104
  });
105
- writeProjectModel(root, projectModel);
106
- writeGuardMemory(root, projectModel);
107
- const revision = writeProjectSnapshot(root, projectModel);
105
+ const revision = writeProjectState(root, projectModel, options.wait === false ? { noWait: true } : undefined);
108
106
  heading('Codapult Guard Analyze');
109
107
  success(`Updated ${GUARD_PROJECT_FILE}, ${GUARD_ARCHITECTURE_FILE}, and ${GUARD_CONVENTIONS_FILE}`);
110
108
  info(`${projectModel.files.length} file(s), ${projectModel.modules.length} module(s), ${projectModel.insights.cycles.length} cycle(s) recorded as project context.`);
@@ -326,7 +324,7 @@ export function guardCheckCommand(options = {}) {
326
324
  baseline: loadBaseline(root),
327
325
  includeArchitectureInsights: true,
328
326
  });
329
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
327
+ const contractIssues = validateGuardPolicy(root, config);
330
328
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
331
329
  const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
332
330
  const contractFindings = contractIssues.map((issue) => ({
@@ -392,7 +390,7 @@ export function guardAuditCommand(options = {}) {
392
390
  return;
393
391
  }
394
392
  const report = scanGuard(root, config, { includeArchitectureInsights: true });
395
- const contractIssues = validateGuardContracts(root, config.contracts ?? []);
393
+ const contractIssues = validateGuardPolicy(root, config);
396
394
  const errors = report.findings.filter((finding) => finding.severity === 'error').length;
397
395
  const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
398
396
  if (options.json) {
@@ -491,11 +489,16 @@ export function guardRulesApproveCommand(ids, options = {}) {
491
489
  return;
492
490
  }
493
491
  const selectedIds = new Set(selected.map((rule) => rule.id));
494
- writeGuardConfig(root, {
492
+ const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
493
+ if (approvalError) {
494
+ fail(approvalError);
495
+ process.exitCode = 1;
496
+ return;
497
+ }
498
+ applyGuardProposalDecision(root, {
495
499
  ...config,
496
500
  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' })));
501
+ }, selected.map((rule) => ({ id: rule.id, type: 'rule', decision: 'approved' })), { source: 'cli' });
499
502
  for (const rule of selected)
500
503
  success(`Activated ${rule.id}`);
501
504
  process.exitCode = 0;
@@ -566,15 +569,20 @@ export function guardContractsApproveCommand(ids, options = {}) {
566
569
  return;
567
570
  }
568
571
  const selectedIds = new Set(selected.map((contract) => contract.id));
569
- writeGuardConfig(root, {
572
+ const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
573
+ if (approvalError) {
574
+ fail(approvalError);
575
+ process.exitCode = 1;
576
+ return;
577
+ }
578
+ applyGuardProposalDecision(root, {
570
579
  ...config,
571
580
  contracts: (config.contracts ?? []).map((contract) => selectedIds.has(contract.id) ? { ...contract, status: 'active' } : contract),
572
- });
573
- recordGuardProposalDecision(root, selected.map((contract) => ({
581
+ }, selected.map((contract) => ({
574
582
  id: contract.id,
575
583
  type: 'contract',
576
584
  decision: 'approved',
577
- })));
585
+ })), { source: 'cli' });
578
586
  for (const contract of selected)
579
587
  success(`Activated ${contract.id}`);
580
588
  process.exitCode = 0;
@@ -601,15 +609,20 @@ export function guardContractsRejectCommand(ids, options = {}) {
601
609
  process.exitCode = 1;
602
610
  return;
603
611
  }
604
- writeGuardConfig(root, {
612
+ const approvalError = validateGuardProposalApproval(config.approval, loadGuardProposals(root));
613
+ if (approvalError) {
614
+ fail(approvalError);
615
+ process.exitCode = 1;
616
+ return;
617
+ }
618
+ applyGuardProposalDecision(root, {
605
619
  ...config,
606
620
  contracts: config.contracts ?? [],
607
- });
608
- recordGuardProposalDecision(root, selected.map((contract) => ({
621
+ }, selected.map((contract) => ({
609
622
  id: contract.id,
610
623
  type: 'contract',
611
624
  decision: 'rejected',
612
- })));
625
+ })), { source: 'cli' });
613
626
  for (const contract of selected)
614
627
  success(`Rejected ${contract.id}`);
615
628
  process.exitCode = 0;
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
4
  import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
5
5
  import { config } from '../core/config.js';
6
- import { GuardConfigError, GuardStateBusyError } from '../core/guard.js';
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,8 +16,8 @@ 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);
@@ -81,15 +81,28 @@ try {
81
81
  program.parse();
82
82
  }
83
83
  catch (error) {
84
- if (error instanceof GuardConfigError || error instanceof GuardStateBusyError) {
84
+ if (error instanceof GuardConfigError ||
85
+ error instanceof GuardBaselineReasonError ||
86
+ error instanceof GuardStateBusyError ||
87
+ error instanceof GuardStateStaleError) {
85
88
  const isBusy = error instanceof GuardStateBusyError;
86
- console.error(JSON.stringify(guardErrorPayload(isBusy ? 'GUARD_STATE_BUSY' : 'GUARD_CONFIG_INVALID', error.message, {
87
- configured: !isBusy,
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,
88
99
  outcome: 'error',
89
100
  recoverable: true,
90
- hint: isBusy
91
- ? 'Retry after the other Guard process finishes.'
92
- : 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
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.',
93
106
  }), null, 2));
94
107
  process.exitCode = 1;
95
108
  }
@@ -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, validateGuardContracts, } from '../guard.js';
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 (guardConfig !== undefined &&
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;
@@ -11,7 +11,7 @@ export interface ModuleRecord {
11
11
  exports: string[];
12
12
  calls: string[];
13
13
  resolvedImports: string[];
14
- resolvedImportMap?: Record<string, string>;
14
+ resolvedImportMap?: Record<string, string> | undefined;
15
15
  dynamicImports: string[];
16
16
  declarations: {
17
17
  classes: number;
@@ -33,20 +33,20 @@ export interface CapabilitySignal {
33
33
  export interface ProjectModel {
34
34
  version: 1;
35
35
  project: {
36
- name?: string;
37
- packageManager?: string;
36
+ name?: string | undefined;
37
+ packageManager?: string | undefined;
38
38
  frameworks: string[];
39
39
  scripts: Record<string, string>;
40
40
  dependencies: Record<string, string>;
41
41
  devDependencies: Record<string, string>;
42
42
  workspacePackages: {
43
43
  path: string;
44
- name?: string;
44
+ name?: string | undefined;
45
45
  private: boolean;
46
46
  scripts: Record<string, string>;
47
47
  dependencies: string[];
48
- exports?: string[];
49
- projectReferences?: string[];
48
+ exports?: string[] | undefined;
49
+ projectReferences?: string[] | undefined;
50
50
  }[];
51
51
  };
52
52
  files: ProjectFileRecord[];
@@ -59,7 +59,7 @@ export interface ProjectModel {
59
59
  };
60
60
  git: {
61
61
  repository: boolean;
62
- branch?: string;
62
+ branch?: string | undefined;
63
63
  dirty: boolean;
64
64
  changedFiles: string[];
65
65
  status: string[];
@@ -125,16 +125,23 @@ export interface DiscoveryMetrics {
125
125
  changedFiles: number;
126
126
  reusedModules: number;
127
127
  }
128
+ export interface DiscoveryResult {
129
+ model: ProjectModel;
130
+ metrics: DiscoveryMetrics;
131
+ }
128
132
  export interface DiscoveryOptions {
129
- persistCache?: boolean;
133
+ persistCache?: boolean | undefined;
130
134
  /** Maximum number of files included in one model. */
131
- maxFiles?: number;
135
+ maxFiles?: number | undefined;
132
136
  /** Maximum size of one included file in bytes. */
133
- maxFileBytes?: number;
137
+ maxFileBytes?: number | undefined;
134
138
  }
135
139
  export declare class DiscoveryLimitError extends Error {
136
140
  constructor(message: string);
137
141
  }
142
+ export declare class DiscoveryStaleError extends Error {
143
+ constructor();
144
+ }
138
145
  export declare function clearDiscoveryCache(root: string): void;
139
146
  export declare function findGuardRoot(from?: string): string;
140
147
  /** Build the internal module graph from AST resolution plus conservative fallback resolution.
@@ -145,7 +152,4 @@ export declare function findGuardRoot(from?: string): string;
145
152
  */
146
153
  export declare function buildModuleTargetGraph(modules: ModuleRecord[]): Map<string, string[]>;
147
154
  export declare function discoverProject(root: string, options?: DiscoveryOptions): ProjectModel;
148
- export declare function discoverProjectWithMetrics(root: string, options?: DiscoveryOptions): {
149
- model: ProjectModel;
150
- metrics: DiscoveryMetrics;
151
- };
155
+ export declare function discoverProjectWithMetrics(root: string, options?: DiscoveryOptions): DiscoveryResult;