@specferret/core 0.4.0 → 0.4.2
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/contract.d.ts +10 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -1
- package/dist/contract.js.map +1 -1
- package/dist/extractor/typescript-contract.d.ts.map +1 -1
- package/dist/extractor/typescript-contract.js +1 -0
- package/dist/extractor/typescript-contract.js.map +1 -1
- package/package.json +2 -2
- package/src/contract.test.ts +29 -2
- package/src/contract.ts +13 -4
- package/src/extractor/typescript-contract.ts +2 -1
package/dist/contract.d.ts
CHANGED
|
@@ -3,14 +3,21 @@ export interface Contract<T extends Record<string, z.ZodTypeAny> = Record<string
|
|
|
3
3
|
id?: string;
|
|
4
4
|
value: string;
|
|
5
5
|
output: T;
|
|
6
|
+
schema?: z.ZodObject<T>;
|
|
6
7
|
invariants?: Array<(r: z.infer<z.ZodObject<T>>) => boolean>;
|
|
7
|
-
consumes?:
|
|
8
|
+
consumes?: ContractRef[];
|
|
8
9
|
forbids?: string[];
|
|
9
10
|
status?: 'complete' | 'active' | 'pending';
|
|
10
11
|
closedBy?: string;
|
|
11
12
|
closedWhen?: string;
|
|
12
|
-
dependsOn?:
|
|
13
|
+
dependsOn?: ContractRef[];
|
|
13
14
|
}
|
|
14
|
-
export
|
|
15
|
+
export type ContractRef = Omit<Contract<any>, 'invariants' | 'schema'> & {
|
|
16
|
+
invariants?: Array<(r: any) => boolean>;
|
|
17
|
+
schema?: z.ZodObject<any>;
|
|
18
|
+
};
|
|
19
|
+
export declare function defineContract<T extends Record<string, z.ZodTypeAny>>(contract: Contract<T>): Contract<T> & {
|
|
20
|
+
schema: z.ZodObject<T>;
|
|
21
|
+
};
|
|
15
22
|
export declare function isContract(value: unknown): value is Contract;
|
|
16
23
|
//# sourceMappingURL=contract.d.ts.map
|
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,WAAW,QAAQ,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC;IAErE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,CAAC,CAAC;IACV,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,WAAW,QAAQ,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC;IAErE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,CAAC,CAAC;IACV,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACxB,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC;IAE5D,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;CAC3B;AAID,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC,GAAG;IACvE,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC;IACxC,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;CAC3B,CAAC;AAEF,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,EACnE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GACpB,QAAQ,CAAC,CAAC,CAAC,GAAG;IAAE,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;CAAE,CAK1C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAW5D"}
|
package/dist/contract.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
export function defineContract(contract) {
|
|
2
3
|
if ('id' in contract && contract.id !== undefined && contract.id.trim() === '') {
|
|
3
4
|
throw new Error('defineContract: id must be a non-empty string if provided');
|
|
4
5
|
}
|
|
5
|
-
return contract;
|
|
6
|
+
return Object.assign(contract, { schema: z.object(contract.output) });
|
|
6
7
|
}
|
|
7
8
|
export function isContract(value) {
|
|
8
9
|
return (typeof value === 'object' &&
|
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA0BxB,MAAM,UAAU,cAAc,CAC5B,QAAqB;IAErB,IAAI,IAAI,IAAI,QAAQ,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAO,IAAI,KAAK;QAChB,OAAQ,KAAiC,CAAC,KAAK,KAAK,QAAQ;QAC5D,QAAQ,IAAI,KAAK;QACjB,OAAQ,KAAiC,CAAC,MAAM,KAAK,QAAQ;QAC5D,KAAiC,CAAC,MAAM,KAAK,IAAI;QAClD,CAAC,KAAK,CAAC,OAAO,CAAE,KAAiC,CAAC,MAAM,CAAC,CAC1D,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typescript-contract.d.ts","sourceRoot":"","sources":["../../src/extractor/typescript-contract.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"typescript-contract.d.ts","sourceRoot":"","sources":["../../src/extractor/typescript-contract.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmEzF"}
|
|
@@ -27,6 +27,7 @@ export async function extractFromContractFile(filePath) {
|
|
|
27
27
|
for (const [exportName, exportValue] of Object.entries(mod)) {
|
|
28
28
|
if (!isContract(exportValue))
|
|
29
29
|
continue;
|
|
30
|
+
// zod-to-json-schema@3 types reference zod@3's ZodTypeDef; cast is safe — runtime supports zod@4
|
|
30
31
|
const shape = zodToJsonSchema(z.object(exportValue.output), { $refStrategy: 'none' });
|
|
31
32
|
const shape_hash = hashSchema(shape);
|
|
32
33
|
// Pass 2: resolve consumes → import IDs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typescript-contract.js","sourceRoot":"","sources":["../../src/extractor/typescript-contract.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,qEAAqE;AAErE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,QAAgB;IAC5D,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAA4B,CAAC;IAEhE,sFAAsF;IACtF,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,KAAK,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5D,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5B,aAAa,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,QAAQ;YACR,QAAQ,EAAE,MAAM;YAChB,SAAS,EAAE,EAAE;YACb,WAAW,EAAE,YAAY;YACzB,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE;YACvB,OAAO,EAAE,gBAAgB;SAC1B,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAkC,EAAE,CAAC;IAEpD,KAAK,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5D,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,SAAS;QAEvC,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"typescript-contract.js","sourceRoot":"","sources":["../../src/extractor/typescript-contract.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,qEAAqE;AAErE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,QAAgB;IAC5D,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAA4B,CAAC;IAEhE,sFAAsF;IACtF,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,KAAK,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5D,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5B,aAAa,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,QAAQ;YACR,QAAQ,EAAE,MAAM;YAChB,SAAS,EAAE,EAAE;YACb,WAAW,EAAE,YAAY;YACzB,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE;YACvB,OAAO,EAAE,gBAAgB;SAC1B,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAkC,EAAE,CAAC;IAEpD,KAAK,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5D,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,SAAS;QAEvC,iGAAiG;QACjG,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAQ,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7F,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QAErC,wCAAwC;QACxC,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;YACzB,KAAK,MAAM,QAAQ,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;gBAC5C,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC7C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;oBAC3B,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACzB,CAAC;qBAAM,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;oBACrC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;gBAC5B,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6DAA6D,QAAQ,eAAe,UAAU,uEAAuE,CACtK,CAAC;oBACF,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;QAED,SAAS,CAAC,IAAI,CAAC;YACb,EAAE,EAAE,UAAU;YACd,IAAI,EAAE,MAAM;YACZ,KAAK;YACL,UAAU;YACV,OAAO;YACP,UAAU,EAAE,QAAQ;YACpB,YAAY,EAAE,UAAU;SACzB,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,MAAM;QAChB,SAAS;QACT,WAAW,EAAE,YAAY;QACzB,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE;KACxB,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specferret/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "SpecFerret core engine — spec drift detection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"gray-matter": "^4.0.3",
|
|
30
30
|
"tree-sitter": "^0.22.4",
|
|
31
31
|
"tree-sitter-typescript": "^0.23.2",
|
|
32
|
-
"zod": "^
|
|
32
|
+
"zod": "^4.0.0",
|
|
33
33
|
"zod-to-json-schema": "^3.25.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
package/src/contract.test.ts
CHANGED
|
@@ -53,9 +53,11 @@ describe('isContract', () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
describe('defineContract', () => {
|
|
56
|
-
it('returns the
|
|
56
|
+
it('returns the same object reference with schema added in-place', () => {
|
|
57
57
|
const contract = { value: 'x', output: {} };
|
|
58
|
-
|
|
58
|
+
const result = defineContract(contract);
|
|
59
|
+
expect(result === contract).toBe(true);
|
|
60
|
+
expect(result.schema).toBeDefined();
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
it('does not throw when id is omitted', () => {
|
|
@@ -96,4 +98,29 @@ describe('defineContract', () => {
|
|
|
96
98
|
});
|
|
97
99
|
expect(isContract(contract)).toBe(true);
|
|
98
100
|
});
|
|
101
|
+
|
|
102
|
+
it('schema is present on the returned object', () => {
|
|
103
|
+
const result = defineContract({ value: 'x', output: { ok: z.boolean() } });
|
|
104
|
+
expect(result.schema).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('schema is a ZodObject that validates the output shape', () => {
|
|
108
|
+
const contract = defineContract({ value: 'x', output: { name: z.string(), age: z.number() } });
|
|
109
|
+
expect(contract.schema.safeParse({ name: 'Alice', age: 30 }).success).toBe(true);
|
|
110
|
+
expect(contract.schema.safeParse({ name: 'Alice' }).success).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('schema enables composition — z.array(contract.schema) parses correctly', () => {
|
|
114
|
+
const item = defineContract({ value: 'item', output: { id: z.string() } });
|
|
115
|
+
const list = z.array(item.schema);
|
|
116
|
+
expect(list.safeParse([{ id: 'a' }, { id: 'b' }]).success).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('consumes accepts contracts with distinct output shapes without casting', () => {
|
|
120
|
+
const a = defineContract({ id: 'a', value: 'a', output: { x: z.string() } });
|
|
121
|
+
const b = defineContract({ id: 'b', value: 'b', output: { y: z.number() } });
|
|
122
|
+
// TypeScript must accept this without error — this is the variance regression test
|
|
123
|
+
const c = defineContract({ id: 'c', value: 'c', output: {}, consumes: [a, b] });
|
|
124
|
+
expect(c.consumes).toHaveLength(2);
|
|
125
|
+
});
|
|
99
126
|
});
|
package/src/contract.ts
CHANGED
|
@@ -6,22 +6,31 @@ export interface Contract<
|
|
|
6
6
|
id?: string;
|
|
7
7
|
value: string;
|
|
8
8
|
output: T;
|
|
9
|
+
schema?: z.ZodObject<T>;
|
|
9
10
|
invariants?: Array<(r: z.infer<z.ZodObject<T>>) => boolean>;
|
|
10
|
-
|
|
11
|
+
// ContractRef avoids invariance errors — dependency arrays don't need invariant type safety
|
|
12
|
+
consumes?: ContractRef[];
|
|
11
13
|
forbids?: string[];
|
|
12
14
|
status?: 'complete' | 'active' | 'pending';
|
|
13
15
|
closedBy?: string;
|
|
14
16
|
closedWhen?: string;
|
|
15
|
-
dependsOn?:
|
|
17
|
+
dependsOn?: ContractRef[];
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
// Non-generic reference type for dependency arrays.
|
|
21
|
+
// Uses (r: any) => boolean so contracts with different output shapes are assignable without casts.
|
|
22
|
+
export type ContractRef = Omit<Contract<any>, 'invariants' | 'schema'> & {
|
|
23
|
+
invariants?: Array<(r: any) => boolean>;
|
|
24
|
+
schema?: z.ZodObject<any>;
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
export function defineContract<T extends Record<string, z.ZodTypeAny>>(
|
|
19
28
|
contract: Contract<T>,
|
|
20
|
-
): Contract<T> {
|
|
29
|
+
): Contract<T> & { schema: z.ZodObject<T> } {
|
|
21
30
|
if ('id' in contract && contract.id !== undefined && contract.id.trim() === '') {
|
|
22
31
|
throw new Error('defineContract: id must be a non-empty string if provided');
|
|
23
32
|
}
|
|
24
|
-
return contract;
|
|
33
|
+
return Object.assign(contract, { schema: z.object(contract.output) });
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
export function isContract(value: unknown): value is Contract {
|
|
@@ -34,7 +34,8 @@ export async function extractFromContractFile(filePath: string): Promise<Extract
|
|
|
34
34
|
for (const [exportName, exportValue] of Object.entries(mod)) {
|
|
35
35
|
if (!isContract(exportValue)) continue;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// zod-to-json-schema@3 types reference zod@3's ZodTypeDef; cast is safe — runtime supports zod@4
|
|
38
|
+
const shape = zodToJsonSchema(z.object(exportValue.output) as any, { $refStrategy: 'none' });
|
|
38
39
|
const shape_hash = hashSchema(shape);
|
|
39
40
|
|
|
40
41
|
// Pass 2: resolve consumes → import IDs
|