@specferret/core 0.1.4 → 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/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/contract.d.ts +16 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +17 -0
- package/dist/contract.js.map +1 -0
- package/dist/extractor/__fixtures__/cross-file-id.fixture.d.ts +15 -0
- package/dist/extractor/__fixtures__/cross-file-id.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/cross-file-id.fixture.js +9 -0
- package/dist/extractor/__fixtures__/cross-file-id.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/cross-file-unresolved.fixture.d.ts +14 -0
- package/dist/extractor/__fixtures__/cross-file-unresolved.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/cross-file-unresolved.fixture.js +9 -0
- package/dist/extractor/__fixtures__/cross-file-unresolved.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/empty.fixture.d.ts +6 -0
- package/dist/extractor/__fixtures__/empty.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/empty.fixture.js +5 -0
- package/dist/extractor/__fixtures__/empty.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/external-no-id.fixture.d.ts +8 -0
- package/dist/extractor/__fixtures__/external-no-id.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/external-no-id.fixture.js +7 -0
- package/dist/extractor/__fixtures__/external-no-id.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/external-with-id.fixture.d.ts +9 -0
- package/dist/extractor/__fixtures__/external-with-id.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/external-with-id.fixture.js +8 -0
- package/dist/extractor/__fixtures__/external-with-id.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/mixed.fixture.d.ts +24 -0
- package/dist/extractor/__fixtures__/mixed.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/mixed.fixture.js +15 -0
- package/dist/extractor/__fixtures__/mixed.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/one-contract.fixture.d.ts +10 -0
- package/dist/extractor/__fixtures__/one-contract.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/one-contract.fixture.js +10 -0
- package/dist/extractor/__fixtures__/one-contract.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/optional-fields.fixture.d.ts +10 -0
- package/dist/extractor/__fixtures__/optional-fields.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/optional-fields.fixture.js +10 -0
- package/dist/extractor/__fixtures__/optional-fields.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/same-file-consumes.fixture.d.ts +20 -0
- package/dist/extractor/__fixtures__/same-file-consumes.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/same-file-consumes.fixture.js +11 -0
- package/dist/extractor/__fixtures__/same-file-consumes.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/three-unresolved.fixture.d.ts +38 -0
- package/dist/extractor/__fixtures__/three-unresolved.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/three-unresolved.fixture.js +21 -0
- package/dist/extractor/__fixtures__/three-unresolved.fixture.js.map +1 -0
- package/dist/extractor/__fixtures__/throws-on-import.fixture.d.ts +2 -0
- package/dist/extractor/__fixtures__/throws-on-import.fixture.d.ts.map +1 -0
- package/dist/extractor/__fixtures__/throws-on-import.fixture.js +5 -0
- package/dist/extractor/__fixtures__/throws-on-import.fixture.js.map +1 -0
- package/dist/extractor/frontmatter.d.ts +5 -1
- package/dist/extractor/frontmatter.d.ts.map +1 -1
- package/dist/extractor/frontmatter.js +5 -0
- package/dist/extractor/frontmatter.js.map +1 -1
- package/dist/extractor/typescript-contract.d.ts +3 -0
- package/dist/extractor/typescript-contract.d.ts.map +1 -0
- package/dist/extractor/typescript-contract.js +67 -0
- package/dist/extractor/typescript-contract.js.map +1 -0
- package/dist/extractor/upward-classifier.d.ts +25 -0
- package/dist/extractor/upward-classifier.d.ts.map +1 -0
- package/dist/extractor/upward-classifier.js +48 -0
- package/dist/extractor/upward-classifier.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/status/index.d.ts +20 -0
- package/dist/status/index.d.ts.map +1 -0
- package/dist/status/index.js +46 -0
- package/dist/status/index.js.map +1 -0
- package/dist/store/sqlite.d.ts +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +37 -32
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/types.d.ts +6 -2
- package/dist/store/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/config.ts +3 -0
- package/src/contract.test.ts +99 -0
- package/src/contract.ts +38 -0
- package/src/extractor/__fixtures__/cross-file-id.fixture.ts +9 -0
- package/src/extractor/__fixtures__/cross-file-unresolved.fixture.ts +9 -0
- package/src/extractor/__fixtures__/empty.fixture.ts +4 -0
- package/src/extractor/__fixtures__/external-no-id.fixture.ts +7 -0
- package/src/extractor/__fixtures__/external-with-id.fixture.ts +8 -0
- package/src/extractor/__fixtures__/mixed.fixture.ts +17 -0
- package/src/extractor/__fixtures__/one-contract.fixture.ts +10 -0
- package/src/extractor/__fixtures__/optional-fields.fixture.ts +10 -0
- package/src/extractor/__fixtures__/same-file-consumes.fixture.ts +12 -0
- package/src/extractor/__fixtures__/three-unresolved.fixture.ts +24 -0
- package/src/extractor/__fixtures__/throws-on-import.fixture.ts +3 -0
- package/src/extractor/frontmatter.test.ts +74 -0
- package/src/extractor/frontmatter.ts +11 -1
- package/src/extractor/typescript-contract.test.ts +143 -0
- package/src/extractor/typescript-contract.ts +76 -0
- package/src/extractor/upward-classifier.test.ts +188 -0
- package/src/extractor/upward-classifier.ts +68 -0
- package/src/index.ts +4 -0
- package/src/status/index.ts +68 -0
- package/src/store/sqlite.test.ts +135 -79
- package/src/store/sqlite.ts +45 -92
- package/src/store/types.ts +7 -6
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type UpwardDriftClass = 'BREAKING' | 'NON_BREAKING' | 'NOOP';
|
|
2
|
+
export interface UpwardDriftResult {
|
|
3
|
+
contractId: string;
|
|
4
|
+
driftClass: UpwardDriftClass;
|
|
5
|
+
sourceFile: string;
|
|
6
|
+
sourceSymbol: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Classifies the drift between a declared contract schema and the live code-derived schema.
|
|
11
|
+
*
|
|
12
|
+
* Input:
|
|
13
|
+
* - declaredSchema: the schema declared in the .contract.md frontmatter (the spec)
|
|
14
|
+
* - codeSchema: the schema extracted from the TypeScript source at lint time
|
|
15
|
+
*
|
|
16
|
+
* Output:
|
|
17
|
+
* - BREAKING: code change breaks the declared contract (required field removed, type changed, etc.)
|
|
18
|
+
* - NON_BREAKING: code change is additive but not declared (optional field added, enum value added)
|
|
19
|
+
* - NOOP: code and declared schema are semantically identical (hash-stable, no action needed)
|
|
20
|
+
*
|
|
21
|
+
* Uses the same classification taxonomy as compareSchemas for consistency.
|
|
22
|
+
* No-op formatting changes (property reorder with stable hash) return NOOP.
|
|
23
|
+
*/
|
|
24
|
+
export declare function classifyUpwardDrift(contractId: string, declaredSchema: unknown, codeSchema: unknown, sourceFile: string, sourceSymbol: string): UpwardDriftResult;
|
|
25
|
+
//# sourceMappingURL=upward-classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upward-classifier.d.ts","sourceRoot":"","sources":["../../src/extractor/upward-classifier.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,cAAc,GAAG,MAAM,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,OAAO,EACvB,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,GACnB,iBAAiB,CA8BnB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Pure function. No I/O. No side effects. Ever.
|
|
2
|
+
// Upward drift classifier — detects when a TypeScript implementation diverges from
|
|
3
|
+
// its declared contract schema (code → spec direction).
|
|
4
|
+
import { compareSchemas } from './validator.js';
|
|
5
|
+
/**
|
|
6
|
+
* Classifies the drift between a declared contract schema and the live code-derived schema.
|
|
7
|
+
*
|
|
8
|
+
* Input:
|
|
9
|
+
* - declaredSchema: the schema declared in the .contract.md frontmatter (the spec)
|
|
10
|
+
* - codeSchema: the schema extracted from the TypeScript source at lint time
|
|
11
|
+
*
|
|
12
|
+
* Output:
|
|
13
|
+
* - BREAKING: code change breaks the declared contract (required field removed, type changed, etc.)
|
|
14
|
+
* - NON_BREAKING: code change is additive but not declared (optional field added, enum value added)
|
|
15
|
+
* - NOOP: code and declared schema are semantically identical (hash-stable, no action needed)
|
|
16
|
+
*
|
|
17
|
+
* Uses the same classification taxonomy as compareSchemas for consistency.
|
|
18
|
+
* No-op formatting changes (property reorder with stable hash) return NOOP.
|
|
19
|
+
*/
|
|
20
|
+
export function classifyUpwardDrift(contractId, declaredSchema, codeSchema, sourceFile, sourceSymbol) {
|
|
21
|
+
const comparison = compareSchemas(declaredSchema, codeSchema);
|
|
22
|
+
if (comparison.classification === 'no-change') {
|
|
23
|
+
return {
|
|
24
|
+
contractId,
|
|
25
|
+
driftClass: 'NOOP',
|
|
26
|
+
sourceFile,
|
|
27
|
+
sourceSymbol,
|
|
28
|
+
reason: comparison.reason,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (comparison.classification === 'breaking') {
|
|
32
|
+
return {
|
|
33
|
+
contractId,
|
|
34
|
+
driftClass: 'BREAKING',
|
|
35
|
+
sourceFile,
|
|
36
|
+
sourceSymbol,
|
|
37
|
+
reason: comparison.reason,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
contractId,
|
|
42
|
+
driftClass: 'NON_BREAKING',
|
|
43
|
+
sourceFile,
|
|
44
|
+
sourceSymbol,
|
|
45
|
+
reason: comparison.reason,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=upward-classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upward-classifier.js","sourceRoot":"","sources":["../../src/extractor/upward-classifier.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,mFAAmF;AACnF,wDAAwD;AAExD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAYhD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAAkB,EAClB,cAAuB,EACvB,UAAmB,EACnB,UAAkB,EAClB,YAAoB;IAEpB,MAAM,UAAU,GAAG,cAAc,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;IAE9D,IAAI,UAAU,CAAC,cAAc,KAAK,WAAW,EAAE,CAAC;QAC9C,OAAO;YACL,UAAU;YACV,UAAU,EAAE,MAAM;YAClB,UAAU;YACV,YAAY;YACZ,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,cAAc,KAAK,UAAU,EAAE,CAAC;QAC7C,OAAO;YACL,UAAU;YACV,UAAU,EAAE,UAAU;YACtB,UAAU;YACV,YAAY;YACZ,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC;IACJ,CAAC;IAED,OAAO;QACL,UAAU;QACV,UAAU,EAAE,cAAc;QAC1B,UAAU;QACV,YAAY;QACZ,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from './extractor/frontmatter.js';
|
|
2
|
+
export * from './extractor/typescript-contract.js';
|
|
2
3
|
export * from './extractor/typescript.js';
|
|
3
4
|
export * from './extractor/validator.js';
|
|
4
5
|
export * from './extractor/contract-types.js';
|
|
5
6
|
export * from './extractor/hash.js';
|
|
7
|
+
export * from './extractor/upward-classifier.js';
|
|
6
8
|
export * from './context/index.js';
|
|
7
9
|
export * from './store/types.js';
|
|
8
10
|
export * from './store/sqlite.js';
|
|
@@ -11,4 +13,6 @@ export * from './reconciler/index.js';
|
|
|
11
13
|
export * from './reconciler/import-suggestions.js';
|
|
12
14
|
export * from './config.js';
|
|
13
15
|
export * from './utils/paths.js';
|
|
16
|
+
export * from './contract.js';
|
|
17
|
+
export * from './status/index.js';
|
|
14
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,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"}
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// @specferret/core public API
|
|
2
2
|
// Do not export llm-fallback — dynamic import only, never a default export.
|
|
3
3
|
export * from './extractor/frontmatter.js';
|
|
4
|
+
export * from './extractor/typescript-contract.js';
|
|
4
5
|
export * from './extractor/typescript.js';
|
|
5
6
|
export * from './extractor/validator.js';
|
|
6
7
|
export * from './extractor/contract-types.js';
|
|
7
8
|
export * from './extractor/hash.js';
|
|
9
|
+
export * from './extractor/upward-classifier.js';
|
|
8
10
|
export * from './context/index.js';
|
|
9
11
|
export * from './store/types.js';
|
|
10
12
|
export * from './store/sqlite.js';
|
|
@@ -13,4 +15,6 @@ export * from './reconciler/index.js';
|
|
|
13
15
|
export * from './reconciler/import-suggestions.js';
|
|
14
16
|
export * from './config.js';
|
|
15
17
|
export * from './utils/paths.js';
|
|
18
|
+
export * from './contract.js';
|
|
19
|
+
export * from './status/index.js';
|
|
16
20
|
//# 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,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,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"}
|
|
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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DBStore, ContractStatus } from '../store/types.js';
|
|
2
|
+
export type StatusContractEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
status: ContractStatus;
|
|
5
|
+
driftClass: 'breaking' | 'stable' | 'roadmap';
|
|
6
|
+
dependentCount: number;
|
|
7
|
+
dependents: string[];
|
|
8
|
+
};
|
|
9
|
+
export type StatusReport = {
|
|
10
|
+
version: '2.0';
|
|
11
|
+
timestamp: string;
|
|
12
|
+
total: number;
|
|
13
|
+
stable: number;
|
|
14
|
+
roadmap: number;
|
|
15
|
+
needsReview: number;
|
|
16
|
+
contracts: StatusContractEntry[];
|
|
17
|
+
};
|
|
18
|
+
export declare function buildStatusReport(store: DBStore): Promise<StatusReport>;
|
|
19
|
+
export declare function buildStatusMarkdown(report: StatusReport): string;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/status/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEjE,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,mBAAmB,EAAE,CAAC;CAClC,CAAC;AAEF,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAkC7E;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAWhE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export async function buildStatusReport(store) {
|
|
2
|
+
const [contracts, nodes, deps] = await Promise.all([store.getContracts(), store.getNodes(), store.getDependencies()]);
|
|
3
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
4
|
+
const dependentsByContractId = new Map();
|
|
5
|
+
for (const dep of deps) {
|
|
6
|
+
const node = nodeById.get(dep.source_node_id);
|
|
7
|
+
if (!node)
|
|
8
|
+
continue;
|
|
9
|
+
const list = dependentsByContractId.get(dep.target_contract_id) ?? [];
|
|
10
|
+
if (!list.includes(node.file_path))
|
|
11
|
+
list.push(node.file_path);
|
|
12
|
+
dependentsByContractId.set(dep.target_contract_id, list);
|
|
13
|
+
}
|
|
14
|
+
const entries = contracts.map((c) => {
|
|
15
|
+
const dependents = dependentsByContractId.get(c.id) ?? [];
|
|
16
|
+
return {
|
|
17
|
+
id: c.id,
|
|
18
|
+
status: c.status,
|
|
19
|
+
driftClass: c.status === 'needs-review' ? 'breaking' : c.status === 'roadmap' ? 'roadmap' : 'stable',
|
|
20
|
+
dependentCount: dependents.length,
|
|
21
|
+
dependents,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
version: '2.0',
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
total: contracts.length,
|
|
28
|
+
stable: contracts.filter((c) => c.status === 'stable').length,
|
|
29
|
+
roadmap: contracts.filter((c) => c.status === 'roadmap').length,
|
|
30
|
+
needsReview: contracts.filter((c) => c.status === 'needs-review').length,
|
|
31
|
+
contracts: entries,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function buildStatusMarkdown(report) {
|
|
35
|
+
const lines = [
|
|
36
|
+
'# Contract Status',
|
|
37
|
+
'',
|
|
38
|
+
`Generated: ${report.timestamp}`,
|
|
39
|
+
'',
|
|
40
|
+
'| Contract | Status | Drift Class | Dependents |',
|
|
41
|
+
'| --- | --- | --- | --- |',
|
|
42
|
+
...report.contracts.map((c) => `| ${c.id} | ${c.status} | ${c.driftClass} | ${c.dependentCount} |`),
|
|
43
|
+
];
|
|
44
|
+
return lines.join('\n') + '\n';
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/status/index.ts"],"names":[],"mappings":"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"}
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DBStore, FerretNode, FerretContract, FerretDependency, FerretReconciliationLog, FerretPlacementDecision, NodeStatus } from
|
|
1
|
+
import type { DBStore, FerretNode, FerretContract, FerretDependency, FerretReconciliationLog, FerretPlacementDecision, NodeStatus } from './types.js';
|
|
2
2
|
export declare class SqliteStore implements DBStore {
|
|
3
3
|
private db;
|
|
4
4
|
private dbPath;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/store/sqlite.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/store/sqlite.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtJ,qBAAa,WAAY,YAAW,OAAO;IACzC,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAAS;gBAEX,UAAU,CAAC,EAAE,MAAM;IAMzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2ErB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAK/D,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3C,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAItC,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BvD,gBAAgB,CAAC,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAU7D,gCAAgC,CAAC,YAAY,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBlG,QAAQ,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAIjC,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAI3D,YAAY,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAIzC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAKvD,eAAe,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAI9C,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAInE,uBAAuB,CAAC,GAAG,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IASpE,qBAAqB,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAM3D,uBAAuB,CAAC,QAAQ,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IASzE,qBAAqB,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC;CAGlE"}
|
package/dist/store/sqlite.js
CHANGED
|
@@ -1,38 +1,33 @@
|
|
|
1
|
-
import { Database } from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as fs from
|
|
4
|
-
import { randomUUID } from
|
|
5
|
-
import { findProjectRoot } from
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { findProjectRoot } from '../utils/paths.js';
|
|
6
6
|
export class SqliteStore {
|
|
7
7
|
db = null;
|
|
8
8
|
dbPath;
|
|
9
9
|
constructor(customPath) {
|
|
10
10
|
const root = findProjectRoot();
|
|
11
|
-
const defaultPath = path.join(root,
|
|
12
|
-
this.dbPath =
|
|
13
|
-
customPath === ":memory:"
|
|
14
|
-
? ":memory:"
|
|
15
|
-
: customPath
|
|
16
|
-
? path.resolve(root, customPath)
|
|
17
|
-
: defaultPath;
|
|
11
|
+
const defaultPath = path.join(root, '.ferret', 'graph.db');
|
|
12
|
+
this.dbPath = customPath === ':memory:' ? ':memory:' : customPath ? path.resolve(root, customPath) : defaultPath;
|
|
18
13
|
}
|
|
19
14
|
async init() {
|
|
20
15
|
if (this.db)
|
|
21
16
|
return;
|
|
22
|
-
if (this.dbPath !==
|
|
17
|
+
if (this.dbPath !== ':memory:') {
|
|
23
18
|
const ferretDir = path.dirname(this.dbPath);
|
|
24
19
|
if (!fs.existsSync(ferretDir)) {
|
|
25
20
|
fs.mkdirSync(ferretDir, { recursive: true });
|
|
26
21
|
}
|
|
27
|
-
const gitignorePath = path.join(ferretDir,
|
|
22
|
+
const gitignorePath = path.join(ferretDir, '.gitignore');
|
|
28
23
|
try {
|
|
29
|
-
fs.writeFileSync(gitignorePath,
|
|
24
|
+
fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!context.json\n', 'utf-8');
|
|
30
25
|
}
|
|
31
26
|
catch { }
|
|
32
27
|
}
|
|
33
28
|
this.db = new Database(this.dbPath);
|
|
34
|
-
this.db.exec(
|
|
35
|
-
this.db.exec(
|
|
29
|
+
this.db.exec('PRAGMA journal_mode = WAL;');
|
|
30
|
+
this.db.exec('PRAGMA foreign_keys = ON;');
|
|
36
31
|
this.db.exec(`
|
|
37
32
|
CREATE TABLE IF NOT EXISTS ferret_nodes (
|
|
38
33
|
id TEXT PRIMARY KEY,
|
|
@@ -80,6 +75,14 @@ export class SqliteStore {
|
|
|
80
75
|
this.db.exec(`ALTER TABLE ferret_contracts ADD COLUMN shape_schema TEXT NOT NULL DEFAULT '{}';`);
|
|
81
76
|
}
|
|
82
77
|
catch { }
|
|
78
|
+
try {
|
|
79
|
+
this.db.exec(`ALTER TABLE ferret_contracts ADD COLUMN code_source_file TEXT;`);
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
try {
|
|
83
|
+
this.db.exec(`ALTER TABLE ferret_contracts ADD COLUMN code_source_symbol TEXT;`);
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
83
86
|
}
|
|
84
87
|
async close() {
|
|
85
88
|
if (this.db) {
|
|
@@ -88,7 +91,7 @@ export class SqliteStore {
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
async getNodeByFilePath(filePath) {
|
|
91
|
-
const row = this.db.prepare(
|
|
94
|
+
const row = this.db.prepare('SELECT * FROM ferret_nodes WHERE file_path = ?').get(filePath);
|
|
92
95
|
return row ?? null;
|
|
93
96
|
}
|
|
94
97
|
async upsertNode(node) {
|
|
@@ -103,19 +106,21 @@ export class SqliteStore {
|
|
|
103
106
|
`).run(node.id, node.file_path, node.hash, node.status);
|
|
104
107
|
}
|
|
105
108
|
async getAllContractIds() {
|
|
106
|
-
return this.db.prepare(
|
|
109
|
+
return this.db.prepare('SELECT id FROM ferret_contracts').all().map((r) => r.id);
|
|
107
110
|
}
|
|
108
111
|
async upsertContract(contract) {
|
|
109
112
|
this.db.prepare(`
|
|
110
|
-
INSERT INTO ferret_contracts (id, node_id, shape_hash, shape_schema, type, status)
|
|
111
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
113
|
+
INSERT INTO ferret_contracts (id, node_id, shape_hash, shape_schema, type, status, code_source_file, code_source_symbol)
|
|
114
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
112
115
|
ON CONFLICT(id) DO UPDATE SET
|
|
113
116
|
node_id = excluded.node_id,
|
|
114
117
|
shape_hash = excluded.shape_hash,
|
|
115
118
|
shape_schema = excluded.shape_schema,
|
|
116
119
|
type = excluded.type,
|
|
117
|
-
status = excluded.status
|
|
118
|
-
|
|
120
|
+
status = excluded.status,
|
|
121
|
+
code_source_file = excluded.code_source_file,
|
|
122
|
+
code_source_symbol = excluded.code_source_symbol
|
|
123
|
+
`).run(contract.id, contract.node_id, contract.shape_hash, contract.shape_schema, contract.type, contract.status, contract.code_source_file ?? null, contract.code_source_symbol ?? null);
|
|
119
124
|
}
|
|
120
125
|
async upsertDependency(dependency) {
|
|
121
126
|
this.db.prepare(`
|
|
@@ -126,7 +131,7 @@ export class SqliteStore {
|
|
|
126
131
|
}
|
|
127
132
|
async replaceDependenciesForSourceNode(sourceNodeId, targetContractIds) {
|
|
128
133
|
const uniqueTargets = [...new Set(targetContractIds)].sort();
|
|
129
|
-
const deleteStatement = this.db.prepare(
|
|
134
|
+
const deleteStatement = this.db.prepare('DELETE FROM ferret_dependencies WHERE source_node_id = ?');
|
|
130
135
|
const insertStatement = this.db.prepare(`
|
|
131
136
|
INSERT INTO ferret_dependencies (id, source_node_id, target_contract_id)
|
|
132
137
|
VALUES (?, ?, ?)
|
|
@@ -140,23 +145,23 @@ export class SqliteStore {
|
|
|
140
145
|
transaction(sourceNodeId);
|
|
141
146
|
}
|
|
142
147
|
async getNodes() {
|
|
143
|
-
return this.db.prepare(
|
|
148
|
+
return this.db.prepare('SELECT * FROM ferret_nodes').all();
|
|
144
149
|
}
|
|
145
150
|
async getNodesByStatus(status) {
|
|
146
|
-
return this.db.prepare(
|
|
151
|
+
return this.db.prepare('SELECT * FROM ferret_nodes WHERE status = ?').all(status);
|
|
147
152
|
}
|
|
148
153
|
async getContracts() {
|
|
149
|
-
return this.db.prepare(
|
|
154
|
+
return this.db.prepare('SELECT * FROM ferret_contracts').all();
|
|
150
155
|
}
|
|
151
156
|
async getContract(id) {
|
|
152
|
-
const row = this.db.prepare(
|
|
157
|
+
const row = this.db.prepare('SELECT * FROM ferret_contracts WHERE id = ?').get(id);
|
|
153
158
|
return row ?? null;
|
|
154
159
|
}
|
|
155
160
|
async getDependencies() {
|
|
156
|
-
return this.db.prepare(
|
|
161
|
+
return this.db.prepare('SELECT * FROM ferret_dependencies').all();
|
|
157
162
|
}
|
|
158
163
|
async updateNodeStatus(nodeId, status) {
|
|
159
|
-
this.db.prepare(
|
|
164
|
+
this.db.prepare('UPDATE ferret_nodes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, nodeId);
|
|
160
165
|
}
|
|
161
166
|
async insertReconciliationLog(log) {
|
|
162
167
|
this.db.prepare(`
|
|
@@ -165,7 +170,7 @@ export class SqliteStore {
|
|
|
165
170
|
`).run(log.id, log.node_id, log.triggered_by, log.resolved_by ?? null, log.resolution_notes ?? null);
|
|
166
171
|
}
|
|
167
172
|
async getReconciliationLogs() {
|
|
168
|
-
return this.db.prepare(
|
|
173
|
+
return this.db.prepare('SELECT id, node_id, triggered_by, resolved_by, resolution_notes FROM ferret_reconciliation_log').all();
|
|
169
174
|
}
|
|
170
175
|
async insertPlacementDecision(decision) {
|
|
171
176
|
this.db.prepare(`
|
|
@@ -174,7 +179,7 @@ export class SqliteStore {
|
|
|
174
179
|
`).run(decision.id, decision.node_id, decision.placed_by, decision.reasoning ?? null);
|
|
175
180
|
}
|
|
176
181
|
async getPlacementDecisions() {
|
|
177
|
-
return this.db.prepare(
|
|
182
|
+
return this.db.prepare('SELECT id, node_id, placed_by, reasoning FROM ferret_placement_decisions').all();
|
|
178
183
|
}
|
|
179
184
|
}
|
|
180
185
|
//# sourceMappingURL=sqlite.js.map
|
package/dist/store/sqlite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/store/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/store/sqlite.ts"],"names":[],"mappings":"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"}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type NodeStatus =
|
|
2
|
-
export type ContractStatus =
|
|
1
|
+
export type NodeStatus = 'stable' | 'needs-review' | 'roadmap' | 'blocked';
|
|
2
|
+
export type ContractStatus = 'stable' | 'roadmap' | 'needs-review';
|
|
3
3
|
export interface FerretNode {
|
|
4
4
|
id: string;
|
|
5
5
|
file_path: string;
|
|
@@ -14,6 +14,10 @@ export interface FerretContract {
|
|
|
14
14
|
shape_schema: string;
|
|
15
15
|
type: string;
|
|
16
16
|
status: ContractStatus;
|
|
17
|
+
/** Path to the TypeScript file this contract was generated from (code-first contracts only). */
|
|
18
|
+
code_source_file?: string;
|
|
19
|
+
/** TypeScript symbol name this contract was generated from (code-first contracts only). */
|
|
20
|
+
code_source_symbol?: string;
|
|
17
21
|
}
|
|
18
22
|
export interface FerretDependency {
|
|
19
23
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,cAAc,GAAG,SAAS,GAAG,SAAS,CAAC;AAC3E,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,cAAc,CAAC;AAEnE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specferret/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "SpecFerret core engine — spec drift detection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"gray-matter": "^4.0.3",
|
|
30
30
|
"tree-sitter": "^0.22.4",
|
|
31
31
|
"tree-sitter-typescript": "^0.23.2",
|
|
32
|
-
"zod": "^3.22.4"
|
|
32
|
+
"zod": "^3.22.4",
|
|
33
|
+
"zod-to-json-schema": "^3.25.2"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@types/bun": "^1.3.11",
|
package/src/config.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { defineContract, isContract } from './contract.js';
|
|
4
|
+
|
|
5
|
+
describe('isContract', () => {
|
|
6
|
+
it('returns true for a minimal valid object', () => {
|
|
7
|
+
expect(isContract({ value: 'x', output: {} })).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns true for object with id set', () => {
|
|
11
|
+
expect(isContract({ id: 'auth.jwt', value: 'x', output: {} })).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns true for object without id — id is optional and not checked', () => {
|
|
15
|
+
expect(isContract({ value: 'x', output: { name: z.string() } })).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns false for null', () => {
|
|
19
|
+
expect(isContract(null)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns false for undefined', () => {
|
|
23
|
+
expect(isContract(undefined)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns false for a number', () => {
|
|
27
|
+
expect(isContract(42)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns false for a string', () => {
|
|
31
|
+
expect(isContract('string')).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns false for object missing value', () => {
|
|
35
|
+
expect(isContract({ output: {} })).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false for object missing output', () => {
|
|
39
|
+
expect(isContract({ value: 'x' })).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns false when value is not a string', () => {
|
|
43
|
+
expect(isContract({ value: 42, output: {} })).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns false when output is null', () => {
|
|
47
|
+
expect(isContract({ value: 'x', output: null })).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns false when output is an array', () => {
|
|
51
|
+
expect(isContract({ value: 'x', output: [] })).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('defineContract', () => {
|
|
56
|
+
it('returns the exact object passed to it when valid', () => {
|
|
57
|
+
const contract = { value: 'x', output: {} };
|
|
58
|
+
expect(defineContract(contract)).toBe(contract);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not throw when id is omitted', () => {
|
|
62
|
+
expect(() => defineContract({ value: 'x', output: {} })).not.toThrow();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not throw when id is explicitly undefined', () => {
|
|
66
|
+
expect(() => defineContract({ id: undefined, value: 'x', output: {} })).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not throw when id is a valid non-empty string', () => {
|
|
70
|
+
expect(() => defineContract({ id: 'auth.jwt', value: 'x', output: {} })).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('throws when id is an empty string', () => {
|
|
74
|
+
expect(() => defineContract({ id: '', value: 'x', output: {} })).toThrow(
|
|
75
|
+
'id must be a non-empty string',
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('throws when id is whitespace only', () => {
|
|
80
|
+
expect(() => defineContract({ id: ' ', value: 'x', output: {} })).toThrow(
|
|
81
|
+
'id must be a non-empty string',
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('enforces typed invariant argument — compile-time check via typed fixture', () => {
|
|
86
|
+
// If r is not typed as { name: string }, the _: string assignment below is a compile error.
|
|
87
|
+
const contract = defineContract({
|
|
88
|
+
value: 'typed invariant test',
|
|
89
|
+
output: { name: z.string() },
|
|
90
|
+
invariants: [
|
|
91
|
+
(r) => {
|
|
92
|
+
const _: string = r.name;
|
|
93
|
+
return _.length > 0;
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
expect(isContract(contract)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export interface Contract<
|
|
4
|
+
T extends Record<string, z.ZodTypeAny> = Record<string, z.ZodTypeAny>,
|
|
5
|
+
> {
|
|
6
|
+
id?: string;
|
|
7
|
+
value: string;
|
|
8
|
+
output: T;
|
|
9
|
+
invariants?: Array<(r: z.infer<z.ZodObject<T>>) => boolean>;
|
|
10
|
+
consumes?: Contract[];
|
|
11
|
+
forbids?: string[];
|
|
12
|
+
status?: 'complete' | 'active' | 'pending';
|
|
13
|
+
closedBy?: string;
|
|
14
|
+
closedWhen?: string;
|
|
15
|
+
dependsOn?: Contract[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function defineContract<T extends Record<string, z.ZodTypeAny>>(
|
|
19
|
+
contract: Contract<T>,
|
|
20
|
+
): Contract<T> {
|
|
21
|
+
if ('id' in contract && contract.id !== undefined && contract.id.trim() === '') {
|
|
22
|
+
throw new Error('defineContract: id must be a non-empty string if provided');
|
|
23
|
+
}
|
|
24
|
+
return contract;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isContract(value: unknown): value is Contract {
|
|
28
|
+
return (
|
|
29
|
+
typeof value === 'object' &&
|
|
30
|
+
value !== null &&
|
|
31
|
+
'value' in value &&
|
|
32
|
+
typeof (value as Record<string, unknown>).value === 'string' &&
|
|
33
|
+
'output' in value &&
|
|
34
|
+
typeof (value as Record<string, unknown>).output === 'object' &&
|
|
35
|
+
(value as Record<string, unknown>).output !== null &&
|
|
36
|
+
!Array.isArray((value as Record<string, unknown>).output)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { externalWithId } from './external-with-id.fixture.js';
|
|
3
|
+
|
|
4
|
+
// Consumes an external contract that has an explicit id — resolves via c.id
|
|
5
|
+
export const myContract = {
|
|
6
|
+
value: 'Contract consuming known external',
|
|
7
|
+
output: { result: z.string() },
|
|
8
|
+
consumes: [externalWithId],
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { externalNoId } from './external-no-id.fixture.js';
|
|
3
|
+
|
|
4
|
+
// Consumes an external contract with no id — triggers exactly one warning
|
|
5
|
+
export const myContract = {
|
|
6
|
+
value: 'Contract consuming unresolvable external',
|
|
7
|
+
output: { result: z.string() },
|
|
8
|
+
consumes: [externalNoId],
|
|
9
|
+
};
|