@adobe/data-testing 0.10.21 → 0.10.23

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.
Files changed (59) hide show
  1. package/README.md +42 -25
  2. package/dist/conformance/adapt-spec.d.ts +1 -6
  3. package/dist/conformance/adapt-spec.js +2 -0
  4. package/dist/conformance/adapt-spec.js.map +1 -1
  5. package/dist/conformance/build-double.js +22 -18
  6. package/dist/conformance/build-double.js.map +1 -1
  7. package/dist/conformance/build-double.test.d.ts +1 -0
  8. package/dist/conformance/build-double.test.js +30 -0
  9. package/dist/conformance/build-double.test.js.map +1 -0
  10. package/dist/conformance/check-feature.d.ts +5 -0
  11. package/dist/conformance/check-feature.js +50 -0
  12. package/dist/conformance/check-feature.js.map +1 -0
  13. package/dist/conformance/check-spec.d.ts +4 -0
  14. package/dist/conformance/check-spec.js +20 -0
  15. package/dist/conformance/check-spec.js.map +1 -0
  16. package/dist/conformance/feature-types.d.ts +2 -2
  17. package/dist/conformance/implementation.d.ts +41 -0
  18. package/dist/conformance/implementation.js +11 -0
  19. package/dist/conformance/implementation.js.map +1 -0
  20. package/dist/conformance/manifest.type-test.d.ts +122 -0
  21. package/dist/conformance/manifest.type-test.js +96 -0
  22. package/dist/conformance/manifest.type-test.js.map +1 -0
  23. package/dist/conformance/public.d.ts +7 -1
  24. package/dist/conformance/public.js +8 -3
  25. package/dist/conformance/public.js.map +1 -1
  26. package/dist/conformance/run-actions.d.ts +1 -0
  27. package/dist/conformance/run-actions.js +1 -1
  28. package/dist/conformance/run-actions.js.map +1 -1
  29. package/dist/conformance/run-feature.d.ts +1 -1
  30. package/dist/conformance/run-feature.js +26 -15
  31. package/dist/conformance/run-feature.js.map +1 -1
  32. package/dist/conformance/run-spec.d.ts +2 -2
  33. package/dist/conformance/run-spec.js +5 -9
  34. package/dist/conformance/run-spec.js.map +1 -1
  35. package/dist/conformance/run-systems.d.ts +33 -0
  36. package/dist/conformance/run-systems.js +126 -0
  37. package/dist/conformance/run-systems.js.map +1 -0
  38. package/dist/conformance/spec.d.ts +22 -0
  39. package/dist/conformance/spec.js +9 -0
  40. package/dist/conformance/spec.js.map +1 -0
  41. package/dist/conformance/state-matches.d.ts +14 -0
  42. package/dist/conformance/state-matches.js +3 -0
  43. package/dist/conformance/state-matches.js.map +1 -0
  44. package/dist/conformance/systems-types.d.ts +41 -0
  45. package/dist/conformance/systems-types.js +2 -0
  46. package/dist/conformance/systems-types.js.map +1 -0
  47. package/dist/conformance/systems.test.d.ts +1 -0
  48. package/dist/conformance/systems.test.js +125 -0
  49. package/dist/conformance/systems.test.js.map +1 -0
  50. package/dist/conformance/systems.type-test.d.ts +30 -0
  51. package/dist/conformance/systems.type-test.js +33 -0
  52. package/dist/conformance/systems.type-test.js.map +1 -0
  53. package/dist/conformance/with-no-op.d.ts +5 -0
  54. package/dist/conformance/with-no-op.js +14 -0
  55. package/dist/conformance/with-no-op.js.map +1 -0
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.js +2 -2
  58. package/dist/index.js.map +1 -1
  59. package/package.json +2 -2
package/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # @adobe/data-testing
2
2
 
3
- Conformance-testing utilities for `@adobe/data` ECS features — the `Match` and
4
- `Conformance` namespaces used by the co-located conformance pattern (see
5
- `@adobe/data-ai`'s `features/data/state.md` and
6
- `features/services/main-service/conformance.md`).
3
+ Conformance-testing utilities for `@adobe/data` ECS features: the `Match` and
4
+ `Conformance` namespaces behind the spec ⇄ ECS conformance pattern (see
5
+ `@adobe/data-ai`'s `features/spec/index.md` and `features/ecs/conformance.md`).
7
6
 
8
7
  This package exists separately from `@adobe/data` so that installing
9
8
  `@adobe/data` never pulls in a `vitest` peer dependency — only projects that
@@ -19,26 +18,44 @@ pnpm add -D @adobe/data-testing vitest
19
18
  ## Usage
20
19
 
21
20
  ```ts
22
- // data/state/conformance-case.ts
23
- import { Conformance as ConformanceApi } from "@adobe/data-testing";
24
- import type { State } from "./state.js";
25
-
26
- export type Conformance<F extends (...args: never[]) => unknown> = ConformanceApi.Cases<State, F>;
27
- export const entity = ConformanceApi.entity;
28
- ```
29
-
30
- ```ts
31
- // data/state/spec.test.ts
32
- import { Conformance } from "@adobe/data-testing";
33
- import { State } from "./state.js";
34
- import { transitions } from "./transitions.js";
35
-
36
- Conformance.runSpec({ state: State, transitions });
21
+ // spec/spec.ts — the pure spec: State, its actions and derivations, their cases
22
+ export const spec = Conformance.spec({
23
+ state: State,
24
+ fns: transforms,
25
+ schemas: { ...components, ...resources },
26
+ services: { analytics: AnalyticsService.createFake },
27
+ cases: { createTodo, toggleComplete, visibleTodos },
28
+ });
29
+
30
+ // spec/spec.test.ts
31
+ Conformance.checkSpec(spec);
32
+
33
+ // ecs/conformance/implementation.ts — the spec paired with its ECS build
34
+ export const implementation = Conformance.implementation(spec, {
35
+ plugin: MainService.plugin,
36
+ computedPlugin: ComputedDatabase.plugin,
37
+ projection,
38
+ hydrate: ["visibleTodos"],
39
+ });
40
+
41
+ // ecs/conformance/conformance.test.ts
42
+ Conformance.checkFeature(implementation);
37
43
  ```
38
44
 
39
- `Match` provides tolerant, matcher-aware value comparison (`matches`, `ref`,
40
- `anyNumber`, `anyString`) used by the `after` side of a conformance case.
41
- `Conformance` provides the case types (`Cases`, `DerivationCases`, `Effects`)
42
- and the runner drivers (`runSpec`, `runTransactions`, `runActions`,
43
- `runComputeds`, `runFeature`) that drive vitest's `describe`/`it` against
44
- spec-owned cases.
45
+ - **`checkSpec`** runs every case against the pure spec functions. No ECS is built.
46
+ - **`checkFeature`** runs every case against the ECS:
47
+ - each spec action against the same-named action;
48
+ - each derivation against its computed;
49
+ - each ECS system against the same-named spec system function, running just that
50
+ system;
51
+ - each frame case by running every modelled system once, in the order the systems'
52
+ `schedule`s declare (the `frame` option lists arg writers and unmodelled systems).
53
+
54
+ It also round-trips `State.samples` through the projection. A spec op with no
55
+ implementation fails by name.
56
+ - **`Conformance.StateMatches<State, typeof resources, typeof components>`** pins a
57
+ `State`'s keys to the feature's resource and component names.
58
+ - **`Match`** provides tolerant, matcher-aware comparison (`matches`, `ref`,
59
+ `anyNumber`, `anyString`).
60
+ - The lower-level runners (`runActions`, `runComputeds`, `runTransactions`,
61
+ `adaptCases`) remain for a feature that needs per-case ambient context.
@@ -1,8 +1,3 @@
1
- import type { Schema } from "@adobe/data/schema";
2
1
  type AnyFn = (...args: never[]) => unknown;
3
- type LooseCaseModule = {
4
- readonly args?: Schema;
5
- readonly cases: readonly Record<string, unknown>[];
6
- };
7
- export declare const adaptCases: (fns: Readonly<Record<string, AnyFn>>, cases: Readonly<Record<string, LooseCaseModule>>, services: Readonly<Record<string, () => object>> | undefined, injectServices: boolean) => Record<string, Record<string, unknown>>;
2
+ export declare const adaptCases: (fns: Readonly<Record<string, AnyFn>>, cases: object, services: Readonly<Record<string, () => object>> | undefined, injectServices: boolean) => Record<string, Record<string, unknown>>;
8
3
  export {};
@@ -27,6 +27,8 @@ const injectDoubles = (testCase, services) => {
27
27
  export const adaptCases = (fns, cases, services, injectServices) => {
28
28
  const out = {};
29
29
  for (const [name, fn] of Object.entries(fns)) {
30
+ // Runtime invariant: a manifest's `cases` holds one case module per fn (the
31
+ // `Conformance.spec` coverage guard).
30
32
  const module = cases[name];
31
33
  const list = module.cases;
32
34
  const first = list[0];
@@ -1 +1 @@
1
- {"version":3,"file":"adapt-spec.js","sourceRoot":"","sources":["../../src/conformance/adapt-spec.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAiB,MAAM,mBAAmB,CAAC;AAQ/D,oFAAoF;AACpF,8EAA8E;AAC9E,mFAAmF;AACnF,oFAAoF;AACpF,kFAAkF;AAClF,gFAAgF;AAChF,MAAM,aAAa,GAAG,CACpB,QAAiC,EACjC,QAAgD,EACvB,EAAE;IAC3B,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAmD,CAAC;IAC1F,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAuB,CAAC;IACpD,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC;IACjE,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;AAC1D,CAAC,CAAC;AAEF,2EAA2E;AAC3E,oFAAoF;AACpF,gFAAgF;AAChF,gFAAgF;AAChF,mFAAmF;AACnF,qFAAqF;AACrF,kFAAkF;AAClF,gFAAgF;AAChF,qEAAqE;AACrE,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,GAAoC,EACpC,KAAgD,EAChD,QAA4D,EAC5D,cAAuB,EACkB,EAAE;IAC3C,MAAM,GAAG,GAA4C,EAAE,CAAC;IACxD,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAwC,CAAC;QAC7D,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;YAC5C,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;YAC1D,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,cAAc,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9G,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG;YACjB,CAAC,IAAI,CAAC,EAAE,EAAE;YACV,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE;SAChF,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
1
+ {"version":3,"file":"adapt-spec.js","sourceRoot":"","sources":["../../src/conformance/adapt-spec.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAiB,MAAM,mBAAmB,CAAC;AAQ/D,oFAAoF;AACpF,8EAA8E;AAC9E,mFAAmF;AACnF,oFAAoF;AACpF,kFAAkF;AAClF,gFAAgF;AAChF,MAAM,aAAa,GAAG,CACpB,QAAiC,EACjC,QAAgD,EACvB,EAAE;IAC3B,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAmD,CAAC;IAC1F,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAuB,CAAC;IACpD,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC;IACjE,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;AAC1D,CAAC,CAAC;AAEF,2EAA2E;AAC3E,oFAAoF;AACpF,gFAAgF;AAChF,gFAAgF;AAChF,mFAAmF;AACnF,qFAAqF;AACrF,kFAAkF;AAClF,gFAAgF;AAChF,qEAAqE;AACrE,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,GAAoC,EACpC,KAAa,EACb,QAA4D,EAC5D,cAAuB,EACkB,EAAE;IAC3C,MAAM,GAAG,GAA4C,EAAE,CAAC;IACxD,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,4EAA4E;QAC5E,sCAAsC;QACtC,MAAM,MAAM,GAAI,KAAmD,CAAC,IAAI,CAAE,CAAC;QAC3E,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAwC,CAAC;QAC7D,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;YAC5C,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;YAC1D,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,cAAc,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9G,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG;YACjB,CAAC,IAAI,CAAC,EAAE,EAAE;YACV,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE;SAChF,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
@@ -1,32 +1,36 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
  // Build a recording double for one service from a SHAPE-ONLY template and a case's
3
- // response schedule. The template — the feature's registered fake — is called once
4
- // purely to enumerate the service's members (types are erased at runtime, and Proxy
5
- // is banned on these paths, so the method names must come from a real value); its
6
- // own behavior is never used. Every value-returning call drains that method's FIFO;
7
- // a `void` method (or any method with no schedule) returns `undefined`. Draining an
8
- // EXHAUSTED schedule throws, so a transform that calls a method more times than the
9
- // case scheduled is a hard failure — the determinism a conformance oracle needs
10
- // (the old fakes cycled, silently masking an over-call).
3
+ // response schedule. The template — the feature's registered fake — supplies the
4
+ // method names (types are erased at runtime, and Proxy is banned on these paths), and
5
+ // one probe call per method tells a value method (returns something) from a void one
6
+ // (returns `undefined`); fakes are inert, so probing is safe. Every value-returning
7
+ // call drains that method's FIFO. Calling a value method with no response scheduled,
8
+ // or more times than scheduled, throws: a case must own every value its code reads.
11
9
  //
12
- // The returned double still gets wrapped by `recordCalls` (call recording) on its
13
- // way into a case's args, exactly like the live fakes it replaces — so effect
14
- // assertion is unchanged; only the RETURNS now come from the case, not the fake.
10
+ // The returned double still gets wrapped by `recordCalls` on its way into a case's
11
+ // args, so effect assertion is unchanged; only the RETURNS come from the case.
15
12
  export const buildDouble = (template, responses) => {
16
13
  const shape = template();
14
+ const service = "serviceName" in shape ? String(shape.serviceName) : "service";
17
15
  const queues = new Map(Object.entries(responses ?? {}).map(([method, schedule]) => [method, [...schedule]]));
18
- return Object.fromEntries(Object.entries(shape).map(([key, value]) => typeof value !== "function"
19
- ? [key, value] // serviceName and other non-method members pass through
20
- : [
16
+ return Object.fromEntries(Object.entries(shape).map(([key, value]) => {
17
+ if (typeof value !== "function")
18
+ return [key, value]; // serviceName and other non-method members pass through
19
+ const returnsValue = value() !== undefined;
20
+ return [
21
21
  key,
22
22
  (..._args) => {
23
23
  const queue = queues.get(key);
24
- if (!queue)
25
- return undefined; // void method, or an unscheduled call
24
+ if (queue === undefined) {
25
+ if (!returnsValue)
26
+ return undefined; // a void method
27
+ throw new Error(`no response scheduled for "${service}.${key}"; add it to the case's responses`);
28
+ }
26
29
  if (queue.length === 0)
27
- throw new Error(`response schedule exhausted for "${key}"`);
30
+ throw new Error(`response schedule exhausted for "${service}.${key}"`);
28
31
  return queue.shift();
29
32
  },
30
- ]));
33
+ ];
34
+ }));
31
35
  };
32
36
  //# sourceMappingURL=build-double.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-double.js","sourceRoot":"","sources":["../../src/conformance/build-double.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAMvD,mFAAmF;AACnF,mFAAmF;AACnF,oFAAoF;AACpF,kFAAkF;AAClF,oFAAoF;AACpF,oFAAoF;AACpF,oFAAoF;AACpF,gFAAgF;AAChF,yDAAyD;AACzD,EAAE;AACF,kFAAkF;AAClF,8EAA8E;AAC9E,iFAAiF;AACjF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAsB,EAAE,SAA+B,EAAU,EAAE;IAC7F,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CACrF,CAAC;IACF,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CACzC,OAAO,KAAK,KAAK,UAAU;QACzB,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,wDAAwD;QACvE,CAAC,CAAC;YACE,GAAG;YACH,CAAC,GAAG,KAAgB,EAAW,EAAE;gBAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAC9B,IAAI,CAAC,KAAK;oBAAE,OAAO,SAAS,CAAC,CAAC,sCAAsC;gBACpE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,GAAG,GAAG,CAAC,CAAC;gBACpF,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;SACF,CACN,CACF,CAAC;AACJ,CAAC,CAAC"}
1
+ {"version":3,"file":"build-double.js","sourceRoot":"","sources":["../../src/conformance/build-double.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAMvD,mFAAmF;AACnF,iFAAiF;AACjF,sFAAsF;AACtF,qFAAqF;AACrF,oFAAoF;AACpF,qFAAqF;AACrF,oFAAoF;AACpF,EAAE;AACF,mFAAmF;AACnF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAsB,EAAE,SAA+B,EAAU,EAAE;IAC7F,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,aAAa,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/E,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CACrF,CAAC;IACF,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QACzC,IAAI,OAAO,KAAK,KAAK,UAAU;YAAE,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,wDAAwD;QAC9G,MAAM,YAAY,GAAI,KAAuB,EAAE,KAAK,SAAS,CAAC;QAC9D,OAAO;YACL,GAAG;YACH,CAAC,GAAG,KAAgB,EAAW,EAAE;gBAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAC9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACxB,IAAI,CAAC,YAAY;wBAAE,OAAO,SAAS,CAAC,CAAC,gBAAgB;oBACrD,MAAM,IAAI,KAAK,CAAC,8BAA8B,OAAO,IAAI,GAAG,mCAAmC,CAAC,CAAC;gBACnG,CAAC;gBACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,OAAO,IAAI,GAAG,GAAG,CAAC,CAAC;gBAC/F,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;SACF,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, it, expect } from "vitest";
3
+ import { buildDouble } from "./build-double.js";
4
+ const template = () => ({
5
+ serviceName: "rng",
6
+ next: () => 0,
7
+ fetch: () => Promise.resolve(""),
8
+ log: () => { },
9
+ });
10
+ describe("buildDouble", () => {
11
+ it("returns scheduled responses in order", () => {
12
+ const double = buildDouble(template, { next: [1, 2] });
13
+ expect([double.next(), double.next()]).toEqual([1, 2]);
14
+ });
15
+ it("throws on a value method with no response scheduled", () => {
16
+ const double = buildDouble(template, undefined);
17
+ expect(() => double.next()).toThrow('no response scheduled for "rng.next"');
18
+ expect(() => double.fetch()).toThrow('no response scheduled for "rng.fetch"');
19
+ });
20
+ it("throws once a schedule is exhausted", () => {
21
+ const double = buildDouble(template, { next: [1] });
22
+ double.next();
23
+ expect(() => double.next()).toThrow('response schedule exhausted for "rng.next"');
24
+ });
25
+ it("lets a void method run unscheduled", () => {
26
+ const double = buildDouble(template, undefined);
27
+ expect(double.log()).toBeUndefined();
28
+ });
29
+ });
30
+ //# sourceMappingURL=build-double.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-double.test.js","sourceRoot":"","sources":["../../src/conformance/build-double.test.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,CAAC;IACtB,WAAW,EAAE,KAAK;IAClB,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IACb,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;IAChC,GAAG,EAAE,GAAG,EAAE,GAAE,CAAC;CACd,CAAC,CAAC;AAGH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAQ,CAAC;QAC9D,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAQ,CAAC;QACvD,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC;QAC5E,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAQ,CAAC;QAC3D,MAAM,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAQ,CAAC;QACvD,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { SchemaSource } from "./refify.js";
2
+ import type { Implementation } from "./implementation.js";
3
+ type AnyFn = (...args: never[]) => unknown;
4
+ export declare const checkFeature: <State extends object, Fns extends Record<string, AnyFn>, StoreT extends SchemaSource>(impl: Implementation<State, Fns, StoreT>) => void;
5
+ export {};
@@ -0,0 +1,50 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { Database } from "@adobe/data/ecs";
3
+ import { runFeature } from "./run-feature.js";
4
+ import { runSystems } from "./run-systems.js";
5
+ import { adaptCases } from "./adapt-spec.js";
6
+ import { withNoOp } from "./with-no-op.js";
7
+ // Frame cases with recording doubles injected into their `args`, as `adaptCases` does
8
+ // for an op's cases.
9
+ const frameCasesOf = (frame, services) => {
10
+ if (frame === undefined)
11
+ return [];
12
+ const adapted = adaptCases({ frame: () => undefined }, { frame }, services, true)["./frame"];
13
+ // Runtime invariant: `adaptCases` keeps a transition envelope's `{ cases }` list.
14
+ return adapted["cases"].cases;
15
+ };
16
+ // The ecs conformance suite: the spec's cases drive the implementation's actions and
17
+ // computeds (fresh doubles again, so the pure and ecs runs never share FIFO state).
18
+ // A spec op with no same-named implementation is a named failure.
19
+ export const checkFeature = (impl) => {
20
+ const { spec } = impl;
21
+ runFeature({
22
+ state: spec.state,
23
+ transitions: adaptCases(spec.fns, spec.cases, spec.services, true),
24
+ plugin: impl.plugin,
25
+ computedPlugin: impl.computedPlugin,
26
+ projection: impl.projection,
27
+ hydrate: impl.hydrate,
28
+ services: impl.services,
29
+ match: spec.match,
30
+ ops: impl.ops,
31
+ });
32
+ if (spec.systems && impl.frame) {
33
+ const baseServices = () => Object.fromEntries(Object.entries(impl.services ?? {}).map(([name, create]) => [name, create()]));
34
+ runSystems({
35
+ // Runtime invariant: a plugin builds a database whose system surface and store
36
+ // are the projection's (`implementation` types `plugin` and `projection` together).
37
+ makeDb: (services) => Database.toSystemDatabase(Database.create(impl.plugin, { services: { ...baseServices(), ...services } })),
38
+ fromState: impl.projection.fromState,
39
+ toState: impl.projection.toState,
40
+ initial: spec.state.create(),
41
+ systems: adaptCases(spec.systems.fns, withNoOp(spec.systems.cases, spec.systems.noOp), spec.services, true),
42
+ frameCases: () => frameCasesOf(impl.frame?.cases, spec.services),
43
+ // Runtime invariant: `implementation` types each writer against this db and arg.
44
+ args: impl.frame.args,
45
+ unmodelled: impl.frame.unmodelled ?? {},
46
+ match: spec.match,
47
+ });
48
+ }
49
+ };
50
+ //# sourceMappingURL=check-feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-feature.js","sourceRoot":"","sources":["../../src/conformance/check-feature.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { Spec } from "./spec.js";
2
+ type AnyFn = (...args: never[]) => unknown;
3
+ export declare const checkSpec: <State extends object, Fns extends Record<string, AnyFn>, C extends object, Sys extends Record<string, AnyFn>>(spec: Spec<State, Fns, C, Sys>) => void;
4
+ export {};
@@ -0,0 +1,20 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { runSpec } from "./run-spec.js";
3
+ import { adaptCases } from "./adapt-spec.js";
4
+ import { withNoOp } from "./with-no-op.js";
5
+ // The pure-spec suite: run every case against its spec fn, with fresh recording
6
+ // doubles built from the `services` templates + each case's `responses`. Reference
7
+ // fields are found from `spec.schemas`; no store or plugin is built. Per-system cases
8
+ // run here too; whole-frame cases need the ECS's system order, so `checkFeature` runs them.
9
+ export const checkSpec = (spec) => {
10
+ runSpec({
11
+ state: spec.state,
12
+ transitions: {
13
+ ...adaptCases(spec.fns, spec.cases, spec.services, true),
14
+ ...(spec.systems ? adaptCases(spec.systems.fns, withNoOp(spec.systems.cases, spec.systems.noOp), spec.services, true) : {}),
15
+ },
16
+ schemas: spec.schemas,
17
+ match: spec.match,
18
+ });
19
+ };
20
+ //# sourceMappingURL=check-spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-spec.js","sourceRoot":"","sources":["../../src/conformance/check-spec.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAK3C,gFAAgF;AAChF,mFAAmF;AACnF,sFAAsF;AACtF,4FAA4F;AAC5F,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,IAA8B,EACxB,EAAE;IACR,OAAO,CAAC;QACN,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,WAAW,EAAE;YACX,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5H;QACD,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -4,7 +4,7 @@ type MethodKeys<T> = {
4
4
  [K in keyof T]-?: T[K] extends (...a: never[]) => unknown ? K : never;
5
5
  }[keyof T];
6
6
  type IsService<T> = T extends readonly unknown[] ? false : T extends (...a: never[]) => unknown ? false : T extends object ? [MethodKeys<T>] extends [never] ? false : true : false;
7
- type ArgsOf<F> = F extends (...a: never[]) => unknown ? Parameters<F> extends [unknown, infer A, ...unknown[]] ? A : void : void;
7
+ export type ArgsOf<F> = F extends (...a: never[]) => unknown ? Parameters<F> extends [unknown, infer A, ...unknown[]] ? A : void : void;
8
8
  type Payload<R> = R extends Promise<infer T> ? T : R extends AsyncGenerator<infer T, unknown, unknown> ? T : R;
9
9
  type ScheduleOf<M> = M extends (...a: never[]) => infer R ? readonly Payload<R>[] : never;
10
10
  type ValueMethodKeys<S> = {
@@ -16,7 +16,7 @@ type ServiceResponses<S> = {
16
16
  export type Responses<Args> = {
17
17
  readonly [K in keyof Args as IsService<Args[K]> extends true ? K : never]?: ServiceResponses<Args[K]>;
18
18
  };
19
- type DataArgKeys<Args> = {
19
+ export type DataArgKeys<Args> = {
20
20
  [K in keyof Args]-?: IsService<Args[K]> extends true ? never : K;
21
21
  }[keyof Args];
22
22
  type DataArgs<Args> = Pick<Args, DataArgKeys<Args>>;
@@ -0,0 +1,41 @@
1
+ import type { Database } from "@adobe/data/ecs";
2
+ import type { HasDerivations, HydrateExact, HydrationKeys, ProjectionShape, ProjectionStore, ProjectionValue } from "./feature-types.js";
3
+ import type { Projection } from "./run-feature.js";
4
+ import type { Spec } from "./spec.js";
5
+ import type { FrameArgWriters, FrameCases, SystemKeys, SystemsExact } from "./systems-types.js";
6
+ type AnyFn = (...args: never[]) => unknown;
7
+ export interface Implementation<State extends object, Fns extends Record<string, AnyFn>, StoreT> {
8
+ readonly spec: Spec<State, Fns, object, Record<string, AnyFn>>;
9
+ readonly plugin: Database.Plugin;
10
+ readonly frame?: {
11
+ readonly args: Readonly<Record<string, (db: never, value: never) => void>>;
12
+ readonly unmodelled?: Readonly<Record<string, string>>;
13
+ readonly cases?: object;
14
+ };
15
+ readonly computedPlugin?: Database.Plugin;
16
+ readonly projection: Projection<StoreT, State>;
17
+ readonly hydrate?: readonly string[];
18
+ readonly services?: Readonly<Record<string, () => object>>;
19
+ readonly ops?: {
20
+ readonly actions?: Record<string, unknown>;
21
+ readonly computeds?: Record<string, unknown>;
22
+ };
23
+ }
24
+ export declare const implementation: <State extends object, Fns extends Record<string, AnyFn>, C extends object, P extends ProjectionShape<State>, const H extends readonly Extract<keyof Fns, string>[] = readonly [], PL extends Database.Plugin = Database.Plugin, Sys extends Record<string, AnyFn> = {}, U extends string = never>(spec: Spec<State, Fns, C, Sys>, impl: Omit<Implementation<State, Fns, ProjectionStore<P>>, "spec" | "plugin" | "projection" | "hydrate" | "computedPlugin" | "frame"> & {
25
+ readonly plugin: PL;
26
+ readonly projection: P;
27
+ readonly hydrate?: H;
28
+ } & (HasDerivations<C> extends true ? {
29
+ readonly computedPlugin: Database.Plugin;
30
+ } : {
31
+ readonly computedPlugin?: Database.Plugin;
32
+ }) & HydrateExact<H, HydrationKeys<Fns, ProjectionValue<P>>> & ([keyof Sys] extends [never] ? {
33
+ readonly frame?: undefined;
34
+ } : {
35
+ readonly frame: {
36
+ readonly args: FrameArgWriters<PL, Sys>;
37
+ readonly unmodelled?: { readonly [K in U]: string; };
38
+ readonly cases?: FrameCases<State, Sys>;
39
+ } & SystemsExact<Extract<keyof Sys, string>, U, SystemKeys<PL>>;
40
+ })) => Implementation<State, Fns, ProjectionStore<P>>;
41
+ export {};
@@ -0,0 +1,11 @@
1
+ // Pair a spec with its ECS implementation. A typed identity that arms the guards:
2
+ // • `hydrate` exactness — EXACTLY the entity-list derivations (`HydrateExact`).
3
+ // • `computedPlugin` requiredness — required when the spec has a derivation.
4
+ // • `frame` requiredness and exactness — required when the spec has systems; one arg
5
+ // writer per frame data arg; every ECS system modelled xor unmodelled.
6
+ export const implementation = (spec, impl) =>
7
+ // Runtime invariant the checker can't see: `impl` plus the spec IS this
8
+ // Implementation (`P` widens to the stored `Projection`, `H` to `readonly string[]`;
9
+ // the requiredness factors are compile-only phantoms).
10
+ ({ ...impl, spec });
11
+ //# sourceMappingURL=implementation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"implementation.js","sourceRoot":"","sources":["../../src/conformance/implementation.ts"],"names":[],"mappings":"AAkDA,kFAAkF;AAClF,kFAAkF;AAClF,+EAA+E;AAC/E,uFAAuF;AACvF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,cAAc,GAAG,CAU5B,IAA8B,EAC9B,IAcQ,EACwC,EAAE;AAClD,wEAAwE;AACxE,qFAAqF;AACrF,uDAAuD;AACvD,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAA8D,CAAC"}
@@ -0,0 +1,122 @@
1
+ import type { CaseModule, SpecCases, SpecDerivations } from "./feature-types.js";
2
+ type Foo = {
3
+ readonly name: string;
4
+ };
5
+ type State = {
6
+ readonly entities: ReadonlyMap<number, Foo>;
7
+ readonly count: number;
8
+ };
9
+ declare const bump: (s: Pick<State, "count">, args: {
10
+ readonly by: number;
11
+ }) => Pick<State, "count">;
12
+ declare const listFoos: (s: Pick<State, "entities">) => readonly Foo[];
13
+ interface Store {
14
+ readonly componentSchemas: Record<string, unknown>;
15
+ }
16
+ export declare const ok: import("./implementation.js").Implementation<State, {
17
+ bump: (s: Pick<State, "count">, args: {
18
+ readonly by: number;
19
+ }) => Pick<State, "count">;
20
+ listFoos: (s: Pick<State, "entities">) => readonly Foo[];
21
+ }, Store>;
22
+ export declare const missingCase: import("./implementation.js").Implementation<State, {
23
+ bump: (s: Pick<State, "count">, args: {
24
+ readonly by: number;
25
+ }) => Pick<State, "count">;
26
+ listFoos: (s: Pick<State, "entities">) => readonly Foo[];
27
+ }, Store>;
28
+ export declare const extraCase: import("./implementation.js").Implementation<State, {
29
+ bump: (s: Pick<State, "count">, args: {
30
+ readonly by: number;
31
+ }) => Pick<State, "count">;
32
+ listFoos: (s: Pick<State, "entities">) => readonly Foo[];
33
+ }, Store>;
34
+ export declare const missingHydrate: import("./implementation.js").Implementation<State, {
35
+ bump: (s: Pick<State, "count">, args: {
36
+ readonly by: number;
37
+ }) => Pick<State, "count">;
38
+ listFoos: (s: Pick<State, "entities">) => readonly Foo[];
39
+ }, Store>;
40
+ export declare const extraHydrate: import("./implementation.js").Implementation<State, {
41
+ bump: (s: Pick<State, "count">, args: {
42
+ readonly by: number;
43
+ }) => Pick<State, "count">;
44
+ listFoos: (s: Pick<State, "entities">) => readonly Foo[];
45
+ }, Store>;
46
+ interface Logger {
47
+ log: (msg: string) => void;
48
+ next: () => Promise<number>;
49
+ }
50
+ declare const record: (s: Pick<State, "count">, args: {
51
+ readonly by: number;
52
+ readonly logger: Logger;
53
+ }) => Pick<State, "count">;
54
+ export declare const recOk: SpecCases<State, typeof record>;
55
+ export declare const recBadArgsService: SpecCases<State, typeof record>;
56
+ export declare const recMissingArg: SpecCases<State, typeof record>;
57
+ export declare const recVoidResponse: SpecCases<State, typeof record>;
58
+ export declare const recWrongResponse: SpecCases<State, typeof record>;
59
+ export declare const recThrows: SpecCases<State, typeof record>;
60
+ export declare const recBoth: SpecCases<State, typeof record>;
61
+ declare const reset: (s: Pick<State, "count">) => Pick<State, "count">;
62
+ export declare const resetOk: CaseModule<State, typeof reset>;
63
+ declare const saveAsync: (s: Pick<State, "count">, a: {
64
+ readonly id: number;
65
+ }) => Promise<Pick<State, "count">>;
66
+ export declare const asyncOk: CaseModule<State, typeof saveAsync>;
67
+ export declare const arrOk: CaseModule<State, typeof listFoos>;
68
+ declare const idsOf: (s: State) => ReadonlySet<number>;
69
+ export declare const setOk: CaseModule<State, typeof idsOf>;
70
+ declare const total: (s: State) => number;
71
+ export declare const scalarOk: CaseModule<State, typeof total>;
72
+ export declare const wrongTransition: SpecCases<State, typeof reset>;
73
+ export declare const wrongDerivation: SpecDerivations<typeof total>;
74
+ export declare const mispair: CaseModule<State, typeof bump>;
75
+ declare const count: (s: State) => number;
76
+ export declare const residualGap: CaseModule<State, typeof count>;
77
+ export declare const missingComputedPlugin: import("./implementation.js").Implementation<State, {
78
+ bump: (s: Pick<State, "count">, args: {
79
+ readonly by: number;
80
+ }) => Pick<State, "count">;
81
+ listFoos: (s: Pick<State, "entities">) => readonly Foo[];
82
+ }, Store>;
83
+ export declare const noComputedOk: import("./implementation.js").Implementation<State, {
84
+ bump: (s: Pick<State, "count">, args: {
85
+ readonly by: number;
86
+ }) => Pick<State, "count">;
87
+ }, Store>;
88
+ export declare const servicesOk: import("./implementation.js").Implementation<State, {
89
+ record: (s: Pick<State, "count">, args: {
90
+ readonly by: number;
91
+ readonly logger: Logger;
92
+ }) => Pick<State, "count">;
93
+ }, Store>;
94
+ export declare const missingServices: import("./implementation.js").Implementation<State, {
95
+ record: (s: Pick<State, "count">, args: {
96
+ readonly by: number;
97
+ readonly logger: Logger;
98
+ }) => Pick<State, "count">;
99
+ }, Store>;
100
+ export declare const incompleteServices: import("./implementation.js").Implementation<State, {
101
+ record: (s: Pick<State, "count">, args: {
102
+ readonly by: number;
103
+ readonly logger: Logger;
104
+ }) => Pick<State, "count">;
105
+ }, Store>;
106
+ export declare const extraServices: import("./implementation.js").Implementation<State, {
107
+ bump: (s: Pick<State, "count">, args: {
108
+ readonly by: number;
109
+ }) => Pick<State, "count">;
110
+ }, Store>;
111
+ import type { StateMatches } from "./state-matches.js";
112
+ type Assert<T extends true> = T;
113
+ type Resources = {
114
+ readonly count: unknown;
115
+ };
116
+ type Components = {
117
+ readonly name: unknown;
118
+ };
119
+ export type _stateOk = Assert<StateMatches<State, Resources, Components>>;
120
+ export type _badSingleton = Assert<StateMatches<State, {}, Components>>;
121
+ export type _badEntity = Assert<StateMatches<State, Resources, {}>>;
122
+ export {};
@@ -0,0 +1,96 @@
1
+ import { spec as specOf } from "./spec.js";
2
+ import { implementation } from "./implementation.js";
3
+ // A transition (2 params) and an entity-list derivation (returns `readonly Foo[]`, so
4
+ // its ecs computed emits ids → it MUST be hydrated).
5
+ const bump = (s, args) => ({ count: s.count + args.by });
6
+ const listFoos = (s) => [...s.entities.values()];
7
+ const fns = { bump, listFoos };
8
+ // A concrete projection whose `toData` returns the entity value `Foo` — that return
9
+ // type is what tells the guard `listFoos` is an entity-list derivation.
10
+ const projection = {
11
+ fromState: (_store, _s) => { },
12
+ toState: (_store) => ({ entities: new Map(), count: 0 }),
13
+ toData: (_store, _e) => ({ name: "" }),
14
+ };
15
+ const bumpCases = {
16
+ cases: [{ name: "bumps", before: {}, args: { by: 1 }, after: { count: 1 } }],
17
+ };
18
+ const listFoosCases = {
19
+ cases: [{ name: "lists", input: { entities: new Map() }, value: [] }],
20
+ };
21
+ // POSITIVE — every fn has a case, and `hydrate` is exactly the entity-list derivation.
22
+ export const ok = implementation(specOf({ state, fns, cases: { bump: bumpCases, listFoos: listFoosCases } }), { plugin, computedPlugin, projection, hydrate: ["listFoos"] });
23
+ // NEGATIVE (cases coverage) — a conformed fn with no case module.
24
+ // @ts-expect-error — `cases` is missing `listFoos`
25
+ export const missingCase = implementation(specOf({ state, fns, cases: { bump: bumpCases } }), { plugin, computedPlugin, projection, hydrate: ["listFoos"] });
26
+ // NEGATIVE (cases coverage) — a case module for a name that is not a conformed fn.
27
+ // @ts-expect-error — `cases` has `bogus`, which is not in `fns`
28
+ export const extraCase = implementation(specOf({ state, fns, cases: { bump: bumpCases, listFoos: listFoosCases, bogus: bumpCases } }), { plugin, computedPlugin, projection, hydrate: ["listFoos"] });
29
+ // NEGATIVE (hydrate exactness) — an entity-list derivation left out of `hydrate`.
30
+ // @ts-expect-error — `hydrate` is missing the entity-list derivation `listFoos`
31
+ export const missingHydrate = implementation(specOf({ state, fns, cases: { bump: bumpCases, listFoos: listFoosCases } }), { plugin, computedPlugin, projection, hydrate: [] });
32
+ // NEGATIVE (hydrate exactness) — a non-entity-list fn listed in `hydrate`.
33
+ // @ts-expect-error — `hydrate` lists `bump`, which does not need hydration
34
+ export const extraHydrate = implementation(specOf({ state, fns, cases: { bump: bumpCases, listFoos: listFoosCases } }), { plugin, computedPlugin, projection, hydrate: ["listFoos", "bump"] });
35
+ const record = (s, args) => ({
36
+ count: s.count + args.by + (args.logger.log("x"), 0),
37
+ });
38
+ // POSITIVE — data-only `args` (the `logger` service is stripped), a scheduled return for
39
+ // the value method `next`, and the void `log` asserted in `effects`.
40
+ export const recOk = {
41
+ cases: [{ name: "ok", before: {}, args: { by: 1 }, responses: { logger: { next: [5] } }, after: { count: 6 }, effects: { logger: [["log", "x"]] } }],
42
+ };
43
+ // NEGATIVE — a live service instance in `args` (args are DATA only).
44
+ // @ts-expect-error — `logger` is a service; it must not appear in `args`
45
+ export const recBadArgsService = { cases: [{ name: "x", before: {}, args: { by: 1, logger: {} }, after: { count: 1 } }] };
46
+ // NEGATIVE — a required data arg omitted.
47
+ // @ts-expect-error — `args` is missing the required `by`
48
+ export const recMissingArg = { cases: [{ name: "x", before: {}, args: {}, after: { count: 1 } }] };
49
+ // NEGATIVE — scheduling a response for a `void` method (only value methods take one).
50
+ // @ts-expect-error — `log` is void; it cannot be scheduled in `responses`
51
+ export const recVoidResponse = { cases: [{ name: "x", before: {}, args: { by: 1 }, responses: { logger: { log: ["y"] } }, after: { count: 1 } }] };
52
+ // NEGATIVE — a scheduled response of the wrong payload type (`next` yields `number`).
53
+ // @ts-expect-error — `next` yields `number`, not `string`
54
+ export const recWrongResponse = { cases: [{ name: "x", before: {}, args: { by: 1 }, responses: { logger: { next: ["nope"] } }, after: { count: 1 } }] };
55
+ // POSITIVE — a `throws` case (no `after`/`effects`/`responses`).
56
+ export const recThrows = { cases: [{ name: "x", before: {}, args: { by: 1 }, throws: "boom" }] };
57
+ // NEGATIVE — `after` and `throws` are mutually exclusive.
58
+ // @ts-expect-error — a case is either an `after` expectation or a `throws`, never both
59
+ export const recBoth = { cases: [{ name: "x", before: {}, args: { by: 1 }, after: { count: 1 }, throws: true }] };
60
+ export const resetOk = resetCases;
61
+ export const asyncOk = asyncCases;
62
+ // POSITIVE — derivations returning an array (also a hydration key), a set, and a scalar.
63
+ export const arrOk = listFoosCases;
64
+ export const setOk = idsCases;
65
+ export const scalarOk = totalCases;
66
+ // NEGATIVE — wrong-arm annotation, caught by the envelope's content (not its arity).
67
+ // @ts-expect-error — a transition envelope cannot hold derivation-shaped `{ input, value }` cases
68
+ export const wrongTransition = { cases: [{ name: "x", input: { count: 0 }, value: 0 }] };
69
+ // @ts-expect-error — a derivation envelope cannot hold transition-shaped `{ before, after }` cases
70
+ export const wrongDerivation = { cases: [{ name: "x", before: {}, after: { count: 1 } }] };
71
+ // NEGATIVE — mis-pairing an array-derivation module into a transition fn's slot, caught
72
+ // by the `ReturnType`/args mismatch inside the envelope generics.
73
+ // @ts-expect-error — a derivation module for `listFoos` is not a valid case module for the transition `bump`
74
+ export const mispair = listFoosCases;
75
+ export const residualGap = resetCases;
76
+ // ── `computedPlugin` requiredness (needed iff the manifest has a derivation) ─────────
77
+ // `fns` above has `listFoos` (a derivation), so `ok` (which includes `computedPlugin`)
78
+ // is the POSITIVE. NEGATIVE — omit it with a derivation present:
79
+ // @ts-expect-error — `computedPlugin` is required because `listFoos` is a derivation
80
+ export const missingComputedPlugin = implementation(specOf({ state, fns, cases: { bump: bumpCases, listFoos: listFoosCases } }), { plugin, projection, hydrate: ["listFoos"] });
81
+ // POSITIVE — a transition-only feature may omit `computedPlugin` (no derivations).
82
+ const txFns = { bump };
83
+ export const noComputedOk = implementation(specOf({ state, fns: txFns, cases: { bump: txCases } }), { plugin, projection });
84
+ const svcFns = { record };
85
+ // POSITIVE — the injected service `logger` has a template.
86
+ export const servicesOk = implementation(specOf({ state, fns: svcFns, services: { logger: () => logger }, cases: { record: recordCases } }), { plugin, projection });
87
+ // NEGATIVE — a fn injects `logger`, but `services` is omitted (today: a runtime crash).
88
+ // @ts-expect-error — `services` is required because `record` injects `logger`
89
+ export const missingServices = implementation(specOf({ state, fns: svcFns, cases: { record: recordCases } }), { plugin, projection });
90
+ // NEGATIVE — `services` omits the required `logger` template.
91
+ // @ts-expect-error — `services` is missing the `logger` template
92
+ export const incompleteServices = implementation(specOf({ state, fns: svcFns, services: {}, cases: { record: recordCases } }), { plugin, projection });
93
+ // NEGATIVE — a service-free feature must not carry dead templates.
94
+ // @ts-expect-error — `bump` injects no service, so `services` must be absent
95
+ export const extraServices = implementation(specOf({ state, fns: txFns, services: { logger: () => logger }, cases: { bump: txCases } }), { plugin, projection });
96
+ //# sourceMappingURL=manifest.type-test.js.map