@neurcode-ai/governance-runtime 0.1.3 → 0.1.5

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.
Files changed (49) hide show
  1. package/dist/admission-provenance.d.ts +111 -0
  2. package/dist/admission-provenance.d.ts.map +1 -0
  3. package/dist/admission-provenance.js +735 -0
  4. package/dist/admission-provenance.js.map +1 -0
  5. package/dist/agent-guard-posture.d.ts +40 -0
  6. package/dist/agent-guard-posture.d.ts.map +1 -0
  7. package/dist/agent-guard-posture.js +117 -0
  8. package/dist/agent-guard-posture.js.map +1 -0
  9. package/dist/agent-invocation-observability.d.ts +47 -0
  10. package/dist/agent-invocation-observability.d.ts.map +1 -0
  11. package/dist/agent-invocation-observability.js +229 -0
  12. package/dist/agent-invocation-observability.js.map +1 -0
  13. package/dist/agent-plan.d.ts +119 -0
  14. package/dist/agent-plan.d.ts.map +1 -0
  15. package/dist/agent-plan.js +590 -0
  16. package/dist/agent-plan.js.map +1 -0
  17. package/dist/agent-runtime-adapter.d.ts +69 -0
  18. package/dist/agent-runtime-adapter.d.ts.map +1 -0
  19. package/dist/agent-runtime-adapter.js +274 -0
  20. package/dist/agent-runtime-adapter.js.map +1 -0
  21. package/dist/ai-change-record.d.ts +185 -0
  22. package/dist/ai-change-record.d.ts.map +1 -0
  23. package/dist/ai-change-record.js +580 -0
  24. package/dist/ai-change-record.js.map +1 -0
  25. package/dist/architecture-graph.d.ts +153 -0
  26. package/dist/architecture-graph.d.ts.map +1 -0
  27. package/dist/architecture-graph.js +646 -0
  28. package/dist/architecture-graph.js.map +1 -0
  29. package/dist/architecture-obligations.d.ts +161 -0
  30. package/dist/architecture-obligations.d.ts.map +1 -0
  31. package/dist/architecture-obligations.js +553 -0
  32. package/dist/architecture-obligations.js.map +1 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +104 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/profile.d.ts +159 -0
  38. package/dist/profile.d.ts.map +1 -0
  39. package/dist/profile.js +611 -0
  40. package/dist/profile.js.map +1 -0
  41. package/dist/session.d.ts +428 -0
  42. package/dist/session.d.ts.map +1 -0
  43. package/dist/session.js +2206 -0
  44. package/dist/session.js.map +1 -0
  45. package/package.json +13 -2
  46. package/src/constraints.ts +0 -828
  47. package/src/index.test.ts +0 -502
  48. package/src/index.ts +0 -463
  49. package/tsconfig.json +0 -19
@@ -0,0 +1,646 @@
1
+ "use strict";
2
+ /**
3
+ * Repository Architecture Graph — V2.
4
+ *
5
+ * Turns the path/owner profile into an architecture-aware model that can reason
6
+ * about module boundaries, ownership, dependency direction, and sensitive
7
+ * surfaces during agentic development.
8
+ *
9
+ * Source-free guarantees:
10
+ * - Import *specifiers* (module strings) may be read locally to infer edges,
11
+ * but raw source, diffs, and file contents are NEVER stored on the graph.
12
+ * The graph holds only module ids, owners, surface tags, and module→module
13
+ * dependency edges — architecture metadata, not code.
14
+ * - Deterministic: same inputs → same `architectureHash`.
15
+ *
16
+ * The extractor + resolver are pure functions so the CLI can read local files,
17
+ * derive specifiers, build edges, and discard the content immediately.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.ARCHITECTURE_GRAPH_SCHEMA_VERSION = void 0;
24
+ exports.moduleIdForPath = moduleIdForPath;
25
+ exports.extractImportSpecifiers = extractImportSpecifiers;
26
+ exports.resolveImportSpecifier = resolveImportSpecifier;
27
+ exports.buildArchitectureGraph = buildArchitectureGraph;
28
+ exports.findModuleForPath = findModuleForPath;
29
+ exports.dependentsOf = dependentsOf;
30
+ exports.dependenciesOf = dependenciesOf;
31
+ exports.modulesInPlay = modulesInPlay;
32
+ exports.deriveGraphObligationSeeds = deriveGraphObligationSeeds;
33
+ exports.isModuleTestSatisfiable = isModuleTestSatisfiable;
34
+ const micromatch_1 = __importDefault(require("micromatch"));
35
+ const node_crypto_1 = require("node:crypto");
36
+ const profile_1 = require("./profile");
37
+ exports.ARCHITECTURE_GRAPH_SCHEMA_VERSION = 2;
38
+ // ── Language + path helpers ───────────────────────────────────────────────────
39
+ const TS_JS_EXT = new Set(['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'mts', 'cts']);
40
+ const PY_EXT = new Set(['py', 'pyi']);
41
+ const SOURCE_EXT = new Set([
42
+ ...TS_JS_EXT,
43
+ ...PY_EXT,
44
+ 'go',
45
+ 'rs',
46
+ 'java',
47
+ 'kt',
48
+ 'rb',
49
+ 'php',
50
+ 'cs',
51
+ // `.sql` is included so migration/database directories form modules even when
52
+ // they hold only SQL files (no imports are extracted from them).
53
+ 'sql',
54
+ ]);
55
+ function extOf(filePath) {
56
+ const m = filePath.match(/\.([a-z0-9]+)$/i);
57
+ return m ? m[1].toLowerCase() : '';
58
+ }
59
+ function languageForExt(ext) {
60
+ if (ext === 'ts' || ext === 'tsx' || ext === 'mts' || ext === 'cts')
61
+ return 'TypeScript';
62
+ if (ext === 'js' || ext === 'jsx' || ext === 'mjs' || ext === 'cjs')
63
+ return 'JavaScript';
64
+ if (ext === 'py' || ext === 'pyi')
65
+ return 'Python';
66
+ if (ext === 'go')
67
+ return 'Go';
68
+ if (ext === 'rs')
69
+ return 'Rust';
70
+ if (ext === 'java')
71
+ return 'Java';
72
+ if (ext === 'kt')
73
+ return 'Kotlin';
74
+ if (ext === 'rb')
75
+ return 'Ruby';
76
+ if (ext === 'php')
77
+ return 'PHP';
78
+ if (ext === 'cs')
79
+ return 'C#';
80
+ if (ext === 'sql')
81
+ return 'SQL';
82
+ return 'unknown';
83
+ }
84
+ function isSourcePath(filePath) {
85
+ return SOURCE_EXT.has(extOf(filePath));
86
+ }
87
+ function normalizePath(filePath) {
88
+ return filePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').trim();
89
+ }
90
+ /**
91
+ * Collapse a file path to a module id using the first `depth` directory
92
+ * segments. Root-level files map to the synthetic module ".".
93
+ *
94
+ * When a repository contains an embedded service/app fixture, preserve the
95
+ * prefix up to a recognizable app root (`src`, `packages`, `services`, etc.)
96
+ * and then apply depth from there. Without this, paths such as
97
+ * `fixtures/demo-svc/src/billing/charge.py` collapse to `fixtures/demo-svc`,
98
+ * mixing billing/auth/migration ownership into one misleading module.
99
+ */
100
+ function moduleIdForPath(filePath, depth = 2) {
101
+ const norm = normalizePath(filePath);
102
+ const segments = norm.split('/').filter(Boolean);
103
+ if (segments.length <= 1)
104
+ return '.';
105
+ const dirSegments = segments.slice(0, -1);
106
+ const effectiveDepth = Math.max(1, depth);
107
+ const topLevelWorkspaceRoot = ['packages', 'services', 'apps', 'web', 'actions'].includes(dirSegments[0] ?? '');
108
+ const embeddedRootIndex = topLevelWorkspaceRoot
109
+ ? -1
110
+ : dirSegments.findIndex((segment, index) => {
111
+ if (index === 0)
112
+ return false;
113
+ return ['src', 'packages', 'services', 'apps', 'web', 'actions', 'migrations'].includes(segment);
114
+ });
115
+ if (embeddedRootIndex > 0) {
116
+ const rootSegment = dirSegments[embeddedRootIndex];
117
+ const rootDepth = rootSegment === 'migrations' ? 1 : effectiveDepth;
118
+ return dirSegments.slice(0, embeddedRootIndex + rootDepth).join('/');
119
+ }
120
+ return dirSegments.slice(0, effectiveDepth).join('/');
121
+ }
122
+ function moduleGlob(moduleId) {
123
+ return moduleId === '.' ? '*' : `${moduleId}/**`;
124
+ }
125
+ function uniqueSorted(values) {
126
+ return Array.from(new Set(values)).sort();
127
+ }
128
+ // ── Import specifier extraction (pure, source-free output) ────────────────────
129
+ const TS_IMPORT_RE = /\b(?:import|export)\s+(?:[^'"`;]*?\sfrom\s+)?['"]([^'"]+)['"]/g;
130
+ const TS_BARE_IMPORT_RE = /\bimport\s+['"]([^'"]+)['"]/g;
131
+ const TS_REQUIRE_RE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
132
+ const TS_DYNAMIC_IMPORT_RE = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
133
+ const PY_IMPORT_RE = /^[ \t]*import\s+([a-zA-Z0-9_.]+)/gm;
134
+ const PY_FROM_RE = /^[ \t]*from\s+(\.*[a-zA-Z0-9_.]*)\s+import\b/gm;
135
+ /**
136
+ * Extract import specifiers (module strings) from a single file's content.
137
+ *
138
+ * Returns only the quoted/dotted module specifiers — never source text. The
139
+ * caller reads file content locally and discards it after calling this.
140
+ */
141
+ function extractImportSpecifiers(filePath, content) {
142
+ if (!content)
143
+ return [];
144
+ const ext = extOf(filePath);
145
+ const found = new Set();
146
+ const collect = (re, input) => {
147
+ const pattern = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
148
+ let match;
149
+ while ((match = pattern.exec(input)) !== null) {
150
+ const spec = match[1]?.trim();
151
+ if (spec)
152
+ found.add(spec);
153
+ }
154
+ };
155
+ if (TS_JS_EXT.has(ext)) {
156
+ collect(TS_IMPORT_RE, content);
157
+ collect(TS_BARE_IMPORT_RE, content);
158
+ collect(TS_REQUIRE_RE, content);
159
+ collect(TS_DYNAMIC_IMPORT_RE, content);
160
+ }
161
+ else if (PY_EXT.has(ext)) {
162
+ collect(PY_IMPORT_RE, content);
163
+ collect(PY_FROM_RE, content);
164
+ }
165
+ return Array.from(found);
166
+ }
167
+ // ── Import resolution (specifier → repo-relative module file) ──────────────────
168
+ const TS_RESOLVE_SUFFIXES = [
169
+ '',
170
+ '.ts',
171
+ '.tsx',
172
+ '.js',
173
+ '.jsx',
174
+ '.mjs',
175
+ '.cjs',
176
+ '.mts',
177
+ '.cts',
178
+ '/index.ts',
179
+ '/index.tsx',
180
+ '/index.js',
181
+ '/index.jsx',
182
+ '/index.mjs',
183
+ ];
184
+ const PY_RESOLVE_SUFFIXES = ['.py', '.pyi', '/__init__.py', '/__init__.pyi'];
185
+ function joinPath(base, rel) {
186
+ const stack = base ? base.split('/') : [];
187
+ for (const part of rel.split('/')) {
188
+ if (part === '' || part === '.')
189
+ continue;
190
+ if (part === '..')
191
+ stack.pop();
192
+ else
193
+ stack.push(part);
194
+ }
195
+ return stack.join('/');
196
+ }
197
+ function dirOf(filePath) {
198
+ const norm = normalizePath(filePath);
199
+ const idx = norm.lastIndexOf('/');
200
+ return idx === -1 ? '' : norm.slice(0, idx);
201
+ }
202
+ /**
203
+ * Resolve an import specifier to a repo-relative source file, if it points to a
204
+ * known in-repo module. External packages (e.g. "fastapi", "react") resolve to
205
+ * null and are intentionally excluded from the internal dependency graph.
206
+ */
207
+ function resolveImportSpecifier(fromFile, specifier, knownPaths) {
208
+ const ext = extOf(fromFile);
209
+ const spec = specifier.trim();
210
+ if (!spec)
211
+ return null;
212
+ // ── TypeScript / JavaScript ──────────────────────────────────────────────
213
+ if (TS_JS_EXT.has(ext)) {
214
+ if (!spec.startsWith('.'))
215
+ return null; // bare/package import → external
216
+ const base = joinPath(dirOf(fromFile), spec.replace(/\/$/, ''));
217
+ for (const suffix of TS_RESOLVE_SUFFIXES) {
218
+ const candidate = `${base}${suffix}`;
219
+ if (knownPaths.has(candidate))
220
+ return candidate;
221
+ }
222
+ return null;
223
+ }
224
+ // ── Python ────────────────────────────────────────────────────────────────
225
+ if (PY_EXT.has(ext)) {
226
+ if (spec.startsWith('.')) {
227
+ // Relative import: leading dots indicate package levels.
228
+ const dots = spec.match(/^\.+/)?.[0].length ?? 0;
229
+ const rest = spec.slice(dots).replace(/\./g, '/');
230
+ let base = dirOf(fromFile);
231
+ for (let i = 1; i < dots; i += 1)
232
+ base = joinPath(base, '..');
233
+ const target = rest ? joinPath(base, rest) : base;
234
+ for (const suffix of PY_RESOLVE_SUFFIXES) {
235
+ const candidate = `${target}${suffix}`;
236
+ if (knownPaths.has(candidate))
237
+ return candidate;
238
+ }
239
+ // `from . import x` → the package's __init__
240
+ if (!rest) {
241
+ for (const suffix of PY_RESOLVE_SUFFIXES) {
242
+ const candidate = `${base}${suffix}`;
243
+ if (knownPaths.has(candidate))
244
+ return candidate;
245
+ }
246
+ }
247
+ return null;
248
+ }
249
+ // Absolute dotted import: try to map onto a repo file (else external).
250
+ const asPath = spec.replace(/\./g, '/');
251
+ for (const suffix of PY_RESOLVE_SUFFIXES) {
252
+ const candidate = `${asPath}${suffix}`;
253
+ if (knownPaths.has(candidate))
254
+ return candidate;
255
+ }
256
+ return null;
257
+ }
258
+ return null;
259
+ }
260
+ // ── Surface detection ──────────────────────────────────────────────────────
261
+ const SENSITIVE_TAG_TO_SURFACE = {
262
+ auth: 'auth',
263
+ crypto: 'crypto',
264
+ secrets: 'secrets',
265
+ payments: 'payments',
266
+ migrations: 'migration',
267
+ security: 'security',
268
+ custom: null,
269
+ };
270
+ const PUBLIC_API_SEGMENTS = new Set([
271
+ 'api',
272
+ 'apis',
273
+ 'routes',
274
+ 'router',
275
+ 'routers',
276
+ 'controller',
277
+ 'controllers',
278
+ 'handler',
279
+ 'handlers',
280
+ 'endpoints',
281
+ 'graphql',
282
+ 'resolvers',
283
+ 'rest',
284
+ ]);
285
+ const DATABASE_SEGMENTS = new Set([
286
+ 'db',
287
+ 'database',
288
+ 'models',
289
+ 'model',
290
+ 'schema',
291
+ 'schemas',
292
+ 'entities',
293
+ 'entity',
294
+ 'repositories',
295
+ 'repository',
296
+ 'dao',
297
+ 'orm',
298
+ ]);
299
+ const MIGRATION_RE = /(^|\/)(migrations?|alembic|flyway|liquibase)(\/|$)/i;
300
+ function surfacesForModule(moduleId, files, sensitiveTags) {
301
+ const surfaces = new Set();
302
+ for (const tag of sensitiveTags) {
303
+ const surface = SENSITIVE_TAG_TO_SURFACE[tag];
304
+ if (surface)
305
+ surfaces.add(surface);
306
+ }
307
+ const segments = moduleId.split('/').filter(Boolean);
308
+ for (const seg of segments) {
309
+ if (PUBLIC_API_SEGMENTS.has(seg))
310
+ surfaces.add('public-api');
311
+ if (DATABASE_SEGMENTS.has(seg))
312
+ surfaces.add('database');
313
+ }
314
+ for (const file of files) {
315
+ if (MIGRATION_RE.test(file))
316
+ surfaces.add('migration');
317
+ if (/\.sql$/i.test(file))
318
+ surfaces.add('database');
319
+ // Next.js / framework API route convention: pages/api or app/api.
320
+ if (/(^|\/)(pages|app)\/api(\/|$)/i.test(file))
321
+ surfaces.add('public-api');
322
+ if (/(^|\/)openapi|swagger|\.proto$/i.test(file))
323
+ surfaces.add('public-api');
324
+ }
325
+ return uniqueSorted(surfaces);
326
+ }
327
+ function dominantLanguage(languages) {
328
+ let best = 'unknown';
329
+ let bestCount = -1;
330
+ for (const [lang, count] of languages) {
331
+ if (count > bestCount || (count === bestCount && lang < best)) {
332
+ best = lang;
333
+ bestCount = count;
334
+ }
335
+ }
336
+ return best;
337
+ }
338
+ function ownersForModuleFiles(files, ownershipBoundaries) {
339
+ const counts = new Map();
340
+ for (const file of files) {
341
+ const owners = (0, profile_1.ownersForPath)(file, ownershipBoundaries);
342
+ if (owners.length === 0)
343
+ continue;
344
+ const sortedOwners = uniqueSorted(owners);
345
+ const key = sortedOwners.join('\u0000');
346
+ const existing = counts.get(key);
347
+ if (existing) {
348
+ existing.count += 1;
349
+ if (file < existing.firstPath)
350
+ existing.firstPath = file;
351
+ }
352
+ else {
353
+ counts.set(key, { owners: sortedOwners, count: 1, firstPath: file });
354
+ }
355
+ }
356
+ const ranked = Array.from(counts.values()).sort((a, b) => {
357
+ if (b.count !== a.count)
358
+ return b.count - a.count;
359
+ return a.firstPath.localeCompare(b.firstPath);
360
+ });
361
+ return ranked[0]?.owners ?? [];
362
+ }
363
+ function sensitiveTagsForModule(moduleId, sensitiveBoundaries) {
364
+ const tags = new Set();
365
+ for (const boundary of sensitiveBoundaries) {
366
+ const prefix = boundary.glob.replace('/**', '').replace('/*', '');
367
+ if (moduleId === prefix || moduleId.startsWith(prefix + '/') || prefix.startsWith(moduleId + '/')) {
368
+ tags.add(boundary.tag);
369
+ }
370
+ }
371
+ return uniqueSorted(tags);
372
+ }
373
+ function moduleApprovalRequired(moduleId, approvalRequiredGlobs) {
374
+ return approvalRequiredGlobs.some((glob) => {
375
+ const prefix = glob.replace('/**', '').replace('/*', '');
376
+ return moduleId === prefix || moduleId.startsWith(prefix + '/') || prefix.startsWith(moduleId + '/');
377
+ });
378
+ }
379
+ /**
380
+ * Build the deterministic repository architecture graph. Pure and source-free:
381
+ * the only edge inputs are import *specifiers*, and only module→module edges
382
+ * are retained.
383
+ */
384
+ function buildArchitectureGraph(input) {
385
+ const moduleDepth = Math.max(1, input.moduleDepth ?? 2);
386
+ const ownershipBoundaries = input.ownershipBoundaries ?? [];
387
+ const sensitiveBoundaries = input.sensitiveBoundaries ?? [];
388
+ const approvalRequiredGlobs = input.approvalRequiredGlobs ?? [];
389
+ const now = input.now || new Date().toISOString();
390
+ const sourcePaths = input.paths.map(normalizePath).filter(isSourcePath);
391
+ const knownPaths = new Set(sourcePaths);
392
+ // 1. Accumulate modules from source paths.
393
+ const accumulators = new Map();
394
+ for (const filePath of sourcePaths) {
395
+ const id = moduleIdForPath(filePath, moduleDepth);
396
+ let acc = accumulators.get(id);
397
+ if (!acc) {
398
+ acc = { id, files: [], languages: new Map() };
399
+ accumulators.set(id, acc);
400
+ }
401
+ acc.files.push(filePath);
402
+ const lang = languageForExt(extOf(filePath));
403
+ acc.languages.set(lang, (acc.languages.get(lang) ?? 0) + 1);
404
+ }
405
+ const modules = Array.from(accumulators.values())
406
+ .map((acc) => {
407
+ const sensitiveTags = sensitiveTagsForModule(acc.id, sensitiveBoundaries);
408
+ const owners = ownersForModuleFiles(acc.files, ownershipBoundaries);
409
+ return {
410
+ id: acc.id,
411
+ glob: moduleGlob(acc.id),
412
+ fileCount: acc.files.length,
413
+ owners,
414
+ sensitiveTags,
415
+ surfaces: surfacesForModule(acc.id, acc.files, sensitiveTags),
416
+ approvalRequired: moduleApprovalRequired(acc.id, approvalRequiredGlobs),
417
+ language: dominantLanguage(acc.languages),
418
+ };
419
+ })
420
+ .sort((a, b) => a.id.localeCompare(b.id));
421
+ const moduleIds = new Set(modules.map((m) => m.id));
422
+ // 2. Resolve import specifiers into module→module edges.
423
+ const edgeWeights = new Map();
424
+ let analyzedFiles = 0;
425
+ let resolvedImports = 0;
426
+ for (const record of input.imports ?? []) {
427
+ const fromFile = normalizePath(record.filePath);
428
+ if (!knownPaths.has(fromFile))
429
+ continue;
430
+ analyzedFiles += 1;
431
+ const fromModule = moduleIdForPath(fromFile, moduleDepth);
432
+ for (const specifier of record.specifiers) {
433
+ const targetFile = resolveImportSpecifier(fromFile, specifier, knownPaths);
434
+ if (!targetFile)
435
+ continue;
436
+ const toModule = moduleIdForPath(targetFile, moduleDepth);
437
+ if (!moduleIds.has(fromModule) || !moduleIds.has(toModule))
438
+ continue;
439
+ if (fromModule === toModule)
440
+ continue; // ignore intra-module imports
441
+ resolvedImports += 1;
442
+ const key = `${fromModule}${toModule}`;
443
+ edgeWeights.set(key, (edgeWeights.get(key) ?? 0) + 1);
444
+ }
445
+ }
446
+ const edges = Array.from(edgeWeights.entries())
447
+ .map(([key, weight]) => {
448
+ const [from, to] = key.split('');
449
+ return { from, to, weight };
450
+ })
451
+ .sort((a, b) => (a.from === b.from ? a.to.localeCompare(b.to) : a.from.localeCompare(b.from)));
452
+ const languages = uniqueSorted(modules.map((m) => m.language).filter((l) => l !== 'unknown'));
453
+ const stats = {
454
+ moduleCount: modules.length,
455
+ edgeCount: edges.length,
456
+ analyzedFiles,
457
+ resolvedImports,
458
+ languages,
459
+ };
460
+ const canonical = JSON.stringify({
461
+ moduleDepth,
462
+ modules: modules.map((m) => ({
463
+ id: m.id,
464
+ owners: [...m.owners].sort(),
465
+ sensitiveTags: m.sensitiveTags,
466
+ surfaces: m.surfaces,
467
+ approvalRequired: m.approvalRequired,
468
+ })),
469
+ edges: edges.map((e) => `${e.from}->${e.to}:${e.weight}`),
470
+ });
471
+ const architectureHash = (0, node_crypto_1.createHash)('sha256').update(canonical).digest('hex').slice(0, 24);
472
+ return {
473
+ schemaVersion: exports.ARCHITECTURE_GRAPH_SCHEMA_VERSION,
474
+ generatedAt: now,
475
+ moduleDepth,
476
+ modules,
477
+ edges,
478
+ stats,
479
+ architectureHash,
480
+ };
481
+ }
482
+ // ── Graph queries ─────────────────────────────────────────────────────────────
483
+ function findModuleForPath(graph, filePath) {
484
+ const norm = normalizePath(filePath);
485
+ const id = moduleIdForPath(norm, graph.moduleDepth);
486
+ const direct = graph.modules.find((m) => m.id === id);
487
+ if (direct)
488
+ return direct;
489
+ // Fall back to the deepest module whose glob contains the path.
490
+ const containing = graph.modules
491
+ .filter((m) => norm === m.id || norm.startsWith(m.id + '/'))
492
+ .sort((a, b) => b.id.length - a.id.length);
493
+ return containing[0] ?? null;
494
+ }
495
+ /** Modules that import the given module (its downstream consumers). */
496
+ function dependentsOf(graph, moduleId) {
497
+ return uniqueSorted(graph.edges.filter((e) => e.to === moduleId).map((e) => e.from));
498
+ }
499
+ /** Modules the given module imports (its upstream providers / dependencies). */
500
+ function dependenciesOf(graph, moduleId) {
501
+ return uniqueSorted(graph.edges.filter((e) => e.from === moduleId).map((e) => e.to));
502
+ }
503
+ const COMPAT_PLAN_PATTERN = '(backward[- ]?compat|backwards[- ]?compat|breaking change|contract|interface stays|preserve (?:the )?(?:public )?(?:interface|api|contract)|do not break|no breaking)';
504
+ const SECURITY_PLAN_PATTERN = '(security review|security owner|security team|threat model|secure by|authn|authz|access control)';
505
+ const MIGRATION_PLAN_PATTERN = '(rollback|reversible|down migration|backfill safety|restore strategy|migration review|expand[- ]and[- ]contract)';
506
+ function moduleMatchesCandidate(module, candidate) {
507
+ const norm = normalizePath(candidate);
508
+ if (!norm)
509
+ return false;
510
+ const prefix = norm.replace('/**', '').replace('/*', '');
511
+ if (module.id === prefix)
512
+ return true;
513
+ if (prefix.startsWith(module.id + '/'))
514
+ return true; // candidate file under module
515
+ if (module.id.startsWith(prefix + '/'))
516
+ return true; // candidate glob over module
517
+ // Glob form (e.g. "src/api/**") matching a file under the module.
518
+ if (norm.includes('*') && micromatch_1.default.isMatch(module.id, prefix, { dot: true }))
519
+ return true;
520
+ return false;
521
+ }
522
+ /** Modules considered "in play" for a set of candidate paths/globs. */
523
+ function modulesInPlay(graph, candidatePaths) {
524
+ const candidates = candidatePaths.map(normalizePath).filter(Boolean);
525
+ if (candidates.length === 0)
526
+ return [];
527
+ return graph.modules.filter((module) => candidates.some((candidate) => moduleMatchesCandidate(module, candidate)));
528
+ }
529
+ function ownerLabel(owners) {
530
+ if (owners.length === 0)
531
+ return 'the owning team';
532
+ return owners.join(', ');
533
+ }
534
+ /**
535
+ * Derive graph obligation seeds for the modules currently in play. Deterministic
536
+ * and ordered by id.
537
+ */
538
+ function deriveGraphObligationSeeds(args) {
539
+ const { graph } = args;
540
+ const inPlay = modulesInPlay(graph, args.candidatePaths);
541
+ const seeds = [];
542
+ for (const module of inPlay) {
543
+ const owners = ownerLabel(module.owners);
544
+ // Payments: billing/payments-owned code requires explicit approval.
545
+ if (module.surfaces.includes('payments')) {
546
+ seeds.push({
547
+ id: `architecture:payments-approval:${module.id}`,
548
+ category: 'payments',
549
+ title: `Approve billing-owned edit in ${module.id}`,
550
+ description: `This edit touches billing-owned code. Approval required from ${owners}.`,
551
+ severity: 'critical',
552
+ module: module.id,
553
+ requiredPath: module.glob,
554
+ triggeredBy: [`edit targets payments surface ${module.id}`],
555
+ requiredEvidence: [`Obtain an approval covering ${module.id} from ${owners}.`],
556
+ surface: 'payments',
557
+ satisfy: { approval: true },
558
+ });
559
+ }
560
+ // Auth / security / secrets / crypto: security-owner awareness.
561
+ const securitySurface = ['auth', 'security', 'secrets', 'crypto'].find((s) => module.surfaces.includes(s));
562
+ if (securitySurface) {
563
+ seeds.push({
564
+ id: `architecture:security-awareness:${module.id}`,
565
+ category: 'security',
566
+ title: `Security-owner awareness for ${module.id}`,
567
+ description: `This edit touches ${securitySurface}-sensitive code in ${module.id}. Security owner (${owners}) awareness required.`,
568
+ severity: 'critical',
569
+ module: module.id,
570
+ requiredPath: module.glob,
571
+ triggeredBy: [`edit targets ${securitySurface} surface ${module.id}`],
572
+ requiredEvidence: [
573
+ `Approve ${module.id} or record a security-review commitment in the accepted plan.`,
574
+ ],
575
+ surface: securitySurface,
576
+ satisfy: { approval: true, planPattern: SECURITY_PLAN_PATTERN },
577
+ });
578
+ }
579
+ // Migration / database migration: migration review.
580
+ if (module.surfaces.includes('migration')) {
581
+ seeds.push({
582
+ id: `architecture:migration-review:${module.id}`,
583
+ category: 'data-model',
584
+ title: `Migration review for ${module.id}`,
585
+ description: `This edit touches database migration files in ${module.id}. Migration review required.`,
586
+ severity: 'critical',
587
+ module: module.id,
588
+ requiredPath: module.glob,
589
+ triggeredBy: [`edit targets migration surface ${module.id}`],
590
+ requiredEvidence: [
591
+ `Approve ${module.id} or state a rollback / migration-review commitment in the accepted plan.`,
592
+ ],
593
+ surface: 'migration',
594
+ satisfy: { approval: true, planPattern: MIGRATION_PLAN_PATTERN },
595
+ });
596
+ }
597
+ // Public API: contract compatibility.
598
+ if (module.surfaces.includes('public-api')) {
599
+ seeds.push({
600
+ id: `architecture:api-contract:${module.id}`,
601
+ category: 'api-contract',
602
+ title: `Confirm API contract compatibility for ${module.id}`,
603
+ description: `This edit touches public API routes in ${module.id}. Confirm contract compatibility before shipping.`,
604
+ severity: 'warn',
605
+ module: module.id,
606
+ requiredPath: module.glob,
607
+ triggeredBy: [`edit targets public-api surface ${module.id}`],
608
+ requiredEvidence: [
609
+ `State a backward-compatibility commitment in the accepted plan, or cover the route with a test.`,
610
+ ],
611
+ surface: 'public-api',
612
+ satisfy: { approval: false, planPattern: COMPAT_PLAN_PATTERN, moduleTest: true },
613
+ });
614
+ }
615
+ // Downstream impact: editing a module with consumers may break their contract.
616
+ const dependents = dependentsOf(graph, module.id);
617
+ const contractBearing = module.surfaces.includes('public-api') ||
618
+ /(^|\/)(lib|core|shared|common|contracts?|api|utils?|types?|models?)(\/|$)/i.test(module.id);
619
+ if (dependents.length > 0 && (contractBearing || dependents.length >= 3)) {
620
+ const shown = dependents.slice(0, 3).join(', ');
621
+ const more = dependents.length > 3 ? ` (+${dependents.length - 3} more)` : '';
622
+ seeds.push({
623
+ id: `architecture:downstream-impact:${module.id}`,
624
+ category: 'dependency',
625
+ title: `Review downstream impact of ${module.id}`,
626
+ description: `This edit may affect downstream module${dependents.length === 1 ? '' : 's'} ${shown}${more}. Review obligation pending.`,
627
+ severity: 'warn',
628
+ module: module.id,
629
+ requiredPath: module.glob,
630
+ triggeredBy: [`${dependents.length} module(s) depend on ${module.id}`],
631
+ requiredEvidence: [
632
+ `State that ${module.id}'s public contract is preserved in the accepted plan, or add a covering test.`,
633
+ ],
634
+ surface: 'dependency',
635
+ satisfy: { approval: false, planPattern: COMPAT_PLAN_PATTERN, moduleTest: true },
636
+ });
637
+ }
638
+ }
639
+ return seeds.sort((a, b) => a.id.localeCompare(b.id));
640
+ }
641
+ /** True when a graph obligation can be satisfied by editing the module's tests. */
642
+ function isModuleTestSatisfiable(obligationId) {
643
+ return (obligationId.startsWith('architecture:api-contract:') ||
644
+ obligationId.startsWith('architecture:downstream-impact:'));
645
+ }
646
+ //# sourceMappingURL=architecture-graph.js.map