@specferret/core 0.4.1 → 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.
@@ -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?: Contract[];
8
+ consumes?: ContractRef[];
8
9
  forbids?: string[];
9
10
  status?: 'complete' | 'active' | 'pending';
10
11
  closedBy?: string;
11
12
  closedWhen?: string;
12
- dependsOn?: Contract[];
13
+ dependsOn?: ContractRef[];
13
14
  }
14
- export declare function defineContract<T extends Record<string, z.ZodTypeAny>>(contract: Contract<T>): Contract<T>;
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
@@ -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;IAC5D,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,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,QAAQ,EAAE,CAAC;CACxB;AAED,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,CAKb;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAW5D"}
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' &&
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAiBA,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,QAAQ,CAAC;AAClB,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
+ {"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,CAkEzF"}
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,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC;QACtF,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"}
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.1",
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": "^3.22.4",
32
+ "zod": "^4.0.0",
33
33
  "zod-to-json-schema": "^3.25.2"
34
34
  },
35
35
  "devDependencies": {
@@ -53,9 +53,11 @@ describe('isContract', () => {
53
53
  });
54
54
 
55
55
  describe('defineContract', () => {
56
- it('returns the exact object passed to it when valid', () => {
56
+ it('returns the same object reference with schema added in-place', () => {
57
57
  const contract = { value: 'x', output: {} };
58
- expect(defineContract(contract)).toBe(contract);
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
- consumes?: Contract[];
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?: Contract[];
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
- const shape = zodToJsonSchema(z.object(exportValue.output), { $refStrategy: 'none' });
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