@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 +18 -0
- package/dist/adapters/command.js +23 -2
- package/dist/cli/commands/guard.d.ts +7 -1
- package/dist/cli/commands/guard.js +125 -15
- package/dist/cli/index.js +27 -4
- package/dist/core/analysis/doctor.js +19 -5
- package/dist/core/discovery/discovery.d.ts +16 -6
- package/dist/core/discovery/discovery.js +150 -17
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/guard.d.ts +18 -5
- package/dist/core/guard.js +226 -57
- package/dist/core/policy/schemas.d.ts +12 -0
- package/dist/core/policy/schemas.js +21 -17
- package/dist/core/verification/verify.d.ts +5 -0
- package/dist/core/verification/verify.js +26 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/tools/guard.js +165 -36
- package/package.json +6 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -645,9 +740,23 @@ function buildInsights(root, files, modules, sourceFiles, dependencies, astProje
|
|
|
645
740
|
function createAstProject(root) {
|
|
646
741
|
const tsConfigPath = resolve(root, 'tsconfig.json');
|
|
647
742
|
try {
|
|
648
|
-
|
|
743
|
+
const project = existsSync(tsConfigPath)
|
|
649
744
|
? new Project({ tsConfigFilePath: tsConfigPath })
|
|
650
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;
|
|
651
760
|
}
|
|
652
761
|
catch {
|
|
653
762
|
return new Project({ skipAddingFilesFromTsConfig: true });
|
|
@@ -658,19 +767,42 @@ export function discoverProject(root, options = {}) {
|
|
|
658
767
|
const dependencies = asStringRecord(packageJson.dependencies);
|
|
659
768
|
const devDependencies = asStringRecord(packageJson.devDependencies);
|
|
660
769
|
const scripts = asStringRecord(packageJson.scripts);
|
|
661
|
-
const
|
|
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
|
+
}
|
|
662
790
|
const signature = JSON.stringify([
|
|
663
791
|
JSON.stringify(packageJson),
|
|
664
792
|
allFiles.map((path) => {
|
|
665
793
|
const stat = statSync(resolve(root, path));
|
|
666
|
-
|
|
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)];
|
|
667
797
|
}),
|
|
668
798
|
runGit(root, ['rev-parse', 'HEAD']),
|
|
669
799
|
runGit(root, ['status', '--porcelain']),
|
|
670
800
|
]);
|
|
671
801
|
const cacheKey = resolve(root);
|
|
802
|
+
discoveryCacheHits.set(cacheKey, false);
|
|
672
803
|
const cached = discoveryCache.get(cacheKey);
|
|
673
804
|
if (cached?.signature === signature) {
|
|
805
|
+
discoveryCacheHits.set(cacheKey, true);
|
|
674
806
|
discoveryReuse.set(cacheKey, cached.model.modules.length);
|
|
675
807
|
return cached.model;
|
|
676
808
|
}
|
|
@@ -683,6 +815,7 @@ export function discoverProject(root, options = {}) {
|
|
|
683
815
|
persisted.model.version === 1) {
|
|
684
816
|
previousModel = persisted.model;
|
|
685
817
|
if (persisted.signature === signature) {
|
|
818
|
+
discoveryCacheHits.set(cacheKey, true);
|
|
686
819
|
discoveryCache.set(cacheKey, { signature, model: previousModel });
|
|
687
820
|
discoveryReuse.set(cacheKey, previousModel.modules.length);
|
|
688
821
|
return previousModel;
|
|
@@ -699,6 +832,7 @@ export function discoverProject(root, options = {}) {
|
|
|
699
832
|
}));
|
|
700
833
|
const projectFiles = new Set(files.map((file) => file.path));
|
|
701
834
|
const astProject = createAstProject(root);
|
|
835
|
+
const aliases = pathAliases(root);
|
|
702
836
|
const previousModules = new Map(previousModel?.modules.map((module) => [module.path, module]));
|
|
703
837
|
const currentShape = files
|
|
704
838
|
.filter((file) => file.kind === 'source' || file.kind === 'test')
|
|
@@ -723,7 +857,7 @@ export function discoverProject(root, options = {}) {
|
|
|
723
857
|
reusedModules += 1;
|
|
724
858
|
return previous;
|
|
725
859
|
}
|
|
726
|
-
return parseModule(astProject, root, file.path, projectFiles);
|
|
860
|
+
return parseModule(astProject, root, file.path, projectFiles, aliases);
|
|
727
861
|
})
|
|
728
862
|
.filter((module) => module !== undefined);
|
|
729
863
|
const sourceFiles = files.filter((file) => file.kind === 'source').map((file) => file.path);
|
|
@@ -806,7 +940,6 @@ export function discoverProject(root, options = {}) {
|
|
|
806
940
|
}
|
|
807
941
|
export function discoverProjectWithMetrics(root, options = {}) {
|
|
808
942
|
const startedAt = Date.now();
|
|
809
|
-
const cacheHit = discoveryCache.has(resolve(root));
|
|
810
943
|
const model = discoverProject(root, options);
|
|
811
944
|
return {
|
|
812
945
|
model,
|
|
@@ -815,7 +948,7 @@ export function discoverProjectWithMetrics(root, options = {}) {
|
|
|
815
948
|
files: model.files.length,
|
|
816
949
|
modules: model.modules.length,
|
|
817
950
|
cacheAvailable: existsSync(discoveryCachePath(root)),
|
|
818
|
-
cacheHit,
|
|
951
|
+
cacheHit: discoveryCacheHits.get(resolve(root)) ?? false,
|
|
819
952
|
changedFiles: model.git.changedFiles.length,
|
|
820
953
|
reusedModules: discoveryReuse.get(resolve(root)) ?? 0,
|
|
821
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
|
+
}
|
package/dist/core/guard.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
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
3
|
import { type GuardImpactAnalysis } from './analysis/impact.js';
|
|
4
4
|
export type GuardSeverity = 'error' | 'warning' | 'info';
|
|
5
5
|
export type GuardOutcomeStatus = 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
|
|
@@ -11,7 +11,7 @@ export declare function classifyGuardOutcome(input: {
|
|
|
11
11
|
}): GuardOutcomeStatus;
|
|
12
12
|
export type GuardRuleKind = 'forbidden-import' | 'client-forbidden-import';
|
|
13
13
|
export type GuardRuleStatus = 'active' | 'proposed';
|
|
14
|
-
export type GuardContractKind = 'guidance' | 'import-boundary' | 'required-call';
|
|
14
|
+
export type GuardContractKind = 'guidance' | 'import-boundary' | 'required-call' | 'package-boundary';
|
|
15
15
|
export interface GuardContract {
|
|
16
16
|
id: string;
|
|
17
17
|
statement: string;
|
|
@@ -25,6 +25,8 @@ export interface GuardContract {
|
|
|
25
25
|
mustImport?: string[];
|
|
26
26
|
mustNotImport?: string[];
|
|
27
27
|
mustCall?: string[];
|
|
28
|
+
fromPackages?: string[];
|
|
29
|
+
mustNotImportPackages?: string[];
|
|
28
30
|
status?: GuardRuleStatus;
|
|
29
31
|
confidence?: 'high' | 'medium' | 'low';
|
|
30
32
|
evidence?: string[];
|
|
@@ -89,6 +91,7 @@ export interface GuardFinding {
|
|
|
89
91
|
file: string;
|
|
90
92
|
line: number;
|
|
91
93
|
importPath: string;
|
|
94
|
+
resolvedPath?: string;
|
|
92
95
|
message: string;
|
|
93
96
|
fingerprint: string;
|
|
94
97
|
}
|
|
@@ -148,6 +151,12 @@ export declare const defaultGuardAgentConfig: GuardAgentConfig;
|
|
|
148
151
|
export declare class GuardAlreadyInitializedError extends Error {
|
|
149
152
|
constructor();
|
|
150
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
|
+
}
|
|
151
160
|
export declare function isGuardContractsFile(value: unknown): value is {
|
|
152
161
|
version: 1;
|
|
153
162
|
contracts: GuardContract[];
|
|
@@ -160,7 +169,6 @@ export declare function loadGuardProposals(root: string): GuardProposalFile | un
|
|
|
160
169
|
export declare function isGuardAgentConfig(value: unknown): value is GuardAgentConfig;
|
|
161
170
|
export declare function loadGuardAgentConfig(root: string): GuardAgentConfig;
|
|
162
171
|
export declare function writeGuardAgentConfig(root: string, agentConfig?: GuardAgentConfig): void;
|
|
163
|
-
export declare function loadGuardArtifact(root: string, relativePath: string): unknown;
|
|
164
172
|
export declare function loadBaseline(root: string): Set<string>;
|
|
165
173
|
export declare function updateBaseline(root: string, options?: {
|
|
166
174
|
add?: string[];
|
|
@@ -170,6 +178,10 @@ export declare function updateBaseline(root: string, options?: {
|
|
|
170
178
|
export declare function writeGuardConfig(root: string, guardConfig: GuardConfig): void;
|
|
171
179
|
export declare function writeGuardProposals(root: string, proposals: GuardProposalFile): void;
|
|
172
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
|
+
}[];
|
|
173
185
|
export declare function buildGuardProposals(model: ProjectModel, guardConfig: GuardConfig): GuardProposalFile;
|
|
174
186
|
export declare function buildArchitectureMemory(model: ProjectModel): Record<string, unknown>;
|
|
175
187
|
export declare function buildConventionsMemory(model: ProjectModel): Record<string, unknown>;
|
|
@@ -179,6 +191,7 @@ export declare function writeBaseline(root: string, findings: GuardFinding[], mo
|
|
|
179
191
|
export declare function loadProjectModel(root: string): ProjectModel | undefined;
|
|
180
192
|
export declare function writeProjectModel(root: string, model: ProjectModel): void;
|
|
181
193
|
export declare function writeProjectSnapshot(root: string, model: ProjectModel): string;
|
|
194
|
+
export declare function loadGuardArtifact(root: string, relativePath: string): unknown;
|
|
182
195
|
export declare function validateGuardContracts(root: string, contracts?: GuardContract[]): GuardContractIssue[];
|
|
183
196
|
/** @internal Redacts common credential shapes before a diff enters an AI review packet. */
|
|
184
197
|
export declare function redactSensitiveText(value: string): {
|
|
@@ -194,4 +207,4 @@ export declare function initializeGuard(root: string, options?: {
|
|
|
194
207
|
config: GuardConfig;
|
|
195
208
|
report: GuardReport;
|
|
196
209
|
};
|
|
197
|
-
export { discoverProject, discoverProjectWithMetrics, findGuardRoot };
|
|
210
|
+
export { clearDiscoveryCache, discoverProject, discoverProjectWithMetrics, findGuardRoot };
|