@specferret/core 0.3.0 → 0.4.1

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,27 @@
1
+ import type { DBStore } from '../store/types.js';
2
+ import type { ImportIntegrityReport, ReconcileReport } from '../reconciler/index.js';
3
+ import type { UpwardDriftResult } from '../extractor/upward-classifier.js';
4
+ import type { StatusReport } from '../status/index.js';
5
+ export interface AuditSummary {
6
+ totalContracts: number;
7
+ stable: number;
8
+ needsReview: number;
9
+ roadmap: number;
10
+ downwardBreaking: number;
11
+ downwardNonBreaking: number;
12
+ upwardBreaking: number;
13
+ upwardNonBreaking: number;
14
+ integrityViolationCount: number;
15
+ }
16
+ export interface AuditReport {
17
+ version: '1.0';
18
+ timestamp: string;
19
+ summary: AuditSummary;
20
+ downwardDrift: ReconcileReport['flagged'];
21
+ upwardDrift: UpwardDriftResult[];
22
+ integrityViolations: ImportIntegrityReport;
23
+ importSuggestions: ReconcileReport['importSuggestions'];
24
+ statusReport: StatusReport;
25
+ }
26
+ export declare function buildAuditReport(store: DBStore, projectRoot: string): Promise<AuditReport>;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQvD,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAC1C,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,mBAAmB,EAAE,qBAAqB,CAAC;IAC3C,iBAAiB,EAAE,eAAe,CAAC,mBAAmB,CAAC,CAAC;IACxD,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA+EhG"}
@@ -0,0 +1,89 @@
1
+ // Audit layer — read-only bidirectional drift report.
2
+ // Combines downward drift (reconciler), upward drift (code → spec), and status.
3
+ // No mutations. No side effects beyond reading the store and filesystem.
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import { Reconciler } from '../reconciler/index.js';
7
+ import { classifyUpwardDrift } from '../extractor/upward-classifier.js';
8
+ import { buildStatusReport } from '../status/index.js';
9
+ import { extractContractsFromTypeScript } from '../extractor/typescript.js';
10
+ import { extractFromContractFile } from '../extractor/typescript-contract.js';
11
+ export async function buildAuditReport(store, projectRoot) {
12
+ const reconciler = new Reconciler(store);
13
+ const reconcileReport = await reconciler.reconcile();
14
+ const statusReport = await buildStatusReport(store);
15
+ const contracts = await store.getContracts();
16
+ const breakingTriggerIds = new Set(contracts.filter((c) => c.status === 'needs-review').map((c) => c.id));
17
+ // Upward drift detection — re-extract live TypeScript and compare to declared schema.
18
+ const upwardDrift = [];
19
+ const tsExtractionCache = new Map();
20
+ for (const contract of contracts) {
21
+ if (!contract.code_source_file || !contract.code_source_symbol)
22
+ continue;
23
+ const sourceAbsPath = path.resolve(projectRoot, contract.code_source_file);
24
+ if (!fs.existsSync(sourceAbsPath))
25
+ continue;
26
+ let codeShape;
27
+ try {
28
+ if (sourceAbsPath.endsWith('.contract.ts')) {
29
+ let tsExtraction = tsExtractionCache.get(sourceAbsPath);
30
+ if (!tsExtraction) {
31
+ tsExtraction = await extractFromContractFile(sourceAbsPath);
32
+ tsExtractionCache.set(sourceAbsPath, tsExtraction);
33
+ }
34
+ const found = tsExtraction.contracts.find((c) => c.id === contract.code_source_symbol);
35
+ if (!found)
36
+ continue;
37
+ codeShape = found.shape;
38
+ }
39
+ else {
40
+ const fileContent = fs.readFileSync(sourceAbsPath, 'utf-8');
41
+ const extraction = extractContractsFromTypeScript(sourceAbsPath, fileContent);
42
+ const found = extraction.contracts.find((c) => c.sourceSymbol === contract.code_source_symbol);
43
+ if (!found)
44
+ continue;
45
+ codeShape = found.shape;
46
+ }
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ let declaredSchema = {};
52
+ try {
53
+ declaredSchema = JSON.parse(contract.shape_schema);
54
+ }
55
+ catch { }
56
+ const result = classifyUpwardDrift(contract.id, declaredSchema, codeShape, contract.code_source_file, contract.code_source_symbol);
57
+ if (result.driftClass !== 'NOOP') {
58
+ upwardDrift.push(result);
59
+ }
60
+ }
61
+ const downwardBreaking = reconcileReport.flagged.filter((f) => breakingTriggerIds.has(f.triggeredByContractId)).length;
62
+ const downwardNonBreaking = reconcileReport.flagged.filter((f) => !breakingTriggerIds.has(f.triggeredByContractId)).length;
63
+ const upwardBreaking = upwardDrift.filter((d) => d.driftClass === 'BREAKING').length;
64
+ const upwardNonBreaking = upwardDrift.filter((d) => d.driftClass === 'NON_BREAKING').length;
65
+ const integrityViolationCount = reconcileReport.integrityViolations.unresolvedImports.length +
66
+ reconcileReport.integrityViolations.selfImports.length +
67
+ reconcileReport.integrityViolations.circularImports.length;
68
+ return {
69
+ version: '1.0',
70
+ timestamp: new Date().toISOString(),
71
+ summary: {
72
+ totalContracts: contracts.length,
73
+ stable: statusReport.stable,
74
+ needsReview: statusReport.needsReview,
75
+ roadmap: statusReport.roadmap,
76
+ downwardBreaking,
77
+ downwardNonBreaking,
78
+ upwardBreaking,
79
+ upwardNonBreaking,
80
+ integrityViolationCount,
81
+ },
82
+ downwardDrift: reconcileReport.flagged,
83
+ upwardDrift,
84
+ integrityViolations: reconcileReport.integrityViolations,
85
+ importSuggestions: reconcileReport.importSuggestions,
86
+ statusReport,
87
+ };
88
+ }
89
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -15,4 +15,6 @@ export * from './config.js';
15
15
  export * from './utils/paths.js';
16
16
  export * from './contract.js';
17
17
  export * from './status/index.js';
18
+ export * from './watch/index.js';
19
+ export * from './audit/index.js';
18
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kCAAkC,CAAC;AACjD,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oCAAoC,CAAC;AACnD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kCAAkC,CAAC;AACjD,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oCAAoC,CAAC;AACnD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -17,4 +17,6 @@ export * from './config.js';
17
17
  export * from './utils/paths.js';
18
18
  export * from './contract.js';
19
19
  export * from './status/index.js';
20
+ export * from './watch/index.js';
21
+ export * from './audit/index.js';
20
22
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,4EAA4E;AAE5E,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kCAAkC,CAAC;AACjD,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oCAAoC,CAAC;AACnD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,4EAA4E;AAE5E,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kCAAkC,CAAC;AACjD,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oCAAoC,CAAC;AACnD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,11 @@
1
+ export interface WatcherOptions {
2
+ specDir: string;
3
+ projectRoot: string;
4
+ debounceMs?: number;
5
+ onChange: (changedFiles: string[]) => void | Promise<void>;
6
+ }
7
+ export interface ContractWatcher {
8
+ close: () => void;
9
+ }
10
+ export declare function createContractWatcher(options: WatcherOptions): ContractWatcher;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/watch/index.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe,CAmC9E"}
@@ -0,0 +1,42 @@
1
+ // Watch layer — file system watcher with debounce for contract files.
2
+ // No business logic. No parsing. No traversal.
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ export function createContractWatcher(options) {
6
+ const { specDir, projectRoot, debounceMs = 300, onChange } = options;
7
+ const watchDir = path.resolve(projectRoot, specDir);
8
+ let debounceTimer = null;
9
+ let pendingFiles = new Set();
10
+ let running = false;
11
+ const watcher = fs.watch(watchDir, { recursive: true }, (_eventType, filename) => {
12
+ if (!filename)
13
+ return;
14
+ const normalized = filename.replace(/\\/g, '/');
15
+ if (!normalized.endsWith('.contract.md') && !normalized.endsWith('.contract.ts'))
16
+ return;
17
+ pendingFiles.add(path.join(specDir, normalized).replace(/\\/g, '/'));
18
+ if (debounceTimer)
19
+ clearTimeout(debounceTimer);
20
+ debounceTimer = setTimeout(async () => {
21
+ if (running)
22
+ return;
23
+ running = true;
24
+ const files = [...pendingFiles];
25
+ pendingFiles = new Set();
26
+ try {
27
+ await onChange(files);
28
+ }
29
+ finally {
30
+ running = false;
31
+ }
32
+ }, debounceMs);
33
+ });
34
+ return {
35
+ close: () => {
36
+ if (debounceTimer)
37
+ clearTimeout(debounceTimer);
38
+ watcher.close();
39
+ },
40
+ };
41
+ }
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/watch/index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,+CAA+C;AAE/C,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAalC,MAAM,UAAU,qBAAqB,CAAC,OAAuB;IAC3D,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,GAAG,GAAG,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAEpD,IAAI,aAAa,GAAyC,IAAI,CAAC;IAC/D,IAAI,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,MAAM,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,EAAE;QAC/E,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,OAAO;QAEzF,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;QAErE,IAAI,aAAa;YAAE,YAAY,CAAC,aAAa,CAAC,CAAC;QAC/C,aAAa,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YACpC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,KAAK,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC;YAChC,YAAY,GAAG,IAAI,GAAG,EAAE,CAAC;YACzB,IAAI,CAAC;gBACH,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;oBAAS,CAAC;gBACT,OAAO,GAAG,KAAK,CAAC;YAClB,CAAC;QACH,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,EAAE,GAAG,EAAE;YACV,IAAI,aAAa;gBAAE,YAAY,CAAC,aAAa,CAAC,CAAC;YAC/C,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specferret/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "SpecFerret core engine — spec drift detection.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,120 @@
1
+ // Audit layer — read-only bidirectional drift report.
2
+ // Combines downward drift (reconciler), upward drift (code → spec), and status.
3
+ // No mutations. No side effects beyond reading the store and filesystem.
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import type { DBStore } from '../store/types.js';
8
+ import type { ImportIntegrityReport, ReconcileReport } from '../reconciler/index.js';
9
+ import type { UpwardDriftResult } from '../extractor/upward-classifier.js';
10
+ import type { StatusReport } from '../status/index.js';
11
+ import type { ExtractionResult } from '../extractor/frontmatter.js';
12
+ import { Reconciler } from '../reconciler/index.js';
13
+ import { classifyUpwardDrift } from '../extractor/upward-classifier.js';
14
+ import { buildStatusReport } from '../status/index.js';
15
+ import { extractContractsFromTypeScript } from '../extractor/typescript.js';
16
+ import { extractFromContractFile } from '../extractor/typescript-contract.js';
17
+
18
+ export interface AuditSummary {
19
+ totalContracts: number;
20
+ stable: number;
21
+ needsReview: number;
22
+ roadmap: number;
23
+ downwardBreaking: number;
24
+ downwardNonBreaking: number;
25
+ upwardBreaking: number;
26
+ upwardNonBreaking: number;
27
+ integrityViolationCount: number;
28
+ }
29
+
30
+ export interface AuditReport {
31
+ version: '1.0';
32
+ timestamp: string;
33
+ summary: AuditSummary;
34
+ downwardDrift: ReconcileReport['flagged'];
35
+ upwardDrift: UpwardDriftResult[];
36
+ integrityViolations: ImportIntegrityReport;
37
+ importSuggestions: ReconcileReport['importSuggestions'];
38
+ statusReport: StatusReport;
39
+ }
40
+
41
+ export async function buildAuditReport(store: DBStore, projectRoot: string): Promise<AuditReport> {
42
+ const reconciler = new Reconciler(store);
43
+ const reconcileReport = await reconciler.reconcile();
44
+ const statusReport = await buildStatusReport(store);
45
+
46
+ const contracts = await store.getContracts();
47
+ const breakingTriggerIds = new Set(contracts.filter((c) => c.status === 'needs-review').map((c) => c.id));
48
+
49
+ // Upward drift detection — re-extract live TypeScript and compare to declared schema.
50
+ const upwardDrift: UpwardDriftResult[] = [];
51
+ const tsExtractionCache = new Map<string, ExtractionResult>();
52
+
53
+ for (const contract of contracts) {
54
+ if (!contract.code_source_file || !contract.code_source_symbol) continue;
55
+ const sourceAbsPath = path.resolve(projectRoot, contract.code_source_file);
56
+ if (!fs.existsSync(sourceAbsPath)) continue;
57
+
58
+ let codeShape: unknown;
59
+ try {
60
+ if (sourceAbsPath.endsWith('.contract.ts')) {
61
+ let tsExtraction = tsExtractionCache.get(sourceAbsPath);
62
+ if (!tsExtraction) {
63
+ tsExtraction = await extractFromContractFile(sourceAbsPath);
64
+ tsExtractionCache.set(sourceAbsPath, tsExtraction);
65
+ }
66
+ const found = tsExtraction.contracts.find((c) => c.id === contract.code_source_symbol);
67
+ if (!found) continue;
68
+ codeShape = found.shape;
69
+ } else {
70
+ const fileContent = fs.readFileSync(sourceAbsPath, 'utf-8');
71
+ const extraction = extractContractsFromTypeScript(sourceAbsPath, fileContent);
72
+ const found = extraction.contracts.find((c) => c.sourceSymbol === contract.code_source_symbol);
73
+ if (!found) continue;
74
+ codeShape = found.shape;
75
+ }
76
+ } catch {
77
+ continue;
78
+ }
79
+
80
+ let declaredSchema: unknown = {};
81
+ try {
82
+ declaredSchema = JSON.parse(contract.shape_schema);
83
+ } catch {}
84
+
85
+ const result = classifyUpwardDrift(contract.id, declaredSchema, codeShape, contract.code_source_file, contract.code_source_symbol);
86
+ if (result.driftClass !== 'NOOP') {
87
+ upwardDrift.push(result);
88
+ }
89
+ }
90
+
91
+ const downwardBreaking = reconcileReport.flagged.filter((f) => breakingTriggerIds.has(f.triggeredByContractId)).length;
92
+ const downwardNonBreaking = reconcileReport.flagged.filter((f) => !breakingTriggerIds.has(f.triggeredByContractId)).length;
93
+ const upwardBreaking = upwardDrift.filter((d) => d.driftClass === 'BREAKING').length;
94
+ const upwardNonBreaking = upwardDrift.filter((d) => d.driftClass === 'NON_BREAKING').length;
95
+ const integrityViolationCount =
96
+ reconcileReport.integrityViolations.unresolvedImports.length +
97
+ reconcileReport.integrityViolations.selfImports.length +
98
+ reconcileReport.integrityViolations.circularImports.length;
99
+
100
+ return {
101
+ version: '1.0',
102
+ timestamp: new Date().toISOString(),
103
+ summary: {
104
+ totalContracts: contracts.length,
105
+ stable: statusReport.stable,
106
+ needsReview: statusReport.needsReview,
107
+ roadmap: statusReport.roadmap,
108
+ downwardBreaking,
109
+ downwardNonBreaking,
110
+ upwardBreaking,
111
+ upwardNonBreaking,
112
+ integrityViolationCount,
113
+ },
114
+ downwardDrift: reconcileReport.flagged,
115
+ upwardDrift,
116
+ integrityViolations: reconcileReport.integrityViolations,
117
+ importSuggestions: reconcileReport.importSuggestions,
118
+ statusReport,
119
+ };
120
+ }
package/src/index.ts CHANGED
@@ -18,3 +18,5 @@ export * from './config.js';
18
18
  export * from './utils/paths.js';
19
19
  export * from './contract.js';
20
20
  export * from './status/index.js';
21
+ export * from './watch/index.js';
22
+ export * from './audit/index.js';
@@ -0,0 +1,53 @@
1
+ // Watch layer — file system watcher with debounce for contract files.
2
+ // No business logic. No parsing. No traversal.
3
+
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+
7
+ export interface WatcherOptions {
8
+ specDir: string;
9
+ projectRoot: string;
10
+ debounceMs?: number;
11
+ onChange: (changedFiles: string[]) => void | Promise<void>;
12
+ }
13
+
14
+ export interface ContractWatcher {
15
+ close: () => void;
16
+ }
17
+
18
+ export function createContractWatcher(options: WatcherOptions): ContractWatcher {
19
+ const { specDir, projectRoot, debounceMs = 300, onChange } = options;
20
+ const watchDir = path.resolve(projectRoot, specDir);
21
+
22
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
23
+ let pendingFiles = new Set<string>();
24
+ let running = false;
25
+
26
+ const watcher = fs.watch(watchDir, { recursive: true }, (_eventType, filename) => {
27
+ if (!filename) return;
28
+ const normalized = filename.replace(/\\/g, '/');
29
+ if (!normalized.endsWith('.contract.md') && !normalized.endsWith('.contract.ts')) return;
30
+
31
+ pendingFiles.add(path.join(specDir, normalized).replace(/\\/g, '/'));
32
+
33
+ if (debounceTimer) clearTimeout(debounceTimer);
34
+ debounceTimer = setTimeout(async () => {
35
+ if (running) return;
36
+ running = true;
37
+ const files = [...pendingFiles];
38
+ pendingFiles = new Set();
39
+ try {
40
+ await onChange(files);
41
+ } finally {
42
+ running = false;
43
+ }
44
+ }, debounceMs);
45
+ });
46
+
47
+ return {
48
+ close: () => {
49
+ if (debounceTimer) clearTimeout(debounceTimer);
50
+ watcher.close();
51
+ },
52
+ };
53
+ }