@codapult/guard 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -168,6 +168,10 @@ Guard does not assume a fixed `UI → actions → services → repositories →
168
168
  It can discover that shape when the project exhibits it, but observed patterns become enforceable
169
169
  only after explicit approval.
170
170
 
171
+ Policy paths are validated as project-relative paths. Guard rejects traversal, absolute paths, and
172
+ symlinks escaping the project root; import-boundary checks include imports, re-exports, and literal
173
+ dynamic imports.
174
+
171
175
  ## What Guard discovers
172
176
 
173
177
  The model is framework-aware without being framework-dependent:
@@ -186,6 +190,15 @@ The model is framework-aware without being framework-dependent:
186
190
  Capabilities are evidence, not requirements. A Vite app, Express service, Hono project, Node
187
191
  package, monorepo, or Next.js SaaS can all use the same Guard core.
188
192
 
193
+ ## What Guard is — and is not
194
+
195
+ Guard is an architecture control plane for AI-assisted development. It protects project-specific
196
+ boundaries and change impact using local facts, approved policy, and deterministic verification.
197
+
198
+ Guard is not a replacement for ESLint, TypeScript, tests, SAST, or a general-purpose PR bot. Those
199
+ tools answer different questions; Guard connects their results with the architectural memory that
200
+ an AI coding agent needs before and after changing a repository.
201
+
189
202
  ## The normal loop
190
203
 
191
204
  ```text
@@ -259,6 +272,7 @@ Recommended agent loop:
259
272
 
260
273
  ```text
261
274
  task finished
275
+ → codapult_guard_next_action
262
276
  → codapult_guard_context
263
277
  → codapult_guard_review(requirement, diff)
264
278
  → codapult_guard_verify
@@ -298,9 +312,13 @@ real project shapes.
298
312
  | `doctor` | Diagnose invalid or missing Guard artifacts. |
299
313
  | `history` / `history-diff` | Inspect project model, module graph, and architecture-edge evolution. |
300
314
  | `impact <files...>` | Explain dependencies, transitive dependents, capabilities, and contracts affected by files. |
315
+ | `policy explain <id>` | Explain a policy item, its evidence, and approval history. |
301
316
  | `rules` / `contracts` | Approve or reject proposed policy. |
302
317
  | `baseline` | Review or intentionally accept existing findings. |
303
318
 
319
+ Use `analyze --refresh` after a structural change. `doctor --fix-cache` removes only the disposable
320
+ discovery cache; it does not change rules, contracts, baseline, or source files.
321
+
304
322
  Run `pnpm exec codapult-guard <command> --help` for command-specific options.
305
323
 
306
324
  ## Security and data handling
@@ -1,4 +1,4 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execFileSync } from 'node:child_process';
2
2
  const MAX_OUTPUT_CHARS = 20_000;
3
3
  const MAX_BUFFER_BYTES = 2_000_000;
4
4
  function redactOutput(value) {
@@ -13,9 +13,30 @@ function captureOutput(value) {
13
13
  ? { value: `${redacted.slice(0, MAX_OUTPUT_CHARS)}\n[output truncated]`, truncated: true }
14
14
  : { value: redacted, truncated: false };
15
15
  }
16
+ function parseCommand(command) {
17
+ const parts = command.trim().split(/\s+/).filter(Boolean);
18
+ if (parts.length === 0 || parts.some((part) => /[;&|<>`$()]/.test(part)))
19
+ return undefined;
20
+ const [executable, ...args] = parts;
21
+ const windowsExecutable = process.platform === 'win32' && /^(?:npm|npx|pnpm|yarn|bun)$/.test(executable)
22
+ ? `${executable}.cmd`
23
+ : executable;
24
+ return { executable: windowsExecutable, args };
25
+ }
16
26
  export function runProjectCommand(command, cwd, options = {}) {
27
+ const parsed = parseCommand(command);
28
+ if (!parsed) {
29
+ return {
30
+ command,
31
+ status: 'failed',
32
+ passed: false,
33
+ exitCode: 2,
34
+ stdout: '',
35
+ stderr: 'Unsafe or empty project command rejected.',
36
+ };
37
+ }
17
38
  try {
18
- const stdout = execSync(command, {
39
+ const stdout = execFileSync(parsed.executable, parsed.args, {
19
40
  cwd,
20
41
  env: { ...process.env, ...options.env },
21
42
  stdio: 'pipe',
@@ -2,7 +2,9 @@ import { type GuardToolMode } from '../../core/guard.js';
2
2
  export declare function guardInitCommand(options?: {
3
3
  force?: boolean;
4
4
  }): void;
5
- export declare function guardAnalyzeCommand(): void;
5
+ export declare function guardAnalyzeCommand(_options?: {
6
+ refresh?: boolean;
7
+ }): void;
6
8
  export declare function guardProposeCommand(options?: {
7
9
  json?: boolean;
8
10
  }): void;
@@ -11,6 +13,7 @@ export declare function guardInstallAgentCommand(target?: string, options?: {
11
13
  }): void;
12
14
  export declare function guardDoctorCommand(options?: {
13
15
  json?: boolean;
16
+ fixCache?: boolean;
14
17
  }): void;
15
18
  export declare function guardHistoryCommand(): void;
16
19
  export declare function guardImpactCommand(files: string[], options?: {
@@ -44,6 +47,9 @@ export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove
44
47
  export declare function guardRulesApproveCommand(ids?: string, options?: {
45
48
  all?: boolean;
46
49
  }): void;
50
+ export declare function guardPolicyExplainCommand(id: string, options?: {
51
+ json?: boolean;
52
+ }): void;
47
53
  export declare function guardContractsApproveCommand(ids?: string, options?: {
48
54
  all?: boolean;
49
55
  }): void;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { relative, resolve } from 'node:path';
3
- import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, writeGuardConfig, writeGuardProposals, recordGuardProposalDecision, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardContracts, writeProjectModel, writeGuardMemory, writeProjectSnapshot, } from '../../core/guard.js';
3
+ import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, writeGuardConfig, writeGuardProposals, recordGuardProposalDecision, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardContracts, writeProjectModel, writeGuardMemory, writeProjectSnapshot, } from '../../core/guard.js';
4
+ import { guardErrorPayload } from '../../core/errors.js';
4
5
  import { runGuardVerification, } from '../../core/verification/verify.js';
5
6
  import { diagnoseGuard } from '../../core/analysis/doctor.js';
6
7
  import { diffGuardSnapshots, listGuardSnapshots } from '../../core/history/history.js';
@@ -13,11 +14,33 @@ function renderFindings(findings) {
13
14
  const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
14
15
  printer(`${finding.file}:${finding.line} [${finding.ruleId}] ${finding.message}`);
15
16
  dim(` import: ${finding.importPath}`);
17
+ if (finding.resolvedPath)
18
+ dim(` resolved: ${finding.resolvedPath}`);
16
19
  }
17
20
  }
18
21
  function getRoot() {
19
22
  return findGuardRoot();
20
23
  }
24
+ function loadConfigSafely(root, options = {}) {
25
+ try {
26
+ return { config: loadGuardConfig(root), invalid: false };
27
+ }
28
+ catch (error) {
29
+ const message = error instanceof Error ? error.message : 'Guard configuration is invalid.';
30
+ const payload = guardErrorPayload('GUARD_CONFIG_INVALID', message, {
31
+ configured: false,
32
+ outcome: 'error',
33
+ recoverable: true,
34
+ hint: 'Repair or remove the invalid Guard artifact, then run `codapult-guard doctor`.',
35
+ });
36
+ if (options.json || options.machine)
37
+ console.log(JSON.stringify(payload, null, 2));
38
+ else
39
+ fail(`${payload.message} Run \`codapult-guard doctor\` for details.`);
40
+ process.exitCode = 1;
41
+ return { invalid: true };
42
+ }
43
+ }
21
44
  function readRequirement(root, file) {
22
45
  if (!file)
23
46
  return undefined;
@@ -74,7 +97,7 @@ export function guardInitCommand(options = {}) {
74
97
  info(`${config.rules.length} rule(s) generated (${proposedRules} proposed); ${report.scannedFiles} source file(s) scanned.`);
75
98
  dim('Existing findings are baselined. New violations will be reported by `codapult-guard check`.');
76
99
  }
77
- export function guardAnalyzeCommand() {
100
+ export function guardAnalyzeCommand(_options = {}) {
78
101
  const root = getRoot();
79
102
  const { model: projectModel, metrics } = discoverProjectWithMetrics(root, {
80
103
  persistCache: true,
@@ -91,7 +114,10 @@ export function guardAnalyzeCommand() {
91
114
  export function guardProposeCommand(options = {}) {
92
115
  const root = getRoot();
93
116
  const model = discoverProject(root);
94
- const config = loadGuardConfig(root) ?? buildGeneratedGuardConfig(model);
117
+ const loaded = loadConfigSafely(root, options);
118
+ if (loaded.invalid)
119
+ return;
120
+ const config = loaded.config ?? buildGeneratedGuardConfig(model);
95
121
  const proposals = buildGuardProposals(model, config);
96
122
  const previous = loadGuardProposals(root);
97
123
  writeGuardProposals(root, {
@@ -135,9 +161,12 @@ export function guardInstallAgentCommand(target = 'generic', options = {}) {
135
161
  process.exitCode = 0;
136
162
  }
137
163
  export function guardDoctorCommand(options = {}) {
164
+ if (options.fixCache)
165
+ clearDiscoveryCache(getRoot());
138
166
  const report = diagnoseGuard(getRoot());
167
+ const result = options.fixCache ? { ...report, cacheFixed: true } : report;
139
168
  if (options.json) {
140
- console.log(JSON.stringify(report, null, 2));
169
+ console.log(JSON.stringify(result, null, 2));
141
170
  }
142
171
  else {
143
172
  heading('Codapult Guard Doctor');
@@ -154,7 +183,7 @@ export function guardDoctorCommand(options = {}) {
154
183
  else
155
184
  success('Guard state is healthy.');
156
185
  }
157
- process.exitCode = report.status === 'fail' ? 1 : 0;
186
+ process.exitCode = result.status === 'fail' ? 1 : 0;
158
187
  }
159
188
  export function guardHistoryCommand() {
160
189
  const snapshots = listGuardSnapshots(getRoot());
@@ -174,7 +203,10 @@ export function guardImpactCommand(files, options = {}) {
174
203
  process.exitCode = 1;
175
204
  return;
176
205
  }
177
- const result = analyzeProjectImpact(discoverProject(root), files, loadGuardConfig(root)?.contracts ?? []);
206
+ const loaded = loadConfigSafely(root, options);
207
+ if (loaded.invalid)
208
+ return;
209
+ const result = analyzeProjectImpact(discoverProject(root), files, loaded.config?.contracts ?? []);
178
210
  if (options.json) {
179
211
  console.log(JSON.stringify({ status: 'ok', ...result }, null, 2));
180
212
  return;
@@ -268,10 +300,20 @@ export function guardVerifyCommand(options = {}) {
268
300
  }
269
301
  export function guardCheckCommand(options = {}) {
270
302
  const root = getRoot();
271
- const config = loadGuardConfig(root);
303
+ const loaded = loadConfigSafely(root, { ...options, machine: options.json || options.sarif });
304
+ if (loaded.invalid)
305
+ return;
306
+ const config = loaded.config;
272
307
  if (!config) {
273
308
  if (options.json) {
274
- console.log(JSON.stringify({ configured: false, error: 'Guard is not initialized' }));
309
+ console.log(JSON.stringify({
310
+ status: 'error',
311
+ outcome: 'not-configured',
312
+ configured: false,
313
+ errorCode: 'GUARD_NOT_CONFIGURED',
314
+ message: 'Guard is not initialized.',
315
+ recoverable: true,
316
+ }));
275
317
  process.exitCode = 1;
276
318
  return;
277
319
  }
@@ -328,10 +370,20 @@ export function guardCheckCommand(options = {}) {
328
370
  }
329
371
  export function guardAuditCommand(options = {}) {
330
372
  const root = getRoot();
331
- const config = loadGuardConfig(root);
373
+ const loaded = loadConfigSafely(root, options);
374
+ if (loaded.invalid)
375
+ return;
376
+ const config = loaded.config;
332
377
  if (!config) {
333
378
  if (options.json) {
334
- console.log(JSON.stringify({ configured: false, error: 'Guard is not initialized' }));
379
+ console.log(JSON.stringify({
380
+ status: 'error',
381
+ outcome: 'not-configured',
382
+ configured: false,
383
+ errorCode: 'GUARD_NOT_CONFIGURED',
384
+ message: 'Guard is not initialized.',
385
+ recoverable: true,
386
+ }));
335
387
  process.exitCode = 1;
336
388
  return;
337
389
  }
@@ -388,9 +440,12 @@ export function guardBaselineCommand(action, ids, options = {}) {
388
440
  process.exitCode = 0;
389
441
  return;
390
442
  }
443
+ const loaded = options.all ? loadConfigSafely(root, options) : { invalid: false };
444
+ if (loaded.invalid)
445
+ return;
391
446
  const selected = options.all
392
447
  ? [
393
- ...scanGuard(root, loadGuardConfig(root) ?? buildGeneratedGuardConfig(discoverProject(root)), {
448
+ ...scanGuard(root, loaded.config ?? buildGeneratedGuardConfig(discoverProject(root)), {
394
449
  includeArchitectureInsights: true,
395
450
  }).findings.map((finding) => finding.fingerprint),
396
451
  ]
@@ -415,7 +470,10 @@ export function guardBaselineCommand(action, ids, options = {}) {
415
470
  }
416
471
  export function guardRulesApproveCommand(ids, options = {}) {
417
472
  const root = getRoot();
418
- const config = loadGuardConfig(root);
473
+ const loaded = loadConfigSafely(root);
474
+ if (loaded.invalid)
475
+ return;
476
+ const config = loaded.config;
419
477
  if (!config) {
420
478
  fail('Guard is not initialized. Run `codapult-guard init` first.');
421
479
  process.exitCode = 1;
@@ -442,9 +500,55 @@ export function guardRulesApproveCommand(ids, options = {}) {
442
500
  success(`Activated ${rule.id}`);
443
501
  process.exitCode = 0;
444
502
  }
503
+ export function guardPolicyExplainCommand(id, options = {}) {
504
+ const root = getRoot();
505
+ const loaded = loadConfigSafely(root, options);
506
+ if (loaded.invalid)
507
+ return;
508
+ const config = loaded.config;
509
+ const proposals = loadGuardProposals(root);
510
+ if (!config) {
511
+ fail('Guard is not initialized. Run `codapult-guard init` first.');
512
+ process.exitCode = 1;
513
+ return;
514
+ }
515
+ const item = [...config.rules, ...(config.contracts ?? [])].find((entry) => entry.id === id);
516
+ const proposed = [...(proposals?.rules ?? []), ...(proposals?.contracts ?? [])].find((entry) => entry.id === id);
517
+ if (!item && !proposed) {
518
+ fail(`Policy item not found: ${id}`);
519
+ process.exitCode = 1;
520
+ return;
521
+ }
522
+ const result = {
523
+ id,
524
+ active: item?.status !== 'proposed' && item !== undefined,
525
+ definition: item ?? proposed,
526
+ proposal: proposed,
527
+ decisions: proposals?.decisions?.filter((decision) => decision.id === id) ?? [],
528
+ };
529
+ if (options.json)
530
+ console.log(JSON.stringify(result, null, 2));
531
+ else {
532
+ heading(`Guard Policy: ${id}`);
533
+ info(`Status: ${result.active ? 'active' : 'proposed'}`);
534
+ const definition = result.definition;
535
+ if (definition && 'description' in definition)
536
+ dim(definition.description);
537
+ if (definition && 'statement' in definition)
538
+ dim(definition.statement);
539
+ if ((definition?.evidence?.length ?? 0) > 0)
540
+ dim(`Evidence: ${definition?.evidence?.join(', ')}`);
541
+ if (result.decisions.length > 0)
542
+ dim(`Decisions: ${result.decisions.length}`);
543
+ }
544
+ process.exitCode = 0;
545
+ }
445
546
  export function guardContractsApproveCommand(ids, options = {}) {
446
547
  const root = getRoot();
447
- const config = loadGuardConfig(root);
548
+ const loaded = loadConfigSafely(root);
549
+ if (loaded.invalid)
550
+ return;
551
+ const config = loaded.config;
448
552
  if (!config) {
449
553
  fail('Guard is not initialized. Run `codapult-guard init` first.');
450
554
  process.exitCode = 1;
@@ -477,7 +581,10 @@ export function guardContractsApproveCommand(ids, options = {}) {
477
581
  }
478
582
  export function guardContractsRejectCommand(ids, options = {}) {
479
583
  const root = getRoot();
480
- const config = loadGuardConfig(root);
584
+ const loaded = loadConfigSafely(root);
585
+ if (loaded.invalid)
586
+ return;
587
+ const config = loaded.config;
481
588
  if (!config) {
482
589
  fail('Guard is not initialized. Run `codapult-guard init` first.');
483
590
  process.exitCode = 1;
@@ -512,7 +619,10 @@ export function guardReviewCommand(options = {}) {
512
619
  const requirement = readRequirement(root, options.requirement);
513
620
  if (options.requirement && !requirement)
514
621
  return;
515
- const config = loadGuardConfig(root);
622
+ const loaded = loadConfigSafely(root);
623
+ if (loaded.invalid)
624
+ return;
625
+ const config = loaded.config;
516
626
  if (!config) {
517
627
  fail(`Guard is not initialized. Run \`codapult-guard init\` first.`);
518
628
  process.exitCode = 1;
package/dist/cli/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
- import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardProposeCommand, guardReviewCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
4
+ import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
5
5
  import { config } from '../core/config.js';
6
+ import { GuardConfigError, GuardStateBusyError } from '../core/guard.js';
7
+ import { guardErrorPayload } from '../core/errors.js';
6
8
  const program = new Command()
7
9
  .name(config.commandName)
8
10
  .description('Local-first architecture guardrails for JavaScript and TypeScript projects')
@@ -15,10 +17,10 @@ const program = new Command()
15
17
  // `codapult-guard init`, not `codapult-guard guard init`.
16
18
  const guard = program;
17
19
  guard.command('init').option('--force').action(guardInitCommand);
18
- guard.command('analyze').action(guardAnalyzeCommand);
20
+ guard.command('analyze').option('--refresh').action(guardAnalyzeCommand);
19
21
  guard.command('propose').option('--json').action(guardProposeCommand);
20
22
  guard.command('install-agent [target]').option('--json').action(guardInstallAgentCommand);
21
- guard.command('doctor').option('--json').action(guardDoctorCommand);
23
+ guard.command('doctor').option('--json').option('--fix-cache').action(guardDoctorCommand);
22
24
  guard.command('history').action(guardHistoryCommand);
23
25
  guard.command('history-diff <from> <to>').option('--json').action(guardHistoryDiffCommand);
24
26
  guard.command('impact <files...>').option('--json').action(guardImpactCommand);
@@ -47,6 +49,8 @@ guard
47
49
  .action(guardReviewCommand);
48
50
  const rules = guard.command('rules');
49
51
  rules.command('approve [ids]').option('--all').action(guardRulesApproveCommand);
52
+ const policy = guard.command('policy');
53
+ policy.command('explain <id>').option('--json').action(guardPolicyExplainCommand);
50
54
  const contracts = guard.command('contracts');
51
55
  contracts.command('approve [ids]').option('--all').action(guardContractsApproveCommand);
52
56
  contracts.command('reject [ids]').option('--all').action(guardContractsRejectCommand);
@@ -73,4 +77,23 @@ program
73
77
  .action(async () => {
74
78
  await import('../mcp/server.js');
75
79
  });
76
- program.parse();
80
+ try {
81
+ program.parse();
82
+ }
83
+ catch (error) {
84
+ if (error instanceof GuardConfigError || error instanceof GuardStateBusyError) {
85
+ const isBusy = error instanceof GuardStateBusyError;
86
+ console.error(JSON.stringify(guardErrorPayload(isBusy ? 'GUARD_STATE_BUSY' : 'GUARD_CONFIG_INVALID', error.message, {
87
+ configured: !isBusy,
88
+ outcome: 'error',
89
+ recoverable: true,
90
+ hint: isBusy
91
+ ? 'Retry after the other Guard process finishes.'
92
+ : 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
93
+ }), null, 2));
94
+ process.exitCode = 1;
95
+ }
96
+ else {
97
+ throw error;
98
+ }
99
+ }
@@ -22,12 +22,18 @@ function parseJson(root, path) {
22
22
  export function diagnoseGuard(root) {
23
23
  const initialized = existsSync(resolve(root, GUARD_BASELINE_FILE));
24
24
  const items = requiredArtifacts.map((path) => {
25
- const guardConfig = path === GUARD_CONTRACTS_FILE ? loadGuardConfig(root) : undefined;
26
25
  if (!existsSync(resolve(root, path))) {
27
26
  return { path, status: 'missing', message: 'Artifact is missing.' };
28
27
  }
29
- if (path === GUARD_RULES_FILE && !loadGuardConfig(root)) {
30
- return { path, status: 'invalid', message: 'Guard rules are invalid or unsupported.' };
28
+ if (path === GUARD_RULES_FILE) {
29
+ try {
30
+ if (!loadGuardConfig(root)) {
31
+ return { path, status: 'invalid', message: 'Guard rules are invalid or unsupported.' };
32
+ }
33
+ }
34
+ catch {
35
+ return { path, status: 'invalid', message: 'Guard rules are invalid or unsupported.' };
36
+ }
31
37
  }
32
38
  if (path === GUARD_PROJECT_FILE && !loadProjectModel(root)) {
33
39
  return { path, status: 'invalid', message: 'Project model is invalid or unsupported.' };
@@ -39,8 +45,16 @@ export function diagnoseGuard(root) {
39
45
  return { path, status: 'invalid', message: 'Guard contracts are invalid or unsupported.' };
40
46
  }
41
47
  if (path === GUARD_CONTRACTS_FILE &&
42
- guardConfig &&
43
- validateGuardContracts(root, guardConfig.contracts).length > 0) {
48
+ (() => {
49
+ try {
50
+ const guardConfig = loadGuardConfig(root);
51
+ return (guardConfig !== undefined &&
52
+ validateGuardContracts(root, guardConfig.contracts).length > 0);
53
+ }
54
+ catch {
55
+ return true;
56
+ }
57
+ })()) {
44
58
  return { path, status: 'invalid', message: 'Guard contract scopes or references are stale.' };
45
59
  }
46
60
  if (path === GUARD_PROPOSALS_FILE && !isGuardProposalFile(parseJson(root, path))) {
@@ -11,6 +11,7 @@ export interface ModuleRecord {
11
11
  exports: string[];
12
12
  calls: string[];
13
13
  resolvedImports: string[];
14
+ resolvedImportMap?: Record<string, string>;
14
15
  dynamicImports: string[];
15
16
  declarations: {
16
17
  classes: number;
@@ -44,6 +45,8 @@ export interface ProjectModel {
44
45
  private: boolean;
45
46
  scripts: Record<string, string>;
46
47
  dependencies: string[];
48
+ exports?: string[];
49
+ projectReferences?: string[];
47
50
  }[];
48
51
  };
49
52
  files: ProjectFileRecord[];
@@ -122,6 +125,17 @@ export interface DiscoveryMetrics {
122
125
  changedFiles: number;
123
126
  reusedModules: number;
124
127
  }
128
+ export interface DiscoveryOptions {
129
+ persistCache?: boolean;
130
+ /** Maximum number of files included in one model. */
131
+ maxFiles?: number;
132
+ /** Maximum size of one included file in bytes. */
133
+ maxFileBytes?: number;
134
+ }
135
+ export declare class DiscoveryLimitError extends Error {
136
+ constructor(message: string);
137
+ }
138
+ export declare function clearDiscoveryCache(root: string): void;
125
139
  export declare function findGuardRoot(from?: string): string;
126
140
  /** Build the internal module graph from AST resolution plus conservative fallback resolution.
127
141
  *
@@ -130,12 +144,8 @@ export declare function findGuardRoot(from?: string): string;
130
144
  * no resolved imports silently drops unresolved edges from mixed projects.
131
145
  */
132
146
  export declare function buildModuleTargetGraph(modules: ModuleRecord[]): Map<string, string[]>;
133
- export declare function discoverProject(root: string, options?: {
134
- persistCache?: boolean;
135
- }): ProjectModel;
136
- export declare function discoverProjectWithMetrics(root: string, options?: {
137
- persistCache?: boolean;
138
- }): {
147
+ export declare function discoverProject(root: string, options?: DiscoveryOptions): ProjectModel;
148
+ export declare function discoverProjectWithMetrics(root: string, options?: DiscoveryOptions): {
139
149
  model: ProjectModel;
140
150
  metrics: DiscoveryMetrics;
141
151
  };