@specverse/engines 4.1.21 → 4.1.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 (76) hide show
  1. package/dist/inference/ui-contracts/index.d.ts +38 -0
  2. package/dist/inference/ui-contracts/index.d.ts.map +1 -0
  3. package/dist/inference/ui-contracts/index.js +212 -0
  4. package/dist/inference/ui-contracts/index.js.map +1 -0
  5. package/dist/inference/ui-contracts/rules/_shared.d.ts +32 -0
  6. package/dist/inference/ui-contracts/rules/_shared.d.ts.map +1 -0
  7. package/dist/inference/ui-contracts/rules/_shared.js +103 -0
  8. package/dist/inference/ui-contracts/rules/_shared.js.map +1 -0
  9. package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts +21 -0
  10. package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts.map +1 -0
  11. package/dist/inference/ui-contracts/rules/action-buttons-present.js +62 -0
  12. package/dist/inference/ui-contracts/rules/action-buttons-present.js.map +1 -0
  13. package/dist/inference/ui-contracts/rules/create-reflects-in-list.d.ts +22 -0
  14. package/dist/inference/ui-contracts/rules/create-reflects-in-list.d.ts.map +1 -0
  15. package/dist/inference/ui-contracts/rules/create-reflects-in-list.js +48 -0
  16. package/dist/inference/ui-contracts/rules/create-reflects-in-list.js.map +1 -0
  17. package/dist/inference/ui-contracts/rules/delete-reflects-in-list.d.ts +22 -0
  18. package/dist/inference/ui-contracts/rules/delete-reflects-in-list.d.ts.map +1 -0
  19. package/dist/inference/ui-contracts/rules/delete-reflects-in-list.js +50 -0
  20. package/dist/inference/ui-contracts/rules/delete-reflects-in-list.js.map +1 -0
  21. package/dist/inference/ui-contracts/rules/detail-view-renders.d.ts +24 -0
  22. package/dist/inference/ui-contracts/rules/detail-view-renders.d.ts.map +1 -0
  23. package/dist/inference/ui-contracts/rules/detail-view-renders.js +34 -0
  24. package/dist/inference/ui-contracts/rules/detail-view-renders.js.map +1 -0
  25. package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.d.ts +21 -0
  26. package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.d.ts.map +1 -0
  27. package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.js +53 -0
  28. package/dist/inference/ui-contracts/rules/evolve-reflects-in-list.js.map +1 -0
  29. package/dist/inference/ui-contracts/rules/form-shows-required-indicators.d.ts +15 -0
  30. package/dist/inference/ui-contracts/rules/form-shows-required-indicators.d.ts.map +1 -0
  31. package/dist/inference/ui-contracts/rules/form-shows-required-indicators.js +38 -0
  32. package/dist/inference/ui-contracts/rules/form-shows-required-indicators.js.map +1 -0
  33. package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.d.ts +17 -0
  34. package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.d.ts.map +1 -0
  35. package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.js +39 -0
  36. package/dist/inference/ui-contracts/rules/hasmany-shows-children-in-detail.js.map +1 -0
  37. package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts +25 -0
  38. package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts.map +1 -0
  39. package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js +66 -0
  40. package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js.map +1 -0
  41. package/dist/inference/ui-contracts/rules/list-shows-business-columns.d.ts +17 -0
  42. package/dist/inference/ui-contracts/rules/list-shows-business-columns.d.ts.map +1 -0
  43. package/dist/inference/ui-contracts/rules/list-shows-business-columns.js +39 -0
  44. package/dist/inference/ui-contracts/rules/list-shows-business-columns.js.map +1 -0
  45. package/dist/inference/ui-contracts/rules/list-view-renders.d.ts +19 -0
  46. package/dist/inference/ui-contracts/rules/list-view-renders.d.ts.map +1 -0
  47. package/dist/inference/ui-contracts/rules/list-view-renders.js +29 -0
  48. package/dist/inference/ui-contracts/rules/list-view-renders.js.map +1 -0
  49. package/dist/inference/ui-contracts/rules/nav-has-model-entries.d.ts +20 -0
  50. package/dist/inference/ui-contracts/rules/nav-has-model-entries.d.ts.map +1 -0
  51. package/dist/inference/ui-contracts/rules/nav-has-model-entries.js +29 -0
  52. package/dist/inference/ui-contracts/rules/nav-has-model-entries.js.map +1 -0
  53. package/dist/inference/ui-contracts/test-case-types.d.ts +126 -0
  54. package/dist/inference/ui-contracts/test-case-types.d.ts.map +1 -0
  55. package/dist/inference/ui-contracts/test-case-types.js +14 -0
  56. package/dist/inference/ui-contracts/test-case-types.js.map +1 -0
  57. package/dist/inference/ui-contracts/translator.d.ts +17 -0
  58. package/dist/inference/ui-contracts/translator.d.ts.map +1 -0
  59. package/dist/inference/ui-contracts/translator.js +127 -0
  60. package/dist/inference/ui-contracts/translator.js.map +1 -0
  61. package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +37 -2
  62. package/dist/libs/instance-factories/applications/templates/react/runtime-app-tsx-generator.js +41 -4
  63. package/dist/libs/instance-factories/applications/templates/react/use-api-hooks-generator.js +27 -7
  64. package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js +10 -0
  65. package/dist/libs/instance-factories/views/templates/react/components-generator.js +34 -23
  66. package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +51 -81
  67. package/dist/realize/index.d.ts.map +1 -1
  68. package/dist/realize/index.js +204 -0
  69. package/dist/realize/index.js.map +1 -1
  70. package/libs/instance-factories/applications/templates/react/api-client-generator.ts +37 -2
  71. package/libs/instance-factories/applications/templates/react/runtime-app-tsx-generator.ts +41 -4
  72. package/libs/instance-factories/applications/templates/react/use-api-hooks-generator.ts +27 -7
  73. package/libs/instance-factories/scaffolding/templates/generic/package-json-generator.ts +13 -0
  74. package/libs/instance-factories/views/templates/react/components-generator.ts +34 -23
  75. package/libs/instance-factories/views/templates/react/hooks-generator.ts +72 -88
  76. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ /**
2
+ * TestCase — declarative description of a UI contract test.
3
+ *
4
+ * Rules (pure functions) produce TestCase[]. The translator renders
5
+ * each TestCase into a Playwright `.spec.ts` file that imports helpers
6
+ * from `@specverse/runtime/test-harness`. Neither rules nor test cases
7
+ * know anything about React Query, cache invalidation, or any
8
+ * implementation detail — they speak only spec language.
9
+ *
10
+ * v1 steps cover navigation + assertion. Later phases add create /
11
+ * delete / evolve / expect-absent for state-sync rules.
12
+ */
13
+ export type TestStep = {
14
+ action: 'bootRuntime';
15
+ } | {
16
+ action: 'expectNavHasModel';
17
+ modelName: string;
18
+ } | {
19
+ action: 'navigateToModel';
20
+ modelName: string;
21
+ } | {
22
+ action: 'expectViewRenders';
23
+ } | {
24
+ action: 'expectListColumn';
25
+ modelName: string;
26
+ attributeName: string;
27
+ } | {
28
+ action: 'expectFormInput';
29
+ modelName: string;
30
+ attributeName: string;
31
+ required?: boolean;
32
+ } | {
33
+ action: 'expectLifecycleState';
34
+ modelName: string;
35
+ stateName: string;
36
+ } | {
37
+ action: 'expectRelationshipSection';
38
+ modelName: string;
39
+ relationshipName: string;
40
+ } | {
41
+ action: 'expectActionButton';
42
+ modelName: string;
43
+ actionLabel: string;
44
+ } | {
45
+ action: 'createEntity';
46
+ modelName: string;
47
+ data: Record<string, unknown>;
48
+ bindAs?: string;
49
+ } | {
50
+ action: 'deleteEntity';
51
+ modelName: string;
52
+ idExpr: string;
53
+ } | {
54
+ action: 'evolveEntity';
55
+ modelName: string;
56
+ idExpr: string;
57
+ targetState: string;
58
+ } | {
59
+ action: 'expectEntityInList';
60
+ modelName: string;
61
+ displayValue: string;
62
+ } | {
63
+ action: 'expectEntityAbsent';
64
+ modelName: string;
65
+ displayValue: string;
66
+ };
67
+ export interface TestCase {
68
+ /** The rule that produced this test case. Used in test names for attribution. */
69
+ ruleId: string;
70
+ /** The spec element the case applies to (e.g. 'Poll', 'PollController.delete'). */
71
+ specElement: string;
72
+ /** Full test name — shown by Playwright reporter. */
73
+ name: string;
74
+ /** Ordered sequence of actions/assertions. */
75
+ steps: TestStep[];
76
+ }
77
+ /**
78
+ * A rule is a pure function from a normalized spec to a list of test
79
+ * cases. Rules never touch the filesystem, the network, or any mutable
80
+ * state — they just describe what should be tested.
81
+ */
82
+ export type UiContractRule = (spec: NormalizedSpec) => TestCase[];
83
+ /**
84
+ * Minimal normalized spec shape the rules operate on. The realize
85
+ * pipeline converts raw spec output into this shape before running
86
+ * rules. Kept deliberately small — rules grow their own helpers as
87
+ * they need richer data.
88
+ */
89
+ export interface NormalizedSpec {
90
+ models: ModelInfo[];
91
+ controllers?: ControllerInfo[];
92
+ services?: ServiceInfo[];
93
+ views?: ViewInfo[];
94
+ }
95
+ export interface ModelInfo {
96
+ name: string;
97
+ attributes?: AttributeInfo[];
98
+ lifecycles?: Record<string, unknown>;
99
+ relationships?: RelationshipInfo[];
100
+ }
101
+ export interface AttributeInfo {
102
+ name: string;
103
+ type?: string;
104
+ required?: boolean;
105
+ category?: 'business' | 'metadata' | 'relationship';
106
+ }
107
+ export interface RelationshipInfo {
108
+ name: string;
109
+ type: string;
110
+ target?: string;
111
+ }
112
+ export interface ControllerInfo {
113
+ name: string;
114
+ model?: string;
115
+ cured?: Record<string, unknown>;
116
+ }
117
+ export interface ServiceInfo {
118
+ name: string;
119
+ operations?: Record<string, unknown>;
120
+ }
121
+ export interface ViewInfo {
122
+ name: string;
123
+ type: string;
124
+ model?: string;
125
+ }
126
+ //# sourceMappingURL=test-case-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-case-types.d.ts","sourceRoot":"","sources":["../../../src/inference/ui-contracts/test-case-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,QAAQ,GAChB;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,GACzB;IAAE,MAAM,EAAE,mBAAmB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,MAAM,EAAE,mBAAmB,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3F;IAAE,MAAM,EAAE,sBAAsB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,MAAM,EAAE,2BAA2B,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACpF;IAAE,MAAM,EAAE,oBAAoB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAGxE;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7F;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAClF;IAAE,MAAM,EAAE,oBAAoB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,MAAM,EAAE,oBAAoB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E,MAAM,WAAW,QAAQ;IACvB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,cAAc,KAAK,QAAQ,EAAE,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,cAAc,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * TestCase — declarative description of a UI contract test.
3
+ *
4
+ * Rules (pure functions) produce TestCase[]. The translator renders
5
+ * each TestCase into a Playwright `.spec.ts` file that imports helpers
6
+ * from `@specverse/runtime/test-harness`. Neither rules nor test cases
7
+ * know anything about React Query, cache invalidation, or any
8
+ * implementation detail — they speak only spec language.
9
+ *
10
+ * v1 steps cover navigation + assertion. Later phases add create /
11
+ * delete / evolve / expect-absent for state-sync rules.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=test-case-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-case-types.js","sourceRoot":"","sources":["../../../src/inference/ui-contracts/test-case-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * TestCase → Playwright .spec.ts translator.
3
+ *
4
+ * Renders a list of TestCase objects into a single Playwright spec
5
+ * file. The output file imports from `@specverse/runtime/test-harness`
6
+ * and nothing else from the runtime — all logic lives in the harness
7
+ * helpers. The translator is intentionally dumb: each step maps to a
8
+ * helper call, nothing more.
9
+ */
10
+ import type { TestCase } from './test-case-types.js';
11
+ /**
12
+ * Render a full `.spec.ts` file for one or more test cases sharing
13
+ * the same rule. One file per rule keeps failures easy to attribute
14
+ * and lets `npx playwright test` filter by rule ID via file glob.
15
+ */
16
+ export declare function renderSpecFile(ruleId: string, testCases: TestCase[]): string;
17
+ //# sourceMappingURL=translator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../../src/inference/ui-contracts/translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,sBAAsB,CAAC;AAwF/D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,CA6B5E"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * TestCase → Playwright .spec.ts translator.
3
+ *
4
+ * Renders a list of TestCase objects into a single Playwright spec
5
+ * file. The output file imports from `@specverse/runtime/test-harness`
6
+ * and nothing else from the runtime — all logic lives in the harness
7
+ * helpers. The translator is intentionally dumb: each step maps to a
8
+ * helper call, nothing more.
9
+ */
10
+ /**
11
+ * Render a single step as a line of test body code.
12
+ */
13
+ function renderStep(step) {
14
+ switch (step.action) {
15
+ case 'bootRuntime':
16
+ // Assign `page` once per test body. Multiple bootRuntime steps
17
+ // inside one test aren't supported and would shadow the binding.
18
+ return ` const { page } = await bootRuntime(browser);`;
19
+ case 'expectNavHasModel':
20
+ return ` await expectNavHasModel(page, ${JSON.stringify(step.modelName)});`;
21
+ case 'navigateToModel':
22
+ return ` await navigateToModel(page, ${JSON.stringify(step.modelName)});`;
23
+ case 'expectViewRenders':
24
+ return ` await expectViewRenders(page);`;
25
+ case 'expectListColumn':
26
+ return ` await expectListColumn(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.attributeName)});`;
27
+ case 'expectFormInput': {
28
+ const opts = step.required ? `, { required: true }` : ``;
29
+ return ` await expectFormInput(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.attributeName)}${opts});`;
30
+ }
31
+ case 'expectLifecycleState':
32
+ return ` await expectLifecycleState(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.stateName)});`;
33
+ case 'expectRelationshipSection':
34
+ return ` await expectRelationshipSection(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.relationshipName)});`;
35
+ case 'expectActionButton':
36
+ return ` await expectActionButton(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.actionLabel)});`;
37
+ case 'createEntity': {
38
+ const varName = step.bindAs || '_entityId';
39
+ const dataLit = JSON.stringify(step.data);
40
+ return ` const ${varName} = await createEntity(page, ${JSON.stringify(step.modelName)}, ${dataLit});`;
41
+ }
42
+ case 'deleteEntity':
43
+ return ` await deleteEntity(page, ${JSON.stringify(step.modelName)}, ${step.idExpr});`;
44
+ case 'evolveEntity':
45
+ return ` await evolveEntity(page, ${JSON.stringify(step.modelName)}, ${step.idExpr}, ${JSON.stringify(step.targetState)});`;
46
+ case 'expectEntityInList':
47
+ return ` await expectEntityInList(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.displayValue)});`;
48
+ case 'expectEntityAbsent':
49
+ return ` await expectEntityAbsent(page, ${JSON.stringify(step.modelName)}, ${JSON.stringify(step.displayValue)});`;
50
+ default: {
51
+ const _exhaustive = step;
52
+ throw new Error(`translator: unknown step action: ${JSON.stringify(_exhaustive)}`);
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Collect the set of harness helpers a test case depends on so the
58
+ * import list only contains what's actually used.
59
+ */
60
+ function helpersFor(step) {
61
+ switch (step.action) {
62
+ case 'bootRuntime':
63
+ return ['bootRuntime'];
64
+ case 'expectNavHasModel':
65
+ return ['expectNavHasModel'];
66
+ case 'navigateToModel':
67
+ return ['navigateToModel'];
68
+ case 'expectViewRenders':
69
+ return ['expectViewRenders'];
70
+ case 'expectListColumn':
71
+ return ['expectListColumn'];
72
+ case 'expectFormInput':
73
+ return ['expectFormInput'];
74
+ case 'expectLifecycleState':
75
+ return ['expectLifecycleState'];
76
+ case 'expectRelationshipSection':
77
+ return ['expectRelationshipSection'];
78
+ case 'expectActionButton':
79
+ return ['expectActionButton'];
80
+ case 'createEntity':
81
+ return ['createEntity'];
82
+ case 'deleteEntity':
83
+ return ['deleteEntity'];
84
+ case 'evolveEntity':
85
+ return ['evolveEntity'];
86
+ case 'expectEntityInList':
87
+ return ['expectEntityInList'];
88
+ case 'expectEntityAbsent':
89
+ return ['expectEntityAbsent'];
90
+ default:
91
+ return [];
92
+ }
93
+ }
94
+ /**
95
+ * Render a full `.spec.ts` file for one or more test cases sharing
96
+ * the same rule. One file per rule keeps failures easy to attribute
97
+ * and lets `npx playwright test` filter by rule ID via file glob.
98
+ */
99
+ export function renderSpecFile(ruleId, testCases) {
100
+ const helpers = new Set();
101
+ for (const tc of testCases) {
102
+ for (const step of tc.steps) {
103
+ for (const h of helpersFor(step))
104
+ helpers.add(h);
105
+ }
106
+ }
107
+ const importList = [...helpers].sort().join(', ');
108
+ const header = `/**
109
+ * Auto-generated UI contract test file.
110
+ * Rule: ${ruleId}
111
+ * DO NOT EDIT — regenerated every \`spv realize\` run. Hand edits are
112
+ * lost on the next regenerate. Change the rule or the spec instead.
113
+ */
114
+
115
+ import { test } from '@playwright/test';
116
+ import { ${importList} } from '@specverse/runtime/test-harness';
117
+ `;
118
+ const body = testCases.map(tc => {
119
+ const stepLines = tc.steps.map(renderStep).join('\n');
120
+ return `
121
+ test(${JSON.stringify(tc.name)}, async ({ browser }) => {
122
+ ${stepLines}
123
+ });`;
124
+ }).join('\n');
125
+ return header + body + '\n';
126
+ }
127
+ //# sourceMappingURL=translator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.js","sourceRoot":"","sources":["../../../src/inference/ui-contracts/translator.ts"],"names":[],"mappings":"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"}
@@ -43,9 +43,12 @@ export const WS_URL = apiUrl
43
43
  : \`\${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}/\${window.location.host}/ws\`;
44
44
 
45
45
  /**
46
- * Generic API request helper
46
+ * Generic API request helper \u2014 exported so view components can
47
+ * use it as a drop-in replacement for raw fetch calls. Always
48
+ * routes through this module so the conformance check
49
+ * \`no-raw-fetch-in-views\` stays green.
47
50
  */
48
- async function apiRequest<T = any>(
51
+ export async function apiRequest<T = any>(
49
52
  method: string,
50
53
  path: string,
51
54
  body: any = null
@@ -304,6 +307,22 @@ export async function executeOperation(
304
307
  return apiRequest<ApiResponse>(method, path, Object.keys(params).length > 0 ? params : null);
305
308
  }
306
309
 
310
+ /**
311
+ * Execute a service operation
312
+ * Services are RPC-style under /services/{serviceName}/{operationName}
313
+ */
314
+ export async function executeServiceOperation(
315
+ serviceName: string,
316
+ operationName: string,
317
+ params: Record<string, any>
318
+ ): Promise<ApiResponse> {
319
+ return apiRequest<ApiResponse>(
320
+ 'POST',
321
+ \`/services/\${serviceName}/\${operationName}\`,
322
+ params
323
+ );
324
+ }
325
+
307
326
  /**
308
327
  * Transition entity lifecycle state
309
328
  */
@@ -395,6 +414,22 @@ export async function executeOperation(
395
414
  );
396
415
  }
397
416
 
417
+ /**
418
+ * Execute a service operation
419
+ * Services are RPC-style under /services/{serviceName}/{operationName}
420
+ */
421
+ export async function executeServiceOperation(
422
+ serviceName: string,
423
+ operationName: string,
424
+ params: Record<string, any>
425
+ ): Promise<ApiResponse> {
426
+ return apiRequest<ApiResponse>(
427
+ 'POST',
428
+ \`/services/\${serviceName}/\${operationName}\`,
429
+ params
430
+ );
431
+ }
432
+
398
433
  /**
399
434
  * Transition entity lifecycle state
400
435
  */
@@ -1,10 +1,10 @@
1
1
  function generateRuntimeAppTsx(context) {
2
2
  const { spec } = context;
3
3
  const appName = spec?.metadata?.name || spec?.name || "SpecVerse App";
4
- return `import { useState, useEffect, useMemo } from 'react';
5
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ return `import { useState, useEffect, useMemo, useCallback } from 'react';
5
+ import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query';
6
6
  import yaml from 'js-yaml';
7
- import { RuntimeViewProvider } from '@specverse/runtime/views/react';
7
+ import { RuntimeViewProvider, useEntitySync } from '@specverse/runtime/views/react';
8
8
  import { DevShell } from '@specverse/runtime/views/react';
9
9
  import {
10
10
  useEntitiesQuery,
@@ -12,6 +12,7 @@ import {
12
12
  useExecuteOperationMutation,
13
13
  useTransitionStateMutation,
14
14
  } from './hooks/useApi';
15
+ import { listEntities, getRuntimeInfo } from './lib/apiClient';
15
16
  import devSpecRaw from './dev.specly?raw';
16
17
 
17
18
  // Parse YAML spec
@@ -64,6 +65,40 @@ function AppContent() {
64
65
  .catch(() => {});
65
66
  }, []);
66
67
 
68
+ // State-sync contract: WebSocket entity events \u2192 React Query cache
69
+ // invalidation. This is what guarantees "delete in one view is visible
70
+ // in every other view within one WS round-trip" \u2014 it runs at the App
71
+ // level so it's always on, regardless of which tab is active.
72
+ const queryClient = useQueryClient();
73
+ const invalidateEntities = useCallback((modelName: string) => {
74
+ queryClient.invalidateQueries({ queryKey: ['entities', modelName] });
75
+ }, [queryClient]);
76
+ useEntitySync({ invalidateEntities, apiBaseUrl: '/api' });
77
+
78
+ // Name resolver for OperationResultView. Kept in the host so runtime
79
+ // view components never issue raw fetch() calls \u2014 all HTTP goes
80
+ // through apiClient's canonical layer.
81
+ const resolveEntityNames = useCallback(async (ids: string[]): Promise<Record<string, string>> => {
82
+ const names: Record<string, string> = {};
83
+ try {
84
+ const info = await getRuntimeInfo();
85
+ const models = info?.models || [];
86
+ await Promise.all(models.map(async (model: string) => {
87
+ try {
88
+ const entities = await listEntities(model + 'Controller');
89
+ for (const entity of entities) {
90
+ const id = (entity as any)?.id;
91
+ const dataId = (entity as any)?.data?.id;
92
+ const display = (entity as any)?.name || (entity as any)?.title || (entity as any)?.label || id;
93
+ if (id && ids.includes(id)) names[id] = String(display);
94
+ if (dataId && ids.includes(dataId)) names[dataId] = String(display);
95
+ }
96
+ } catch { /* skip this model */ }
97
+ }));
98
+ } catch { /* ignore \u2014 caller falls back to raw IDs */ }
99
+ return names;
100
+ }, []);
101
+
67
102
  const runtimeValue = useMemo(() => ({
68
103
  useEntitiesQuery,
69
104
  useModelSchemaQuery,
@@ -74,7 +109,9 @@ function AppContent() {
74
109
  views: [],
75
110
  spec: appSpec,
76
111
  apiBaseUrl: '/api',
77
- }), [appSpec]);
112
+ invalidateEntities,
113
+ resolveEntityNames,
114
+ }), [appSpec, invalidateEntities, resolveEntityNames]);
78
115
 
79
116
  return (
80
117
  <RuntimeViewProvider value={runtimeValue}>
@@ -11,6 +11,7 @@ import {
11
11
  getModelSchema,
12
12
  listEntities,
13
13
  executeOperation,
14
+ executeServiceOperation,
14
15
  transitionState
15
16
  } from '../lib/apiClient';
16
17
  import type { ModelSchema, Entity, ApiResponse } from '../types/api';
@@ -49,9 +50,14 @@ export function useEntitiesQuery(controllerName: string | null, modelName: strin
49
50
  }
50
51
 
51
52
  /**
52
- * Mutation hook for executing operations
53
- * Matches RuntimeViewProviderValue.useExecuteOperationMutation contract:
54
- * { controllerName, operationName, data, entityId? }
53
+ * Mutation hook for executing operations \u2014 handles both controllers
54
+ * and services. Pass either \`controllerName\` (for CRUD or custom
55
+ * controller actions) or \`serviceName\` (for RPC-style service ops).
56
+ *
57
+ * Matches RuntimeViewProviderValue.useExecuteOperationMutation contract.
58
+ * Service operations invalidate the \`services\` query key; controller
59
+ * operations invalidate the corresponding model's \`entities\` key so
60
+ * all mounted views refetch automatically.
55
61
  */
56
62
  export function useExecuteOperationMutation() {
57
63
  const queryClient = useQueryClient();
@@ -59,12 +65,14 @@ export function useExecuteOperationMutation() {
59
65
  return useMutation({
60
66
  mutationFn: ({
61
67
  controllerName,
68
+ serviceName,
62
69
  operationName,
63
70
  data,
64
71
  params,
65
72
  entityId
66
73
  }: {
67
- controllerName: string;
74
+ controllerName?: string;
75
+ serviceName?: string;
68
76
  operationName: string;
69
77
  data?: Record<string, any>;
70
78
  params?: Record<string, any>;
@@ -73,11 +81,23 @@ export function useExecuteOperationMutation() {
73
81
  // Merge: accept both 'data' and 'params' for compatibility
74
82
  const mergedParams = { ...(data || params || {}) };
75
83
  if (entityId) mergedParams.id = entityId;
76
- return executeOperation(controllerName, operationName, mergedParams);
84
+ if (serviceName) {
85
+ return executeServiceOperation(serviceName, operationName, mergedParams);
86
+ }
87
+ if (controllerName) {
88
+ return executeOperation(controllerName, operationName, mergedParams);
89
+ }
90
+ throw new Error('useExecuteOperationMutation: either controllerName or serviceName is required');
77
91
  },
78
92
  onSuccess: (_data, variables) => {
79
- const modelName = variables.controllerName.replace(/Controller$/, '');
80
- queryClient.invalidateQueries({ queryKey: ['entities', modelName] });
93
+ if (variables.controllerName) {
94
+ const modelName = variables.controllerName.replace(/Controller$/, '');
95
+ queryClient.invalidateQueries({ queryKey: ['entities', modelName] });
96
+ }
97
+ // Service operations aren't tied to a single entity type; no
98
+ // cache key to invalidate here. If a service mutation affects
99
+ // entities, the backend should publish an event that
100
+ // useEntitySync picks up via WebSocket.
81
101
  }
82
102
  });
83
103
  }
@@ -29,6 +29,13 @@ function generatePackageJson(context) {
29
29
  "test": "npm test --workspaces",
30
30
  "test:backend": `npm test --workspace=${backendDir}`,
31
31
  "test:frontend": `npm test --workspace=${frontendDir}`,
32
+ // UI contract tests — auto-generated from spec inference rules.
33
+ // Assumes backend + frontend are already running (see docs/proposals/UI-CONTRACT-INFERENCE.md).
34
+ "test:contract": "playwright test --config=playwright.contract.config.ts",
35
+ // Implementation conformance — static grep-level checks that
36
+ // verify the runtime implementation has no escape hatches
37
+ // violating the design contract. Fast (millis), no browser.
38
+ "test:conformance": "node scripts/test-conformance.mjs",
32
39
  // Database management (backend)
33
40
  "db:setup": `npm run db:setup --workspace=${backendDir}`,
34
41
  "db:generate": `npm run db:generate --workspace=${backendDir}`,
@@ -39,6 +46,9 @@ function generatePackageJson(context) {
39
46
  "lint": "npm run lint --workspaces",
40
47
  "lint:fix": "npm run lint:fix --workspaces"
41
48
  };
49
+ pkg.devDependencies = {
50
+ "@playwright/test": "^1.40.0"
51
+ };
42
52
  } else {
43
53
  pkg.scripts = {
44
54
  ...aggregated.scripts,