@qontinui/shared-types 0.2.3 → 0.2.4

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.
@@ -74,3 +74,17 @@ export type { RecommendationReviewDecisionPayload } from "../generated/Recommend
74
74
  // pre-dates this generated re-export; both shapes are intentionally
75
75
  // identical. New consumers should prefer this generated import. ──
76
76
  export type { CanvasPanel as CanvasUpdatePanel } from "../generated/CanvasPanel";
77
+
78
+ // ── UI Bridge IPC envelope payloads carried on the `ui-bridge-request` and
79
+ // `ui-bridge-response` Tauri channels. Stage 1 of the ui-bridge-request
80
+ // envelope concretization (deferral note in commit ea5d9a61f). The Rust
81
+ // sources live in `qontinui-schemas/rust/src/app_events.rs`; the runner's
82
+ // React handler in `src/hooks/ui-bridge-events/types.ts` should pick the
83
+ // `requestId | type` fields off these to keep the envelope head in sync
84
+ // while leaving its rich set of optional payload fields hand-written.
85
+ // Stage 2 (per-payload tagged union of all 171 request_types) is deferred
86
+ // indefinitely — the existing AssertEqual<AllHandledTypes,
87
+ // UIBridgeRequestType> + per-hook never checks already enforce dispatch
88
+ // exhaustiveness from the right end of the pipeline. ──
89
+ export type { UiBridgeRequestEnvelope } from "../generated/UiBridgeRequestEnvelope";
90
+ export type { UiBridgeResponseEnvelope } from "../generated/UiBridgeResponseEnvelope";
@@ -6,6 +6,7 @@
6
6
  * adapter folds it into ui-bridge-auto's `WorkflowConfig`.
7
7
  */
8
8
 
9
+ import type { IRGroup } from "./group";
9
10
  import type { IRMetadata, IRProvenance } from "./primitives";
10
11
  import type { IRState } from "./state";
11
12
  import type { IRTransition } from "./transition";
@@ -100,6 +101,14 @@ export interface IRDocument {
100
101
  /** Transition declarations within this document. */
101
102
  transitions: IRTransition[];
102
103
 
104
+ /**
105
+ * Groups produced by workflow-criteria synthesis (not by IR state authoring).
106
+ * Synthesis appends to this list; the projection emits them after state-derived
107
+ * groups in legacy `groups[]` output. Authoring tools should treat this field
108
+ * as read-only.
109
+ */
110
+ synthesizedGroups?: IRGroup[];
111
+
103
112
  /** ID of the initial/starting state, if applicable. */
104
113
  initialState?: string;
105
114
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * IR-side group + assertion types.
3
+ *
4
+ * The IR's primary authoring surface is `states + transitions`, which the
5
+ * projection maps into legacy `groups[]` (one group per state). However, the
6
+ * runner's `workflow_generation::spec_synthesis` pipeline produces "free-form"
7
+ * groups from acceptance criteria — these are NOT tied to any state and
8
+ * therefore have no home in the state-derived projection path.
9
+ *
10
+ * `IRGroup` is the dedicated channel for those synthesized groups. The IR
11
+ * carries them in the optional `IRDocument.synthesizedGroups` field; the
12
+ * projection appends them after the state-derived groups, in declared order,
13
+ * to preserve byte-stable output.
14
+ *
15
+ * The shape mirrors the legacy `SpecGroup` / `SpecAssertion` (canonical
16
+ * definitions live in `@qontinui/ui-bridge/specs`), but is re-defined here to
17
+ * keep `qontinui-schemas/ts` free of any runtime dep on `@qontinui/ui-bridge/*`
18
+ * (per the schemas package's "zero ui-bridge deps" policy).
19
+ *
20
+ * Note on field shape: the IR-side `IRAssertion` keeps the legacy assertion's
21
+ * field types as loose as the synthesis path emits them. Strict consumers
22
+ * (e.g. the canonical `SpecAssertion` in `@qontinui/ui-bridge/specs`) can
23
+ * narrow as needed at the consumer boundary; the projection preserves the
24
+ * objects verbatim into the legacy output.
25
+ */
26
+
27
+ /**
28
+ * IR-side assertion target. Always `type: "search"` for synthesis-emitted
29
+ * assertions; the wider legacy schema also supports point/region targets but
30
+ * the IR doesn't express those today.
31
+ */
32
+ export interface IRAssertionTarget {
33
+ /** Always `"search"` for synthesis-emitted assertions. */
34
+ type: string;
35
+ /**
36
+ * Free-form criteria object. Kept as a loose record so synthesis can emit
37
+ * partially-populated criteria without tripping the schema.
38
+ */
39
+ criteria: Record<string, unknown>;
40
+ label: string;
41
+ }
42
+
43
+ /**
44
+ * IR-side assertion. Mirrors the legacy `SpecAssertion` shape.
45
+ */
46
+ export interface IRAssertion {
47
+ id: string;
48
+ description: string;
49
+ category: string;
50
+ severity: string;
51
+ assertionType: string;
52
+ target: IRAssertionTarget;
53
+ source: string;
54
+ reviewed: boolean;
55
+ enabled: boolean;
56
+ /**
57
+ * Optional human-readable precondition. Synthesis populates this from the
58
+ * acceptance criterion's verification hint.
59
+ */
60
+ precondition?: string;
61
+ }
62
+
63
+ /**
64
+ * IR-side group produced by workflow-criteria synthesis (i.e. NOT derived
65
+ * from `IRState` annotations). Mirrors the legacy `SpecGroup` shape one-to-one
66
+ * so the projection can pass it through to legacy `groups[]` without loss.
67
+ */
68
+ export interface IRGroup {
69
+ id: string;
70
+ name: string;
71
+ description: string;
72
+ category: string;
73
+ assertions: IRAssertion[];
74
+ /**
75
+ * Provenance of the group itself (typically `"ai-generated"` for synthesis
76
+ * output, but kept open for future authoring channels).
77
+ */
78
+ source?: string;
79
+ /**
80
+ * Free-form tags. Synthesis emits e.g. `["workflow-generated",
81
+ * "acceptance-criteria"]` so downstream tooling can filter.
82
+ */
83
+ tags?: string[];
84
+ }
@@ -18,6 +18,7 @@ export * from "./primitives";
18
18
  export * from "./element-criteria";
19
19
  export * from "./state";
20
20
  export * from "./transition";
21
+ export * from "./group";
21
22
  export * from "./document";
22
23
  export * from "./adapter";
23
24
  export * from "./projection";
@@ -24,6 +24,10 @@
24
24
  "transitions": {
25
25
  "type": "array",
26
26
  "items": { "$ref": "#/$defs/IRTransition" }
27
+ },
28
+ "synthesizedGroups": {
29
+ "type": "array",
30
+ "items": { "$ref": "#/$defs/IRGroup" }
27
31
  }
28
32
  },
29
33
  "$defs": {
@@ -230,6 +234,60 @@
230
234
  "provenance": { "$ref": "#/$defs/IRProvenance" },
231
235
  "crossRefs": { "type": "array", "items": { "$ref": "#/$defs/IRCrossRef" } }
232
236
  }
237
+ },
238
+ "IRAssertionTarget": {
239
+ "type": "object",
240
+ "required": ["type", "criteria", "label"],
241
+ "additionalProperties": false,
242
+ "properties": {
243
+ "type": { "type": "string" },
244
+ "criteria": { "type": "object", "additionalProperties": true },
245
+ "label": { "type": "string" }
246
+ }
247
+ },
248
+ "IRAssertion": {
249
+ "type": "object",
250
+ "required": [
251
+ "id",
252
+ "description",
253
+ "category",
254
+ "severity",
255
+ "assertionType",
256
+ "target",
257
+ "source",
258
+ "reviewed",
259
+ "enabled"
260
+ ],
261
+ "additionalProperties": false,
262
+ "properties": {
263
+ "id": { "type": "string" },
264
+ "description": { "type": "string" },
265
+ "category": { "type": "string" },
266
+ "severity": { "type": "string" },
267
+ "assertionType": { "type": "string" },
268
+ "target": { "$ref": "#/$defs/IRAssertionTarget" },
269
+ "source": { "type": "string" },
270
+ "reviewed": { "type": "boolean" },
271
+ "enabled": { "type": "boolean" },
272
+ "precondition": { "type": "string" }
273
+ }
274
+ },
275
+ "IRGroup": {
276
+ "type": "object",
277
+ "required": ["id", "name", "description", "category", "assertions"],
278
+ "additionalProperties": false,
279
+ "properties": {
280
+ "id": { "type": "string" },
281
+ "name": { "type": "string" },
282
+ "description": { "type": "string" },
283
+ "category": { "type": "string" },
284
+ "assertions": {
285
+ "type": "array",
286
+ "items": { "$ref": "#/$defs/IRAssertion" }
287
+ },
288
+ "source": { "type": "string" },
289
+ "tags": { "type": "array", "items": { "type": "string" } }
290
+ }
233
291
  }
234
292
  }
235
293
  }
@@ -179,6 +179,7 @@
179
179
 
180
180
  import type { IRElementCriteria } from "./element-criteria";
181
181
  import type { IRDocument } from "./document";
182
+ import type { IRAssertion, IRGroup } from "./group";
182
183
  import type { IRState } from "./state";
183
184
  import type { IRTransition, IRTransitionAction, IRWaitSpec } from "./transition";
184
185
 
@@ -432,6 +433,55 @@ function buildGroup(state: IRState): LegacyGroup {
432
433
  };
433
434
  }
434
435
 
436
+ /**
437
+ * Convert an `IRAssertion` (IR-side, free-form criteria) into a `LegacyAssertion`.
438
+ * The shapes are near-identical; the only meaningful difference is that the
439
+ * IR-side `target.criteria` is `Record<string, unknown>` while the legacy
440
+ * shape is `LegacyCriteria` (a typed but extensible record). We pass the
441
+ * criteria object through verbatim — the open `[key: string]: unknown` index
442
+ * signature on `LegacyCriteria` accepts any record shape.
443
+ */
444
+ function synthesizedAssertionToLegacy(assertion: IRAssertion): LegacyAssertion {
445
+ const target: LegacyAssertionTarget = {
446
+ type: "search",
447
+ criteria: assertion.target.criteria as LegacyCriteria,
448
+ label: assertion.target.label,
449
+ };
450
+ const out: LegacyAssertion = {
451
+ id: assertion.id,
452
+ description: assertion.description,
453
+ category: assertion.category,
454
+ severity: assertion.severity as LegacyAssertion["severity"],
455
+ assertionType: assertion.assertionType,
456
+ target,
457
+ source: assertion.source,
458
+ reviewed: assertion.reviewed,
459
+ enabled: assertion.enabled,
460
+ };
461
+ if (assertion.precondition !== undefined) {
462
+ out.precondition = assertion.precondition;
463
+ }
464
+ return out;
465
+ }
466
+
467
+ /**
468
+ * Convert a synthesized `IRGroup` into a `LegacyGroup`. Mirrors the legacy
469
+ * shape one-to-one (modulo the free-form criteria pass-through described in
470
+ * `synthesizedAssertionToLegacy`). The synthesized group always carries a
471
+ * `source` (synthesis emits `"ai-generated"`); fall back to `"ai-generated"`
472
+ * for hand-written IR documents that omit it.
473
+ */
474
+ function synthesizedGroupToLegacy(group: IRGroup): LegacyGroup {
475
+ return {
476
+ id: group.id,
477
+ name: group.name,
478
+ description: group.description,
479
+ category: group.category,
480
+ assertions: group.assertions.map(synthesizedAssertionToLegacy),
481
+ source: group.source ?? "ai-generated",
482
+ };
483
+ }
484
+
435
485
  /**
436
486
  * Convert an IR transition action into the legacy `process[]` step shape.
437
487
  * Renames IR's `type` to legacy `action` (matches the runtime engine
@@ -520,6 +570,14 @@ export function projectIRToBundledPage(doc: IRDocument, notes?: string): LegacyS
520
570
  if (doc.metadata?.tags !== undefined) metadata.tags = doc.metadata.tags;
521
571
 
522
572
  const groups: LegacyGroup[] = doc.states.map(buildGroup);
573
+ if (doc.synthesizedGroups !== undefined) {
574
+ // Append synthesis-produced groups AFTER state-derived ones, in declared
575
+ // order. Byte-stable output (see file header) requires this exact ordering
576
+ // — anything else breaks `check-spec-pairing` style hashes.
577
+ for (const g of doc.synthesizedGroups) {
578
+ groups.push(synthesizedGroupToLegacy(g));
579
+ }
580
+ }
523
581
  const smStates: LegacyStateMachineState[] = doc.states.map((s) =>
524
582
  buildStateMachineState(s, doc.transitions, doc),
525
583
  );