@qontinui/shared-types 0.2.2 → 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.
- package/dist/TaskRunFindingActionType.d-B4VmmbB6.d.cts +62 -0
- package/dist/TaskRunFindingActionType.d-B4VmmbB6.d.ts +62 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/task-run.d.cts +3 -62
- package/dist/task-run.d.ts +3 -62
- package/dist/tauri-events.cjs +19 -0
- package/dist/tauri-events.cjs.map +1 -0
- package/dist/tauri-events.d.cts +1273 -0
- package/dist/tauri-events.d.ts +1273 -0
- package/dist/tauri-events.js +1 -0
- package/dist/tauri-events.js.map +1 -0
- package/dist/ui-bridge-ir.cjs +37 -0
- package/dist/ui-bridge-ir.cjs.map +1 -1
- package/dist/ui-bridge-ir.d.cts +90 -1
- package/dist/ui-bridge-ir.d.ts +90 -1
- package/dist/ui-bridge-ir.js +37 -0
- package/dist/ui-bridge-ir.js.map +1 -1
- package/package.json +7 -2
- package/src/generated/AppEvent.d.ts +587 -586
- package/src/generated/CanvasPanel.d.ts +35 -0
- package/src/generated/DevSeedFindingPayload.d.ts +30 -0
- package/src/generated/FlowEvent.d.ts +86 -66
- package/src/generated/RecommendationReviewDecisionPayload.d.ts +24 -0
- package/src/generated/RunnerConnectedConnection.d.ts +34 -0
- package/src/generated/RunnerFinding.d.ts +48 -0
- package/src/generated/RunnerFindingCodeContext.d.ts +20 -0
- package/src/generated/RunnerFindingUserInput.d.ts +19 -0
- package/src/generated/RunnerRelayMessage.d.ts +75 -0
- package/src/generated/RunnerStatusEvent.d.ts +67 -0
- package/src/generated/UiBridgeRequestEnvelope.d.ts +45 -0
- package/src/generated/UiBridgeResponseEnvelope.d.ts +66 -0
- package/src/generated/index.ts +11 -0
- package/src/tauri-events/_api.ts +90 -0
- package/src/tauri-events/index.ts +1 -0
- package/src/ui-bridge-ir/document.ts +9 -0
- package/src/ui-bridge-ir/group.ts +84 -0
- package/src/ui-bridge-ir/index.ts +1 -0
- package/src/ui-bridge-ir/ir.schema.json +58 -0
- package/src/ui-bridge-ir/projection.ts +58 -0
- package/src/ui-bridge-ir/__tests__/legacy-roster.test.ts +0 -182
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tauri Event Payload Types
|
|
3
|
+
*
|
|
4
|
+
* Wire-format types for payloads emitted on Tauri event channels by the
|
|
5
|
+
* runner. Use these to type the generic parameter of `listen<T>(...)` so
|
|
6
|
+
* future Rust serde renames (e.g. snake_case → camelCase) and shape
|
|
7
|
+
* changes break the TypeScript build instead of silently dropping events
|
|
8
|
+
* at the listener guard.
|
|
9
|
+
*
|
|
10
|
+
* ## Adjacently-tagged envelope (`AppEvent`)
|
|
11
|
+
*
|
|
12
|
+
* Most runner events are variants of the `AppEvent` enum, which is
|
|
13
|
+
* serialized via `#[serde(tag = "event_type", content = "data")]`. The
|
|
14
|
+
* wire payload is therefore `{ event_type: "<VariantName>", data: { ... } }`
|
|
15
|
+
* and consumers MUST read `event.payload.data.<field>` (NOT
|
|
16
|
+
* `event.payload.<field>`). The `AppEvent` discriminated union below
|
|
17
|
+
* encodes that envelope; reaching `data.<field>` after narrowing by
|
|
18
|
+
* `event_type` is type-safe.
|
|
19
|
+
*
|
|
20
|
+
* ## Direct-payload events
|
|
21
|
+
*
|
|
22
|
+
* A handful of events bypass `AppEvent` and emit a payload struct
|
|
23
|
+
* directly:
|
|
24
|
+
* - `terminal-output` → `TerminalOutputEvent`
|
|
25
|
+
* - `terminal-exit` → `TerminalExitEvent`
|
|
26
|
+
* - `terminal-created` → `TerminalInfo`
|
|
27
|
+
* - `finding_detected` / `finding_resolved` → wrap a `RunnerFinding`
|
|
28
|
+
* inside the `AppEvent` envelope's `data.finding` field as untyped
|
|
29
|
+
* JSON; consumers should cast `data.finding as RunnerFinding`.
|
|
30
|
+
*
|
|
31
|
+
* Source of truth:
|
|
32
|
+
* - `AppEvent`: `qontinui-runner/src-tauri/src/event_system/types.rs`
|
|
33
|
+
* - Terminal events: `qontinui-schemas/rust/src/terminal.rs`
|
|
34
|
+
* - `RunnerFinding`: `qontinui-runner/src-tauri/src/tauri_event_payloads.rs`
|
|
35
|
+
*
|
|
36
|
+
* Regenerate via `cd qontinui-runner && npm run gen-events`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export type { AppEvent } from "../generated/AppEvent";
|
|
40
|
+
|
|
41
|
+
// ── Terminal events (direct payloads, not wrapped in AppEvent) ──
|
|
42
|
+
export type { TerminalInfo } from "../generated/TerminalInfo";
|
|
43
|
+
export type { TerminalOutputEvent } from "../generated/TerminalOutputEvent";
|
|
44
|
+
export type { TerminalExitEvent } from "../generated/TerminalExitEvent";
|
|
45
|
+
|
|
46
|
+
// ── Runner-local finding payload (carried inside AppEvent::FindingDetected /
|
|
47
|
+
// AppEvent::FindingResolved as `data.finding`). Distinct from
|
|
48
|
+
// `qontinui_types::verification::Finding`. ──
|
|
49
|
+
export type { RunnerFinding } from "../generated/RunnerFinding";
|
|
50
|
+
export type { RunnerFindingCodeContext } from "../generated/RunnerFindingCodeContext";
|
|
51
|
+
export type { RunnerFindingUserInput } from "../generated/RunnerFindingUserInput";
|
|
52
|
+
|
|
53
|
+
// ── WS relay envelopes (runner→backend) and backend→client status events.
|
|
54
|
+
// Discriminated unions tagged on `type`. Source of truth lives in
|
|
55
|
+
// `qontinui-runner/src-tauri/src/relay_envelopes.rs`; backend-originated
|
|
56
|
+
// events mirror `qontinui-web/backend/app/services/runner/event_publisher.py`. ──
|
|
57
|
+
export type { RunnerRelayMessage } from "../generated/RunnerRelayMessage";
|
|
58
|
+
export type { RunnerStatusEvent } from "../generated/RunnerStatusEvent";
|
|
59
|
+
export type { RunnerConnectedConnection } from "../generated/RunnerConnectedConnection";
|
|
60
|
+
|
|
61
|
+
// ── Dev-only seed-finding payload (`dev:seed-finding` Tauri event from
|
|
62
|
+
// `commands/dev_findings.rs`). Sibling to the camelCase RunnerFinding
|
|
63
|
+
// fields so the TS listener can spread them straight into a Finding. ──
|
|
64
|
+
export type { DevSeedFindingPayload } from "../generated/DevSeedFindingPayload";
|
|
65
|
+
|
|
66
|
+
// ── Recommendation review decision payloads (`review-approved` /
|
|
67
|
+
// `review-rejected` from `commands/productivity.rs`). One struct
|
|
68
|
+
// covers both channels — only `userDecision` differs. ──
|
|
69
|
+
export type { RecommendationReviewDecisionPayload } from "../generated/RecommendationReviewDecisionPayload";
|
|
70
|
+
|
|
71
|
+
// ── Canvas panel payload carried inside AppEvent::CanvasUpdate's
|
|
72
|
+
// `data.panel` field. Wire-format mirror of the runner's `StoredPanel`
|
|
73
|
+
// (`mcp/canvas.rs`). The hand-written `CanvasPanel` in `../canvas/index.ts`
|
|
74
|
+
// pre-dates this generated re-export; both shapes are intentionally
|
|
75
|
+
// identical. New consumers should prefer this generated import. ──
|
|
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";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./_api";
|
|
@@ -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
|
+
}
|
|
@@ -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
|
);
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Legacy spec roster test (Phase A2 fixture gate).
|
|
3
|
-
*
|
|
4
|
-
* Walks the entire `qontinui_parent` monorepo, collects every shipping
|
|
5
|
-
* `*.spec.uibridge.json`, and round-trips each through:
|
|
6
|
-
*
|
|
7
|
-
* legacy --projectLegacyToIR--> IR --projectIRToBundledPage--> legacy'
|
|
8
|
-
*
|
|
9
|
-
* For each fixture we assert (semantically — not byte-strict):
|
|
10
|
-
* - same number of groups
|
|
11
|
-
* - same group ids in input order
|
|
12
|
-
* - same total assertion count across all groups
|
|
13
|
-
*
|
|
14
|
-
* This is coarse but effective: it catches any data loss in the inverse
|
|
15
|
-
* projection without forcing the inverse to preserve hand-edited assertion
|
|
16
|
-
* ids / severity / category / etc. (which the forward direction regenerates
|
|
17
|
-
* from defaults). Phase A4 will tighten the contract for hand-authored
|
|
18
|
-
* fields.
|
|
19
|
-
*
|
|
20
|
-
* The walker fails fast if it finds < 100 fixtures (canary against a
|
|
21
|
-
* misconfigured directory walk) — current expected count is 121 (96 runner +
|
|
22
|
-
* 19 web + 5 mobile + 2 supervisor) as of 2026-04. The number is allowed to
|
|
23
|
-
* grow; the lower bound just guards against accidentally testing nothing.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
27
|
-
import path from "node:path";
|
|
28
|
-
|
|
29
|
-
import { describe, expect, it } from "vitest";
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
projectIRToBundledPage,
|
|
33
|
-
projectLegacyToIR,
|
|
34
|
-
type LegacySpec,
|
|
35
|
-
} from "../projection";
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Discovery
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve the monorepo root from the test file's location. The test file
|
|
43
|
-
* lives at `qontinui-schemas/ts/src/ui-bridge-ir/__tests__/`, so five
|
|
44
|
-
* `..` segments take us to `qontinui_parent/`.
|
|
45
|
-
*
|
|
46
|
-
* Use `process.cwd()` as a fallback only when the test runs from outside the
|
|
47
|
-
* usual layout (e.g. CI checkout with renamed parent) — vitest sets cwd to
|
|
48
|
-
* the package root by default.
|
|
49
|
-
*/
|
|
50
|
-
const MONOREPO_ROOT = path.resolve(
|
|
51
|
-
__dirname,
|
|
52
|
-
"..",
|
|
53
|
-
"..",
|
|
54
|
-
"..",
|
|
55
|
-
"..",
|
|
56
|
-
"..",
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
// Directories to skip. Note: do NOT include "build" — Next.js apps have route
|
|
60
|
-
// folders named `build/` (e.g., qontinui-web/frontend/src/app/(app)/build/)
|
|
61
|
-
// that legitimately contain spec fixtures.
|
|
62
|
-
const SKIP_DIRS = new Set([
|
|
63
|
-
"node_modules",
|
|
64
|
-
"target",
|
|
65
|
-
"target-agent",
|
|
66
|
-
"dist",
|
|
67
|
-
".git",
|
|
68
|
-
".next",
|
|
69
|
-
".venv",
|
|
70
|
-
"__pycache__",
|
|
71
|
-
"out",
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Recursively walk `root`, collecting every `*.spec.uibridge.json` file.
|
|
76
|
-
* Skips the directory names in `SKIP_DIRS`. Synchronous; the fixture set is
|
|
77
|
-
* small (low hundreds) so we don't bother with worker threads.
|
|
78
|
-
*/
|
|
79
|
-
function collectSpecs(root: string): string[] {
|
|
80
|
-
const out: string[] = [];
|
|
81
|
-
const stack: string[] = [root];
|
|
82
|
-
while (stack.length > 0) {
|
|
83
|
-
const current = stack.pop()!;
|
|
84
|
-
let entries: import("node:fs").Dirent[];
|
|
85
|
-
try {
|
|
86
|
-
entries = readdirSync(current, { withFileTypes: true });
|
|
87
|
-
} catch {
|
|
88
|
-
// Permission errors / vanished dirs — skip silently.
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
for (const entry of entries) {
|
|
92
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
93
|
-
const full = path.join(current, entry.name);
|
|
94
|
-
if (entry.isDirectory()) {
|
|
95
|
-
stack.push(full);
|
|
96
|
-
} else if (entry.isFile() && entry.name.endsWith(".spec.uibridge.json")) {
|
|
97
|
-
out.push(full);
|
|
98
|
-
} else if (entry.isSymbolicLink()) {
|
|
99
|
-
// Resolve symlinks defensively — some plugin checkouts use them.
|
|
100
|
-
try {
|
|
101
|
-
const stat = statSync(full);
|
|
102
|
-
if (stat.isDirectory()) {
|
|
103
|
-
stack.push(full);
|
|
104
|
-
} else if (stat.isFile() && entry.name.endsWith(".spec.uibridge.json")) {
|
|
105
|
-
out.push(full);
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
// ignore
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// Sort for deterministic test ordering.
|
|
114
|
-
out.sort();
|
|
115
|
-
return out;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const SPECS = collectSpecs(MONOREPO_ROOT);
|
|
119
|
-
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Tests
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
describe("legacy spec roster — discovery sanity", () => {
|
|
125
|
-
it("finds at least 100 *.spec.uibridge.json files in the monorepo", () => {
|
|
126
|
-
expect(SPECS.length).toBeGreaterThanOrEqual(100);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("does not include node_modules / dist / target paths", () => {
|
|
130
|
-
for (const f of SPECS) {
|
|
131
|
-
expect(f).not.toMatch(/[\\/](?:node_modules|dist|target|\.git)[\\/]/);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("legacy spec roster — round-trip via IR", () => {
|
|
137
|
-
// Use it.each so every fixture gets its own test name and failures pinpoint
|
|
138
|
-
// the exact file. Pre-load each fixture as a tuple for the iterator.
|
|
139
|
-
const cases: ReadonlyArray<readonly [string, string]> = SPECS.map((p) => [
|
|
140
|
-
path.relative(MONOREPO_ROOT, p),
|
|
141
|
-
p,
|
|
142
|
-
] as const);
|
|
143
|
-
|
|
144
|
-
it.each(cases)("%s round-trips through IR without data loss", (_relpath, full) => {
|
|
145
|
-
let raw = readFileSync(full, "utf-8");
|
|
146
|
-
// Strip UTF-8 BOM if present — at least one shipping spec
|
|
147
|
-
// (settings-security.spec.uibridge.json) was saved with a BOM.
|
|
148
|
-
if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
|
|
149
|
-
const legacy = JSON.parse(raw) as LegacySpec;
|
|
150
|
-
|
|
151
|
-
const ir = projectLegacyToIR(legacy);
|
|
152
|
-
const projected = projectIRToBundledPage(ir);
|
|
153
|
-
|
|
154
|
-
// 1. Same number of groups.
|
|
155
|
-
expect(projected.groups.length).toBe(legacy.groups.length);
|
|
156
|
-
|
|
157
|
-
// 2. Same group ids in input order.
|
|
158
|
-
expect(projected.groups.map((g) => g.id)).toEqual(legacy.groups.map((g) => g.id));
|
|
159
|
-
|
|
160
|
-
// 3. Same total assertion count across all groups (coarse data-loss check).
|
|
161
|
-
const projectedAssertionCount = projected.groups.reduce(
|
|
162
|
-
(sum, g) => sum + g.assertions.length,
|
|
163
|
-
0,
|
|
164
|
-
);
|
|
165
|
-
const legacyAssertionCount = legacy.groups.reduce(
|
|
166
|
-
(sum, g) => sum + (Array.isArray(g.assertions) ? g.assertions.length : 0),
|
|
167
|
-
0,
|
|
168
|
-
);
|
|
169
|
-
// The forward projection emits a placeholder assertion for any state with
|
|
170
|
-
// zero requiredElements. Real legacy fixtures never have zero-assertion
|
|
171
|
-
// groups, but be defensive: allow projected >= legacy when legacy had
|
|
172
|
-
// groups with zero assertions.
|
|
173
|
-
const legacyHasEmptyGroup = legacy.groups.some(
|
|
174
|
-
(g) => !Array.isArray(g.assertions) || g.assertions.length === 0,
|
|
175
|
-
);
|
|
176
|
-
if (legacyHasEmptyGroup) {
|
|
177
|
-
expect(projectedAssertionCount).toBeGreaterThanOrEqual(legacyAssertionCount);
|
|
178
|
-
} else {
|
|
179
|
-
expect(projectedAssertionCount).toBe(legacyAssertionCount);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
});
|