@specferret/core 0.4.1 → 0.4.3

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"}
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+ export declare const tables_dataSource: {
3
+ id: string;
4
+ value: string;
5
+ output: {
6
+ data: z.ZodString;
7
+ };
8
+ };
9
+ export declare const tables_ingestionRun: {
10
+ id: string;
11
+ value: string;
12
+ output: {
13
+ runId: z.ZodString;
14
+ };
15
+ consumes: {
16
+ id: string;
17
+ value: string;
18
+ output: {
19
+ data: z.ZodString;
20
+ };
21
+ }[];
22
+ };
23
+ //# sourceMappingURL=same-file-dotted-id-consumes.fixture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"same-file-dotted-id-consumes.fixture.d.ts","sourceRoot":"","sources":["../../../src/extractor/__fixtures__/same-file-dotted-id-consumes.fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,iBAAiB;;;;;;CAI7B,CAAC;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;CAK/B,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ // Export name uses underscores; .id uses dot notation — the standard convention.
3
+ export const tables_dataSource = {
4
+ id: 'tables.dataSource',
5
+ value: 'tables data source',
6
+ output: { data: z.string() },
7
+ };
8
+ export const tables_ingestionRun = {
9
+ id: 'tables.ingestionRun',
10
+ value: 'tables ingestion run',
11
+ output: { runId: z.string() },
12
+ consumes: [tables_dataSource],
13
+ };
14
+ //# sourceMappingURL=same-file-dotted-id-consumes.fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"same-file-dotted-id-consumes.fixture.js","sourceRoot":"","sources":["../../../src/extractor/__fixtures__/same-file-dotted-id-consumes.fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,iFAAiF;AACjF,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,EAAE,EAAE,mBAAmB;IACvB,KAAK,EAAE,oBAAoB;IAC3B,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE;CAC7B,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,EAAE,EAAE,qBAAqB;IACzB,KAAK,EAAE,sBAAsB;IAC7B,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE;IAC7B,QAAQ,EAAE,CAAC,iBAAiB,CAAC;CAC9B,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
@@ -35,7 +36,7 @@ export async function extractFromContractFile(filePath) {
35
36
  for (const consumed of exportValue.consumes) {
36
37
  const samefile = exportNameMap.get(consumed);
37
38
  if (samefile !== undefined) {
38
- imports.push(samefile);
39
+ imports.push(consumed.id ?? samefile);
39
40
  }
40
41
  else if (consumed.id !== undefined) {
41
42
  imports.push(consumed.id);
@@ -47,7 +48,7 @@ export async function extractFromContractFile(filePath) {
47
48
  }
48
49
  }
49
50
  contracts.push({
50
- id: exportName,
51
+ id: exportValue.id ?? exportName,
51
52
  type: 'type',
52
53
  shape,
53
54
  shape_hash,
@@ -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,EAAE,IAAI,QAAQ,CAAC,CAAC;gBACxC,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,WAAW,CAAC,EAAE,IAAI,UAAU;YAChC,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.3",
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 {
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ // Export name uses underscores; .id uses dot notation — the standard convention.
4
+ export const tables_dataSource = {
5
+ id: 'tables.dataSource',
6
+ value: 'tables data source',
7
+ output: { data: z.string() },
8
+ };
9
+
10
+ export const tables_ingestionRun = {
11
+ id: 'tables.ingestionRun',
12
+ value: 'tables ingestion run',
13
+ output: { runId: z.string() },
14
+ consumes: [tables_dataSource],
15
+ };
@@ -61,9 +61,7 @@ describe('extractFromContractFile', () => {
61
61
  });
62
62
 
63
63
  it('zod schema with optional fields extracts without throwing', async () => {
64
- await assert.doesNotReject(() =>
65
- extractFromContractFile(fixtures('optional-fields.fixture.ts')),
66
- );
64
+ await assert.doesNotReject(() => extractFromContractFile(fixtures('optional-fields.fixture.ts')));
67
65
 
68
66
  const result = await extractFromContractFile(fixtures('optional-fields.fixture.ts'));
69
67
  assert.equal(result.contracts.length, 1);
@@ -134,10 +132,25 @@ describe('extractFromContractFile', () => {
134
132
  assert.equal(result.contracts[0].sourceSymbol, 'userContract');
135
133
  });
136
134
 
135
+ it('stored contract id uses .id field when present, not export name', async () => {
136
+ const result = await extractFromContractFile(fixtures('same-file-dotted-id-consumes.fixture.ts'));
137
+
138
+ const ids = result.contracts.map((c) => c.id);
139
+ assert.ok(ids.includes('tables.dataSource'), 'expected tables.dataSource stored id');
140
+ assert.ok(ids.includes('tables.ingestionRun'), 'expected tables.ingestionRun stored id');
141
+ assert.ok(!ids.includes('tables_dataSource'), 'export name must not be used as stored id when .id is present');
142
+ assert.ok(!ids.includes('tables_ingestionRun'), 'export name must not be used as stored id when .id is present');
143
+ });
144
+
145
+ it('same-file consumes with dotted .id resolves to .id, not export name', async () => {
146
+ const result = await extractFromContractFile(fixtures('same-file-dotted-id-consumes.fixture.ts'));
147
+
148
+ const ingestionRun = result.contracts.find((c) => c.id === 'tables.ingestionRun');
149
+ assert.ok(ingestionRun, 'tables.ingestionRun not found');
150
+ assert.deepEqual(ingestionRun.imports, ['tables.dataSource']);
151
+ });
152
+
137
153
  it('module that throws at top-level causes extractFromContractFile to reject', async () => {
138
- await assert.rejects(
139
- () => extractFromContractFile(fixtures('throws-on-import.fixture.ts')),
140
- /intentional module-level throw/,
141
- );
154
+ await assert.rejects(() => extractFromContractFile(fixtures('throws-on-import.fixture.ts')), /intentional module-level throw/);
142
155
  });
143
156
  });
@@ -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
@@ -43,7 +44,7 @@ export async function extractFromContractFile(filePath: string): Promise<Extract
43
44
  for (const consumed of exportValue.consumes) {
44
45
  const samefile = exportNameMap.get(consumed);
45
46
  if (samefile !== undefined) {
46
- imports.push(samefile);
47
+ imports.push(consumed.id ?? samefile);
47
48
  } else if (consumed.id !== undefined) {
48
49
  imports.push(consumed.id);
49
50
  } else {
@@ -56,7 +57,7 @@ export async function extractFromContractFile(filePath: string): Promise<Extract
56
57
  }
57
58
 
58
59
  contracts.push({
59
- id: exportName,
60
+ id: exportValue.id ?? exportName,
60
61
  type: 'type',
61
62
  shape,
62
63
  shape_hash,