@codapult/guard 0.1.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.
@@ -0,0 +1,73 @@
1
+ import { buildModuleTargetGraph } from '../discovery/discovery.js';
2
+ function matchesScope(path, scope) {
3
+ return path === scope || path.startsWith(`${scope.replace(/\\/g, '/')}/`);
4
+ }
5
+ function walk(graph, starts) {
6
+ const visited = new Set();
7
+ const queue = [...starts];
8
+ while (queue.length > 0) {
9
+ const current = queue.shift();
10
+ if (!current || visited.has(current))
11
+ continue;
12
+ visited.add(current);
13
+ for (const next of graph.get(current) ?? []) {
14
+ if (!visited.has(next))
15
+ queue.push(next);
16
+ }
17
+ }
18
+ return visited;
19
+ }
20
+ /**
21
+ * Computes the blast radius of a change from the discovered graph.
22
+ *
23
+ * The graph direction is importer -> imported module. Therefore `dependencies` are what the
24
+ * changed code relies on, while `dependents` are callers that can regress when the changed
25
+ * code changes. Both directions matter for an AI review packet.
26
+ */
27
+ export function analyzeProjectImpact(model, files, contracts = []) {
28
+ const requestedFiles = [...new Set([...files].map((file) => file.replace(/\\/g, '/')))].sort();
29
+ const modulePaths = new Set(model.modules.map((module) => module.path));
30
+ const directModules = requestedFiles.filter((file) => modulePaths.has(file));
31
+ const graph = buildModuleTargetGraph(model.modules);
32
+ const reverse = new Map();
33
+ for (const [from, targets] of graph) {
34
+ for (const to of targets) {
35
+ const callers = reverse.get(to) ?? [];
36
+ callers.push(from);
37
+ reverse.set(to, callers);
38
+ }
39
+ }
40
+ const dependencies = walk(graph, directModules);
41
+ const dependencyList = [...dependencies].filter((path) => !directModules.includes(path)).sort();
42
+ const dependents = walk(reverse, directModules);
43
+ const dependentList = [...dependents].filter((path) => !directModules.includes(path)).sort();
44
+ const affected = new Set([...directModules, ...dependencyList, ...dependentList]);
45
+ const impactPaths = model.insights.impactPaths.filter((impact) => impact.entrypoint &&
46
+ (affected.has(impact.entrypoint) || impact.files.some((file) => affected.has(file))));
47
+ const capabilities = Object.entries(model.capabilities)
48
+ .filter(([, signal]) => signal.files.some((file) => affected.has(file)))
49
+ .map(([id]) => id)
50
+ .sort();
51
+ const relevantContracts = contracts.filter((contract) => {
52
+ const scopes = [...(contract.scope ?? []), ...(contract.entrypoints ?? [])];
53
+ return (scopes.length === 0 ||
54
+ scopes.some((scope) => [...affected].some((file) => matchesScope(file, scope))));
55
+ });
56
+ const dependencyEdges = model.insights.dependencyEdges.filter((edge) => affected.has(edge.from) || affected.has(edge.to));
57
+ const affectedLayers = new Set(Object.entries(model.insights.layers)
58
+ .filter(([, paths]) => paths.some((path) => affected.has(path)))
59
+ .map(([layer]) => layer));
60
+ const layerEdges = model.insights.layerEdges.filter((edge) => affectedLayers.has(edge.from) || affectedLayers.has(edge.to));
61
+ return {
62
+ requestedFiles,
63
+ directModules,
64
+ dependencies: dependencyList,
65
+ dependents: dependentList,
66
+ affectedModules: [...affected].sort(),
67
+ impactPaths,
68
+ capabilities,
69
+ relevantContracts,
70
+ dependencyEdges,
71
+ layerEdges,
72
+ };
73
+ }
@@ -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,13 +125,27 @@ export interface DiscoveryMetrics {
122
125
  changedFiles: number;
123
126
  reusedModules: number;
124
127
  }
125
- export declare function findGuardRoot(from?: string): string;
126
- export declare function discoverProject(root: string, options?: {
127
- persistCache?: boolean;
128
- }): ProjectModel;
129
- export declare function discoverProjectWithMetrics(root: string, options?: {
128
+ export interface DiscoveryOptions {
130
129
  persistCache?: boolean;
131
- }): {
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;
139
+ export declare function findGuardRoot(from?: string): string;
140
+ /** Build the internal module graph from AST resolution plus conservative fallback resolution.
141
+ *
142
+ * TypeScript resolution is authoritative when available, but it is not complete for every
143
+ * JavaScript/configuration shape. The fallback must be additive: using it only when there are
144
+ * no resolved imports silently drops unresolved edges from mixed projects.
145
+ */
146
+ export declare function buildModuleTargetGraph(modules: ModuleRecord[]): Map<string, string[]>;
147
+ export declare function discoverProject(root: string, options?: DiscoveryOptions): ProjectModel;
148
+ export declare function discoverProjectWithMetrics(root: string, options?: DiscoveryOptions): {
132
149
  model: ProjectModel;
133
150
  metrics: DiscoveryMetrics;
134
151
  };
@@ -1,9 +1,17 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, renameSync, statSync, writeFileSync, } from 'node:fs';
4
4
  import { basename, dirname, extname, join, normalize, relative, resolve } from 'node:path';
5
5
  import { Project, SyntaxKind } from 'ts-morph';
6
6
  import { config } from '../config.js';
7
+ export class DiscoveryLimitError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'DiscoveryLimitError';
11
+ }
12
+ }
13
+ const DEFAULT_MAX_FILES = 100_000;
14
+ const DEFAULT_MAX_FILE_BYTES = 25 * 1024 * 1024;
7
15
  const IGNORED_DIRECTORIES = new Set([
8
16
  '.git',
9
17
  '.next',
@@ -40,12 +48,16 @@ const CONFIG_NAMES = new Set([
40
48
  ]);
41
49
  const discoveryCache = new Map();
42
50
  const discoveryReuse = new Map();
51
+ const discoveryCacheHits = new Map();
43
52
  // Bump when the persisted model shape changes; old cache entries must never
44
53
  // bypass discovery and return an incomplete ProjectModel.
45
54
  const DISCOVERY_CACHE_VERSION = 3;
46
55
  function discoveryCachePath(root) {
47
56
  return resolve(root, `.${config.appName}/guard/cache.json`);
48
57
  }
58
+ export function clearDiscoveryCache(root) {
59
+ rmSync(discoveryCachePath(root), { force: true });
60
+ }
49
61
  function contentHash(root, path) {
50
62
  return createHash('sha256')
51
63
  .update(readFileSync(resolve(root, path)))
@@ -121,20 +133,45 @@ function workspacePackages(root, files) {
121
133
  ...asStringRecord(manifest.devDependencies),
122
134
  ...asStringRecord(manifest.peerDependencies),
123
135
  }).sort(),
136
+ ...(manifest.exports !== undefined
137
+ ? {
138
+ exports: Array.isArray(manifest.exports)
139
+ ? manifest.exports.filter((item) => typeof item === 'string')
140
+ : manifest.exports !== null && typeof manifest.exports === 'object'
141
+ ? Object.keys(manifest.exports)
142
+ : [],
143
+ }
144
+ : {}),
145
+ ...(Array.isArray(readJsonObject(resolve(root, dirname(path), 'tsconfig.json')).references)
146
+ ? {
147
+ projectReferences: readJsonObject(resolve(root, dirname(path), 'tsconfig.json'))
148
+ .references
149
+ .filter((reference) => reference !== null &&
150
+ typeof reference === 'object' &&
151
+ typeof reference.path === 'string')
152
+ .map((reference) => reference.path)
153
+ .sort(),
154
+ }
155
+ : {}),
124
156
  };
125
157
  })
126
158
  .sort((left, right) => left.path.localeCompare(right.path));
127
159
  }
128
- function listFiles(root, directory = root) {
160
+ function listFiles(root, directory = root, maxFiles = DEFAULT_MAX_FILES) {
129
161
  const result = [];
130
162
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
131
163
  if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name))
132
164
  continue;
133
165
  const path = resolve(directory, entry.name);
134
166
  if (entry.isDirectory())
135
- result.push(...listFiles(root, path));
136
- else if (entry.isFile() && !ignoredFile(path))
167
+ result.push(...listFiles(root, path, maxFiles - result.length));
168
+ else if (entry.isFile() && !ignoredFile(path)) {
137
169
  result.push(relative(root, path));
170
+ if (result.length > maxFiles) {
171
+ throw new DiscoveryLimitError(`Project contains more than ${maxFiles} files; discovery limit was reached. ` +
172
+ 'Use an ignore file or raise maxFiles deliberately.');
173
+ }
174
+ }
138
175
  }
139
176
  return result.sort();
140
177
  }
@@ -169,7 +206,54 @@ function resolveInternalImport(from, importPath, modules) {
169
206
  ];
170
207
  return candidates.find((candidate) => modules.has(candidate));
171
208
  }
172
- function parseModule(project, root, path, projectFiles) {
209
+ function pathAliases(root) {
210
+ return listFiles(root)
211
+ .filter((path) => basename(path) === 'tsconfig.json' || basename(path) === 'jsconfig.json')
212
+ .flatMap((path) => {
213
+ const tsconfigJson = readJsonObject(resolve(root, path));
214
+ const compilerOptions = tsconfigJson.compilerOptions !== null && typeof tsconfigJson.compilerOptions === 'object'
215
+ ? tsconfigJson.compilerOptions
216
+ : {};
217
+ const paths = compilerOptions.paths;
218
+ if (paths === null || typeof paths !== 'object')
219
+ return [];
220
+ const baseUrl = typeof compilerOptions.baseUrl === 'string' ? compilerOptions.baseUrl : '.';
221
+ return Object.entries(paths).flatMap(([pattern, value]) => {
222
+ if (!Array.isArray(value))
223
+ return [];
224
+ const targets = value.filter((item) => typeof item === 'string');
225
+ return targets.length > 0
226
+ ? [{ pattern, baseDirectory: resolve(root, dirname(path), baseUrl), targets }]
227
+ : [];
228
+ });
229
+ });
230
+ }
231
+ function resolveAliasedImport(root, importPath, aliases, modules) {
232
+ for (const alias of aliases) {
233
+ const wildcard = alias.pattern.indexOf('*');
234
+ const prefix = wildcard >= 0 ? alias.pattern.slice(0, wildcard) : alias.pattern;
235
+ const suffix = wildcard >= 0 ? alias.pattern.slice(wildcard + 1) : '';
236
+ if (!importPath.startsWith(prefix) || (suffix && !importPath.endsWith(suffix)))
237
+ continue;
238
+ const replacement = importPath.slice(prefix.length, importPath.length - suffix.length);
239
+ for (const target of alias.targets) {
240
+ const targetPath = target.replace('*', replacement);
241
+ const absoluteBase = resolve(alias.baseDirectory, targetPath);
242
+ const relativeBase = relative(root, absoluteBase);
243
+ const normalizedBase = relativeBase.replace(/^\.\//, '').replaceAll('\\', '/');
244
+ const candidates = [
245
+ normalizedBase,
246
+ ...[...SOURCE_EXTENSIONS].map((extension) => `${normalizedBase}${extension}`),
247
+ ...[...SOURCE_EXTENSIONS].map((extension) => `${normalizedBase}/index${extension}`),
248
+ ];
249
+ const projectCandidate = candidates.find((candidate) => modules.has(candidate));
250
+ if (projectCandidate)
251
+ return projectCandidate;
252
+ }
253
+ }
254
+ return undefined;
255
+ }
256
+ function parseModule(project, root, path, projectFiles, aliases) {
173
257
  if (!SOURCE_EXTENSIONS.has(extname(path)))
174
258
  return undefined;
175
259
  let sourceFile;
@@ -187,6 +271,7 @@ function parseModule(project, root, path, projectFiles) {
187
271
  const dynamicImports = [];
188
272
  const declarations = { classes: 0, functions: 0, interfaces: 0, types: 0, variables: 0 };
189
273
  const resolvedImports = new Set();
274
+ const resolvedImportMap = {};
190
275
  for (const declaration of sourceFile.getImportDeclarations()) {
191
276
  const importPath = declaration.getModuleSpecifierValue();
192
277
  imports.push(importPath);
@@ -200,9 +285,12 @@ function parseModule(project, root, path, projectFiles) {
200
285
  importedSymbols.push(namedImport.getAliasNode()?.getText() ?? namedImport.getName());
201
286
  }
202
287
  const resolved = declaration.getModuleSpecifierSourceFile();
203
- const resolvedPath = resolved && sourceFilePath(root, resolved, projectFiles);
204
- if (resolvedPath)
288
+ const resolvedPath = (resolved && sourceFilePath(root, resolved, projectFiles)) ??
289
+ resolveAliasedImport(root, importPath, aliases, projectFiles);
290
+ if (resolvedPath) {
205
291
  resolvedImports.add(resolvedPath);
292
+ resolvedImportMap[importPath] = resolvedPath;
293
+ }
206
294
  }
207
295
  for (const declaration of sourceFile.getExportDeclarations()) {
208
296
  const moduleSpecifier = declaration.getModuleSpecifierValue();
@@ -210,9 +298,12 @@ function parseModule(project, root, path, projectFiles) {
210
298
  continue;
211
299
  exports.push(moduleSpecifier);
212
300
  const resolved = declaration.getModuleSpecifierSourceFile();
213
- const resolvedPath = resolved && sourceFilePath(root, resolved, projectFiles);
214
- if (resolvedPath)
301
+ const resolvedPath = (resolved && sourceFilePath(root, resolved, projectFiles)) ??
302
+ resolveAliasedImport(root, moduleSpecifier, aliases, projectFiles);
303
+ if (resolvedPath) {
215
304
  resolvedImports.add(resolvedPath);
305
+ resolvedImportMap[moduleSpecifier] = resolvedPath;
306
+ }
216
307
  }
217
308
  declarations.classes = sourceFile.getClasses().length;
218
309
  declarations.functions = sourceFile.getFunctions().length;
@@ -234,9 +325,12 @@ function parseModule(project, root, path, projectFiles) {
234
325
  if (!importPath)
235
326
  continue;
236
327
  dynamicImports.push(importPath);
237
- const resolvedPath = resolveInternalImport(path, importPath, projectFiles);
238
- if (resolvedPath)
328
+ const resolvedPath = resolveInternalImport(path, importPath, projectFiles) ??
329
+ resolveAliasedImport(root, importPath, aliases, projectFiles);
330
+ if (resolvedPath) {
239
331
  resolvedImports.add(resolvedPath);
332
+ resolvedImportMap[importPath] = resolvedPath;
333
+ }
240
334
  }
241
335
  const directives = sourceFile
242
336
  .getStatements()
@@ -257,6 +351,7 @@ function parseModule(project, root, path, projectFiles) {
257
351
  exports: [...new Set(exports)].sort(),
258
352
  calls: [...new Set(calls)].sort(),
259
353
  resolvedImports: [...resolvedImports].sort(),
354
+ resolvedImportMap,
260
355
  dynamicImports: [...new Set(dynamicImports)].sort(),
261
356
  declarations,
262
357
  directives,
@@ -337,16 +432,23 @@ function layerForPath(path) {
337
432
  }
338
433
  return 'other';
339
434
  }
340
- function buildModuleTargetGraph(modules) {
435
+ /** Build the internal module graph from AST resolution plus conservative fallback resolution.
436
+ *
437
+ * TypeScript resolution is authoritative when available, but it is not complete for every
438
+ * JavaScript/configuration shape. The fallback must be additive: using it only when there are
439
+ * no resolved imports silently drops unresolved edges from mixed projects.
440
+ */
441
+ export function buildModuleTargetGraph(modules) {
341
442
  const moduleSet = new Set(modules.map((candidate) => candidate.path));
342
- return new Map(modules.map((module) => [
343
- module.path,
344
- module.resolvedImports.length > 0
345
- ? module.resolvedImports
346
- : module.imports
347
- .map((importPath) => resolveInternalImport(module.path, importPath, moduleSet))
348
- .filter((path) => path !== undefined),
349
- ]));
443
+ return new Map(modules.map((module) => {
444
+ const targets = new Set(module.resolvedImports.filter((path) => moduleSet.has(path)));
445
+ for (const importPath of [...module.imports, ...module.dynamicImports]) {
446
+ const target = resolveInternalImport(module.path, importPath, moduleSet);
447
+ if (target)
448
+ targets.add(target);
449
+ }
450
+ return [module.path, [...targets].sort()];
451
+ }));
350
452
  }
351
453
  function findCycles(modules) {
352
454
  const graph = buildModuleTargetGraph(modules);
@@ -638,9 +740,23 @@ function buildInsights(root, files, modules, sourceFiles, dependencies, astProje
638
740
  function createAstProject(root) {
639
741
  const tsConfigPath = resolve(root, 'tsconfig.json');
640
742
  try {
641
- return existsSync(tsConfigPath)
743
+ const project = existsSync(tsConfigPath)
642
744
  ? new Project({ tsConfigFilePath: tsConfigPath })
643
745
  : new Project({ skipAddingFilesFromTsConfig: true });
746
+ // Monorepos commonly keep compiler options and path aliases per package.
747
+ // Load those projects as well; files still remain bounded by listFiles().
748
+ for (const configPath of listFiles(root).filter((path) => basename(path) === 'tsconfig.json')) {
749
+ const absolutePath = resolve(root, configPath);
750
+ if (absolutePath === tsConfigPath)
751
+ continue;
752
+ try {
753
+ project.addSourceFilesFromTsConfig(absolutePath);
754
+ }
755
+ catch {
756
+ // A package config can be partial or reference an unavailable project.
757
+ }
758
+ }
759
+ return project;
644
760
  }
645
761
  catch {
646
762
  return new Project({ skipAddingFilesFromTsConfig: true });
@@ -651,19 +767,42 @@ export function discoverProject(root, options = {}) {
651
767
  const dependencies = asStringRecord(packageJson.dependencies);
652
768
  const devDependencies = asStringRecord(packageJson.devDependencies);
653
769
  const scripts = asStringRecord(packageJson.scripts);
654
- const allFiles = listFiles(root);
770
+ const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
771
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
772
+ if (!Number.isInteger(maxFiles) || maxFiles < 1) {
773
+ throw new DiscoveryLimitError('Discovery maxFiles must be a positive integer.');
774
+ }
775
+ if (!Number.isInteger(maxFileBytes) || maxFileBytes < 1) {
776
+ throw new DiscoveryLimitError('Discovery maxFileBytes must be a positive integer.');
777
+ }
778
+ const allFiles = listFiles(root, root, maxFiles);
779
+ if (allFiles.length > maxFiles) {
780
+ throw new DiscoveryLimitError(`Project contains ${allFiles.length} files; discovery limit is ${maxFiles}. ` +
781
+ 'Use an ignore file or raise maxFiles deliberately.');
782
+ }
783
+ for (const path of allFiles) {
784
+ const bytes = statSync(resolve(root, path)).size;
785
+ if (bytes > maxFileBytes) {
786
+ throw new DiscoveryLimitError(`File ${path} is ${bytes} bytes; discovery limit is ${maxFileBytes}. ` +
787
+ 'Ignore the file or raise maxFileBytes deliberately.');
788
+ }
789
+ }
655
790
  const signature = JSON.stringify([
656
791
  JSON.stringify(packageJson),
657
792
  allFiles.map((path) => {
658
793
  const stat = statSync(resolve(root, path));
659
- return [path, stat.size, stat.mtimeMs];
794
+ // Size/mtime alone can miss an in-place edit that preserves both values.
795
+ // The model is a correctness boundary, so include content in the cache key.
796
+ return [path, stat.size, stat.mtimeMs, contentHash(root, path)];
660
797
  }),
661
798
  runGit(root, ['rev-parse', 'HEAD']),
662
799
  runGit(root, ['status', '--porcelain']),
663
800
  ]);
664
801
  const cacheKey = resolve(root);
802
+ discoveryCacheHits.set(cacheKey, false);
665
803
  const cached = discoveryCache.get(cacheKey);
666
804
  if (cached?.signature === signature) {
805
+ discoveryCacheHits.set(cacheKey, true);
667
806
  discoveryReuse.set(cacheKey, cached.model.modules.length);
668
807
  return cached.model;
669
808
  }
@@ -676,6 +815,7 @@ export function discoverProject(root, options = {}) {
676
815
  persisted.model.version === 1) {
677
816
  previousModel = persisted.model;
678
817
  if (persisted.signature === signature) {
818
+ discoveryCacheHits.set(cacheKey, true);
679
819
  discoveryCache.set(cacheKey, { signature, model: previousModel });
680
820
  discoveryReuse.set(cacheKey, previousModel.modules.length);
681
821
  return previousModel;
@@ -692,6 +832,7 @@ export function discoverProject(root, options = {}) {
692
832
  }));
693
833
  const projectFiles = new Set(files.map((file) => file.path));
694
834
  const astProject = createAstProject(root);
835
+ const aliases = pathAliases(root);
695
836
  const previousModules = new Map(previousModel?.modules.map((module) => [module.path, module]));
696
837
  const currentShape = files
697
838
  .filter((file) => file.kind === 'source' || file.kind === 'test')
@@ -716,7 +857,7 @@ export function discoverProject(root, options = {}) {
716
857
  reusedModules += 1;
717
858
  return previous;
718
859
  }
719
- return parseModule(astProject, root, file.path, projectFiles);
860
+ return parseModule(astProject, root, file.path, projectFiles, aliases);
720
861
  })
721
862
  .filter((module) => module !== undefined);
722
863
  const sourceFiles = files.filter((file) => file.kind === 'source').map((file) => file.path);
@@ -799,7 +940,6 @@ export function discoverProject(root, options = {}) {
799
940
  }
800
941
  export function discoverProjectWithMetrics(root, options = {}) {
801
942
  const startedAt = Date.now();
802
- const cacheHit = discoveryCache.has(resolve(root));
803
943
  const model = discoverProject(root, options);
804
944
  return {
805
945
  model,
@@ -808,7 +948,7 @@ export function discoverProjectWithMetrics(root, options = {}) {
808
948
  files: model.files.length,
809
949
  modules: model.modules.length,
810
950
  cacheAvailable: existsSync(discoveryCachePath(root)),
811
- cacheHit,
951
+ cacheHit: discoveryCacheHits.get(resolve(root)) ?? false,
812
952
  changedFiles: model.git.changedFiles.length,
813
953
  reusedModules: discoveryReuse.get(resolve(root)) ?? 0,
814
954
  },
@@ -0,0 +1,11 @@
1
+ export type GuardErrorCode = 'GUARD_NOT_CONFIGURED' | 'GUARD_CONFIG_INVALID' | 'GUARD_STATE_BUSY' | 'GUARD_INVALID_INPUT';
2
+ export interface GuardErrorPayload {
3
+ status: 'error';
4
+ outcome: 'not-configured' | 'error';
5
+ configured: boolean;
6
+ errorCode: GuardErrorCode;
7
+ message: string;
8
+ recoverable: boolean;
9
+ hint?: string;
10
+ }
11
+ export declare function guardErrorPayload(errorCode: GuardErrorCode, message: string, options: Pick<GuardErrorPayload, 'configured' | 'recoverable' | 'outcome'> & Partial<Pick<GuardErrorPayload, 'hint'>>): GuardErrorPayload;
@@ -0,0 +1,11 @@
1
+ export function guardErrorPayload(errorCode, message, options) {
2
+ return {
3
+ status: 'error',
4
+ outcome: options.outcome,
5
+ configured: options.configured,
6
+ errorCode,
7
+ message,
8
+ recoverable: options.recoverable,
9
+ ...(options.hint ? { hint: options.hint } : {}),
10
+ };
11
+ }
@@ -1,5 +1,6 @@
1
- import type { ProjectModel } from './discovery/discovery.js';
2
- import { discoverProject, discoverProjectWithMetrics, findGuardRoot } from './discovery/discovery.js';
1
+ import { type ProjectModel } from './discovery/discovery.js';
2
+ import { clearDiscoveryCache, discoverProject, discoverProjectWithMetrics, findGuardRoot } from './discovery/discovery.js';
3
+ import { type GuardImpactAnalysis } from './analysis/impact.js';
3
4
  export type GuardSeverity = 'error' | 'warning' | 'info';
4
5
  export type GuardOutcomeStatus = 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
5
6
  export declare function classifyGuardOutcome(input: {
@@ -10,7 +11,7 @@ export declare function classifyGuardOutcome(input: {
10
11
  }): GuardOutcomeStatus;
11
12
  export type GuardRuleKind = 'forbidden-import' | 'client-forbidden-import';
12
13
  export type GuardRuleStatus = 'active' | 'proposed';
13
- export type GuardContractKind = 'guidance' | 'import-boundary' | 'required-call';
14
+ export type GuardContractKind = 'guidance' | 'import-boundary' | 'required-call' | 'package-boundary';
14
15
  export interface GuardContract {
15
16
  id: string;
16
17
  statement: string;
@@ -24,6 +25,8 @@ export interface GuardContract {
24
25
  mustImport?: string[];
25
26
  mustNotImport?: string[];
26
27
  mustCall?: string[];
28
+ fromPackages?: string[];
29
+ mustNotImportPackages?: string[];
27
30
  status?: GuardRuleStatus;
28
31
  confidence?: 'high' | 'medium' | 'low';
29
32
  evidence?: string[];
@@ -88,9 +91,15 @@ export interface GuardFinding {
88
91
  file: string;
89
92
  line: number;
90
93
  importPath: string;
94
+ resolvedPath?: string;
91
95
  message: string;
92
96
  fingerprint: string;
93
97
  }
98
+ export interface GuardFileChange {
99
+ path: string;
100
+ status: 'added' | 'modified' | 'deleted' | 'renamed';
101
+ previousPath?: string;
102
+ }
94
103
  export interface GuardContractIssue {
95
104
  contractId: string;
96
105
  field: 'scope' | 'reference' | 'definition';
@@ -114,6 +123,7 @@ export interface GuardReviewPacket {
114
123
  outcome: GuardOutcomeStatus;
115
124
  diffBase?: string;
116
125
  changedFiles: string[];
126
+ changes: GuardFileChange[];
117
127
  diff: string;
118
128
  truncated: boolean;
119
129
  redacted: boolean;
@@ -122,6 +132,7 @@ export interface GuardReviewPacket {
122
132
  contracts: GuardContract[];
123
133
  requirement?: string;
124
134
  deterministicFindings: GuardFinding[];
135
+ impact: GuardImpactAnalysis;
125
136
  reviewInstructions: string[];
126
137
  }
127
138
  export declare const GUARD_DIR: string;
@@ -140,6 +151,12 @@ export declare const defaultGuardAgentConfig: GuardAgentConfig;
140
151
  export declare class GuardAlreadyInitializedError extends Error {
141
152
  constructor();
142
153
  }
154
+ export declare class GuardConfigError extends Error {
155
+ constructor(path: string);
156
+ }
157
+ export declare class GuardStateBusyError extends Error {
158
+ constructor(path: string);
159
+ }
143
160
  export declare function isGuardContractsFile(value: unknown): value is {
144
161
  version: 1;
145
162
  contracts: GuardContract[];
@@ -152,7 +169,6 @@ export declare function loadGuardProposals(root: string): GuardProposalFile | un
152
169
  export declare function isGuardAgentConfig(value: unknown): value is GuardAgentConfig;
153
170
  export declare function loadGuardAgentConfig(root: string): GuardAgentConfig;
154
171
  export declare function writeGuardAgentConfig(root: string, agentConfig?: GuardAgentConfig): void;
155
- export declare function loadGuardArtifact(root: string, relativePath: string): unknown;
156
172
  export declare function loadBaseline(root: string): Set<string>;
157
173
  export declare function updateBaseline(root: string, options?: {
158
174
  add?: string[];
@@ -162,6 +178,10 @@ export declare function updateBaseline(root: string, options?: {
162
178
  export declare function writeGuardConfig(root: string, guardConfig: GuardConfig): void;
163
179
  export declare function writeGuardProposals(root: string, proposals: GuardProposalFile): void;
164
180
  export declare function recordGuardProposalDecision(root: string, decisions: Pick<GuardProposalDecision, 'id' | 'type' | 'decision'>[]): void;
181
+ export declare function getPendingGuardProposals(proposals: GuardProposalFile | undefined): {
182
+ id: string;
183
+ type: 'rule' | 'contract';
184
+ }[];
165
185
  export declare function buildGuardProposals(model: ProjectModel, guardConfig: GuardConfig): GuardProposalFile;
166
186
  export declare function buildArchitectureMemory(model: ProjectModel): Record<string, unknown>;
167
187
  export declare function buildConventionsMemory(model: ProjectModel): Record<string, unknown>;
@@ -171,6 +191,7 @@ export declare function writeBaseline(root: string, findings: GuardFinding[], mo
171
191
  export declare function loadProjectModel(root: string): ProjectModel | undefined;
172
192
  export declare function writeProjectModel(root: string, model: ProjectModel): void;
173
193
  export declare function writeProjectSnapshot(root: string, model: ProjectModel): string;
194
+ export declare function loadGuardArtifact(root: string, relativePath: string): unknown;
174
195
  export declare function validateGuardContracts(root: string, contracts?: GuardContract[]): GuardContractIssue[];
175
196
  /** @internal Redacts common credential shapes before a diff enters an AI review packet. */
176
197
  export declare function redactSensitiveText(value: string): {
@@ -186,4 +207,4 @@ export declare function initializeGuard(root: string, options?: {
186
207
  config: GuardConfig;
187
208
  report: GuardReport;
188
209
  };
189
- export { discoverProject, discoverProjectWithMetrics, findGuardRoot };
210
+ export { clearDiscoveryCache, discoverProject, discoverProjectWithMetrics, findGuardRoot };