@lumencast/protocol 0.1.0

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 (84) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +162 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/codec.d.ts +15 -0
  9. package/dist/codec.d.ts.map +1 -0
  10. package/dist/codec.js +328 -0
  11. package/dist/codec.js.map +1 -0
  12. package/dist/conformance/bundle-hash.d.ts +4 -0
  13. package/dist/conformance/bundle-hash.d.ts.map +1 -0
  14. package/dist/conformance/bundle-hash.js +49 -0
  15. package/dist/conformance/bundle-hash.js.map +1 -0
  16. package/dist/conformance/control-client.d.ts +42 -0
  17. package/dist/conformance/control-client.d.ts.map +1 -0
  18. package/dist/conformance/control-client.js +63 -0
  19. package/dist/conformance/control-client.js.map +1 -0
  20. package/dist/conformance/harness.d.ts +41 -0
  21. package/dist/conformance/harness.d.ts.map +1 -0
  22. package/dist/conformance/harness.js +441 -0
  23. package/dist/conformance/harness.js.map +1 -0
  24. package/dist/conformance/index.d.ts +8 -0
  25. package/dist/conformance/index.d.ts.map +1 -0
  26. package/dist/conformance/index.js +12 -0
  27. package/dist/conformance/index.js.map +1 -0
  28. package/dist/conformance/loader.d.ts +9 -0
  29. package/dist/conformance/loader.d.ts.map +1 -0
  30. package/dist/conformance/loader.js +27 -0
  31. package/dist/conformance/loader.js.map +1 -0
  32. package/dist/conformance/match.d.ts +7 -0
  33. package/dist/conformance/match.d.ts.map +1 -0
  34. package/dist/conformance/match.js +82 -0
  35. package/dist/conformance/match.js.map +1 -0
  36. package/dist/conformance/placeholders.d.ts +2 -0
  37. package/dist/conformance/placeholders.d.ts.map +1 -0
  38. package/dist/conformance/placeholders.js +40 -0
  39. package/dist/conformance/placeholders.js.map +1 -0
  40. package/dist/conformance/scenario.d.ts +33 -0
  41. package/dist/conformance/scenario.d.ts.map +1 -0
  42. package/dist/conformance/scenario.js +26 -0
  43. package/dist/conformance/scenario.js.map +1 -0
  44. package/dist/envelope.d.ts +66 -0
  45. package/dist/envelope.d.ts.map +1 -0
  46. package/dist/envelope.js +111 -0
  47. package/dist/envelope.js.map +1 -0
  48. package/dist/errors.d.ts +25 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +38 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/index.d.ts +7 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +8 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/leaf-path.d.ts +21 -0
  57. package/dist/leaf-path.d.ts.map +1 -0
  58. package/dist/leaf-path.js +51 -0
  59. package/dist/leaf-path.js.map +1 -0
  60. package/dist/sequence.d.ts +25 -0
  61. package/dist/sequence.d.ts.map +1 -0
  62. package/dist/sequence.js +51 -0
  63. package/dist/sequence.js.map +1 -0
  64. package/dist/types.d.ts +159 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +13 -0
  67. package/dist/types.js.map +1 -0
  68. package/package.json +56 -0
  69. package/src/cli.ts +176 -0
  70. package/src/codec.ts +374 -0
  71. package/src/conformance/bundle-hash.ts +54 -0
  72. package/src/conformance/control-client.ts +93 -0
  73. package/src/conformance/harness.ts +492 -0
  74. package/src/conformance/index.ts +34 -0
  75. package/src/conformance/loader.ts +39 -0
  76. package/src/conformance/match.ts +92 -0
  77. package/src/conformance/placeholders.ts +45 -0
  78. package/src/conformance/scenario.ts +71 -0
  79. package/src/envelope.ts +180 -0
  80. package/src/errors.ts +55 -0
  81. package/src/index.ts +63 -0
  82. package/src/leaf-path.ts +58 -0
  83. package/src/sequence.ts +60 -0
  84. package/src/types.ts +201 -0
@@ -0,0 +1,159 @@
1
+ /** LSDP major version. Bumped only on breaking envelope/semantic changes. */
2
+ export declare const PROTOCOL_VERSION: 1;
3
+ /** LSDP/1.0 WebSocket subprotocol — see LSDP/1 §1. Kept for 1.0 client compat. */
4
+ export declare const WS_SUBPROTOCOL: "lsdp.v1";
5
+ /** LSDP/1.1 WebSocket subprotocol — opts into the additive 1.1 frame surface
6
+ * (since_sequence resume, unsubscribe, transition, cause, nonce, client_msg_id,
7
+ * from_scene_id + show transition). See LSDP/1.1 envelope/header section. */
8
+ export declare const WS_SUBPROTOCOL_V1_1: "lsdp.v1.1";
9
+ /** Canonical advertise/accept list, ordered by preference (1.1 first, 1.0 fallback). */
10
+ export declare const WS_SUBPROTOCOLS: readonly ["lsdp.v1.1", "lsdp.v1"];
11
+ /** A leaf path expressed as a dot-separated string. See LSDP/1 §10 for reserved namespaces. */
12
+ export type LeafPath = string;
13
+ /** A scene identifier — operator-chosen, not derived. */
14
+ export type SceneId = string;
15
+ /** A scene version — sha256 hash prefixed with `sha256:`. */
16
+ export type SceneVersion = string;
17
+ /** A test session identifier. */
18
+ export type SessionId = string;
19
+ /** Permitted JSON values inside a `delta.patches[].value`. Objects are forbidden — push leaf-grain. */
20
+ export type LeafValue = string | number | boolean | null | LeafValue[];
21
+ /** Closed taxonomy of LSDP/1 error codes. Match by exact string equality. */
22
+ export type ErrorCode = "AUTH_DENIED" | "WRITE_FORBIDDEN" | "SCENE_NOT_FOUND" | "BUNDLE_FETCH_FAILED" | "BUNDLE_INCOMPATIBLE" | "VERSION_GAP" | "VERSION_MISMATCH" | "UNKNOWN_PATH" | "INVALID_VALUE" | "RATE_LIMIT" | "TEST_SESSION_EXPIRED" | "INTERNAL";
23
+ /** Per-leaf animation directive on a delta patch (LSDP/1.1 §3.2.2).
24
+ * Servers MAY emit ; runtimes interpret when applying the new value.
25
+ * 1.0 receivers ignore. */
26
+ export interface TransitionSpec {
27
+ kind: "tween" | "spring" | "snap";
28
+ /** tween only */
29
+ duration_ms?: number;
30
+ /** tween only */
31
+ easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out";
32
+ /** spring only */
33
+ stiffness?: number;
34
+ /** spring only */
35
+ damping?: number;
36
+ }
37
+ /** Optional provenance metadata on a delta (LSDP/1.1 §3.2.3). Receivers
38
+ * MUST NOT use it for semantic decisions — debug/audit only. */
39
+ export interface Cause {
40
+ /** e.g. "operator:user-abc", "adapter:http_poll", "service:ranker" */
41
+ source: string;
42
+ /** Echoes InputFrame.client_msg_id verbatim when the delta was caused
43
+ * by an operator input. */
44
+ input_id?: string;
45
+ }
46
+ /** Show-level scene-swap transition on a scene_changed frame
47
+ * (LSDP/1.1 §3.3.1). Runtimes that don't recognise `kind` fall back
48
+ * to crossfade. */
49
+ export interface SceneTransition {
50
+ kind: "crossfade" | (string & {});
51
+ duration_ms?: number;
52
+ }
53
+ /** A leaf-grain patch. */
54
+ export interface Patch {
55
+ path: LeafPath;
56
+ value: LeafValue;
57
+ /** Optional 1.1 per-leaf transition directive. */
58
+ transition?: TransitionSpec;
59
+ }
60
+ interface BaseFrame {
61
+ v: typeof PROTOCOL_VERSION;
62
+ /** Monotonically increasing per subscription. Required on server frames except `pong`. */
63
+ seq?: number;
64
+ /** ISO 8601 timestamp. SHOULD be sent on snapshots and errors; MAY be omitted on deltas. */
65
+ ts?: string;
66
+ }
67
+ export interface SnapshotFrame extends BaseFrame {
68
+ type: "snapshot";
69
+ seq: number;
70
+ scene_id: SceneId;
71
+ scene_version: SceneVersion;
72
+ /** Flat dictionary of leaf paths to JSON values. */
73
+ state: Record<LeafPath, LeafValue>;
74
+ }
75
+ export interface DeltaFrame extends BaseFrame {
76
+ type: "delta";
77
+ seq: number;
78
+ /** Non-empty array of patches; applied left-to-right, atomic per frame. */
79
+ patches: Patch[];
80
+ /** Optional provenance (LSDP/1.1 §3.2.3). Debug/audit only. */
81
+ cause?: Cause;
82
+ }
83
+ export interface SceneChangedFrame extends BaseFrame {
84
+ type: "scene_changed";
85
+ seq: number;
86
+ scene_id: SceneId;
87
+ scene_version: SceneVersion;
88
+ /** Previously active scene id (LSDP/1.1 §3.3.1). 1.0 receivers ignore. */
89
+ from_scene_id?: SceneId;
90
+ /** Show-level transition between old and new scene (LSDP/1.1 §3.3.1). */
91
+ transition?: SceneTransition;
92
+ }
93
+ export interface ErrorFrame extends BaseFrame {
94
+ type: "error";
95
+ seq: number;
96
+ code: ErrorCode;
97
+ message: string;
98
+ recoverable: boolean;
99
+ /**
100
+ * REQUIRED for path-scoped codes (`WRITE_FORBIDDEN`, `UNKNOWN_PATH`,
101
+ * `INVALID_VALUE`) per LSDP/1.0.1 §3.4.1. Forbidden for codes that
102
+ * are not path-scoped.
103
+ */
104
+ path?: LeafPath;
105
+ /** Optional, for `RATE_LIMIT`. */
106
+ retry_after_ms?: number;
107
+ /** Optional, for `BUNDLE_INCOMPATIBLE`. */
108
+ requested_version?: string;
109
+ supported_version?: string;
110
+ /** Optional, for `TEST_SESSION_EXPIRED`. */
111
+ session?: string;
112
+ }
113
+ export interface PongFrame {
114
+ v: typeof PROTOCOL_VERSION;
115
+ type: "pong";
116
+ /** Echoes PingFrame.nonce verbatim (LSDP/1.1 §3.5). 1.0 servers omit. */
117
+ nonce?: string;
118
+ }
119
+ export type ServerFrame = SnapshotFrame | DeltaFrame | SceneChangedFrame | ErrorFrame | PongFrame;
120
+ export interface SubscribeFrame {
121
+ v: typeof PROTOCOL_VERSION;
122
+ type: "subscribe";
123
+ /** Opaque authentication token. */
124
+ token: string;
125
+ /** Required for test mode (preview a specific scene); forbidden for live. */
126
+ scene?: SceneId;
127
+ /** Required for test mode with isolated session; forbidden otherwise. */
128
+ session?: SessionId;
129
+ /** Last seq the client successfully observed before disconnect
130
+ * (LSDP/1.1 §4.1, §18). Server resumes with deltas from
131
+ * since_sequence+1 if the replay buffer covers, else fresh snapshot.
132
+ * 1.0 servers MUST ignore this field. Omit (or 0) means no resume. */
133
+ since_sequence?: number;
134
+ }
135
+ export interface InputFrame {
136
+ v: typeof PROTOCOL_VERSION;
137
+ type: "input";
138
+ /** Non-empty. Server validates each path against active scene's `operator_inputs`. */
139
+ patches: Patch[];
140
+ /** Free-form correlation tag (LSDP/1.1 §4.2). Server MUST echo
141
+ * verbatim in the resulting Delta.cause.input_id. 1.0 servers ignore. */
142
+ client_msg_id?: string;
143
+ }
144
+ export interface PingFrame {
145
+ v: typeof PROTOCOL_VERSION;
146
+ type: "ping";
147
+ /** Free-form correlation identifier (LSDP/1.1 §4.3). Receiver MUST
148
+ * echo verbatim in the Pong reply. */
149
+ nonce?: string;
150
+ }
151
+ /** Clean teardown signal (LSDP/1.1 §4.4). Server MUST close the
152
+ * WebSocket within 1 second of receipt. No data flows after. */
153
+ export interface UnsubscribeFrame {
154
+ v: typeof PROTOCOL_VERSION;
155
+ type: "unsubscribe";
156
+ }
157
+ export type ClientFrame = SubscribeFrame | InputFrame | PingFrame | UnsubscribeFrame;
158
+ export {};
159
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ // LSDP/1 wire types and shared shapes.
2
+ // Canonical reference: lumencast-protocol/spec/LSDP-1.md (and ERROR-CODES.md, RUNTIME-API.md).
3
+ /** LSDP major version. Bumped only on breaking envelope/semantic changes. */
4
+ export const PROTOCOL_VERSION = 1;
5
+ /** LSDP/1.0 WebSocket subprotocol — see LSDP/1 §1. Kept for 1.0 client compat. */
6
+ export const WS_SUBPROTOCOL = "lsdp.v1";
7
+ /** LSDP/1.1 WebSocket subprotocol — opts into the additive 1.1 frame surface
8
+ * (since_sequence resume, unsubscribe, transition, cause, nonce, client_msg_id,
9
+ * from_scene_id + show transition). See LSDP/1.1 envelope/header section. */
10
+ export const WS_SUBPROTOCOL_V1_1 = "lsdp.v1.1";
11
+ /** Canonical advertise/accept list, ordered by preference (1.1 first, 1.0 fallback). */
12
+ export const WS_SUBPROTOCOLS = [WS_SUBPROTOCOL_V1_1, WS_SUBPROTOCOL];
13
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,+FAA+F;AAE/F,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAC;AAE3C,kFAAkF;AAClF,MAAM,CAAC,MAAM,cAAc,GAAG,SAAkB,CAAC;AAEjD;;6EAE6E;AAC7E,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAoB,CAAC;AAExD,wFAAwF;AACxF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,mBAAmB,EAAE,cAAc,CAAU,CAAC"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@lumencast/protocol",
3
+ "version": "0.1.0",
4
+ "description": "LSDP/1 wire protocol — envelope, codec, sequence, leaf-path utilities, error taxonomy, types. Pure code, no IO.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./conformance": {
16
+ "types": "./dist/conformance/index.d.ts",
17
+ "import": "./dist/conformance/index.js"
18
+ },
19
+ "./cli": "./dist/cli.js"
20
+ },
21
+ "bin": {
22
+ "lumencast-js-conformance": "./dist/cli.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "homepage": "https://github.com/Lumencast/lumencast-js/tree/main/packages/protocol",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/Lumencast/lumencast-js.git",
32
+ "directory": "packages/protocol"
33
+ },
34
+ "engines": {
35
+ "node": ">=22"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "dependencies": {
42
+ "ws": "^8.18.0",
43
+ "yaml": "^2.6.1"
44
+ },
45
+ "devDependencies": {
46
+ "@types/ws": "^8.5.13",
47
+ "vitest": "^4.1.5"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -b",
51
+ "typecheck": "tsc -b --pretty",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "conformance": "vitest run --config vitest.conformance.config.ts"
55
+ }
56
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // lumencast-js conformance CLI.
3
+ //
4
+ // Subcommands:
5
+ // conformance --server <ws-url> --control-url <http-url>
6
+ // [--scenarios-dir <path>] [--tag required]
7
+ // [--scenario <name>] [--timeout 60s]
8
+ //
9
+ // Walks every scenario whose tag matches `--tag` (default required) under
10
+ // `<scenarios-dir>` (defaults to <env LUMENCAST_PROTOCOL_REPO>/conformance/v1/scenarios)
11
+ // and runs the harness against the supplied server + control plane.
12
+ //
13
+ // Exit code: 0 if all PASS, 1 if any FAIL.
14
+
15
+ import { existsSync } from "node:fs";
16
+ import { resolve } from "node:path";
17
+ import { parseArgs } from "node:util";
18
+ import { Harness, loadScenarios, type Report, type Tag } from "./conformance/index.js";
19
+
20
+ async function main(argv: string[]): Promise<number> {
21
+ const sub = argv[0];
22
+ if (!sub || sub === "--help" || sub === "-h") {
23
+ printUsage();
24
+ return sub ? 0 : 2;
25
+ }
26
+ if (sub === "conformance") return cmdConformance(argv.slice(1));
27
+ process.stderr.write(`lumencast-js: unknown subcommand "${sub}"\n`);
28
+ printUsage();
29
+ return 2;
30
+ }
31
+
32
+ function printUsage(): void {
33
+ process.stderr.write(
34
+ [
35
+ "usage: lumencast-js-conformance conformance <flags>",
36
+ "",
37
+ " --server <ws-url> WS endpoint (used if /test/setup doesn't return one)",
38
+ " --control-url <http-url> HTTP test control plane root (required)",
39
+ " --scenarios-dir <path> directory containing scenario YAMLs",
40
+ " default: $LUMENCAST_PROTOCOL_REPO/conformance/v1/scenarios",
41
+ " --tag <required|recommended|extended> default: required",
42
+ " --scenario <name> run a single scenario (basename without .yaml)",
43
+ " --timeout <ms> total run timeout, default 60000",
44
+ "",
45
+ ].join("\n"),
46
+ );
47
+ }
48
+
49
+ async function cmdConformance(args: string[]): Promise<number> {
50
+ let parsed;
51
+ try {
52
+ parsed = parseArgs({
53
+ args,
54
+ options: {
55
+ server: { type: "string" },
56
+ "control-url": { type: "string" },
57
+ "scenarios-dir": { type: "string" },
58
+ tag: { type: "string", default: "required" },
59
+ scenario: { type: "string" },
60
+ timeout: { type: "string", default: "60000" },
61
+ help: { type: "boolean", short: "h" },
62
+ },
63
+ strict: true,
64
+ allowPositionals: false,
65
+ });
66
+ } catch (err) {
67
+ process.stderr.write(`conformance: ${(err as Error).message}\n`);
68
+ return 2;
69
+ }
70
+
71
+ if (parsed.values["help"]) {
72
+ printUsage();
73
+ return 0;
74
+ }
75
+
76
+ const controlUrl = parsed.values["control-url"] as string | undefined;
77
+ if (!controlUrl) {
78
+ process.stderr.write("--control-url required\n");
79
+ return 2;
80
+ }
81
+ const serverUrl = parsed.values["server"] as string | undefined;
82
+ const tagRaw = parsed.values["tag"] as string;
83
+ if (tagRaw !== "required" && tagRaw !== "recommended" && tagRaw !== "extended") {
84
+ process.stderr.write(`--tag must be required|recommended|extended, got ${tagRaw}\n`);
85
+ return 2;
86
+ }
87
+ const tag: Tag = tagRaw;
88
+
89
+ const scenariosDir = resolveScenariosDir(parsed.values["scenarios-dir"] as string | undefined);
90
+ if (!scenariosDir) return 2;
91
+
92
+ const timeoutMs = Number.parseInt(parsed.values["timeout"] as string, 10);
93
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
94
+ process.stderr.write(`--timeout invalid\n`);
95
+ return 2;
96
+ }
97
+
98
+ let scenarios;
99
+ try {
100
+ scenarios = loadScenarios({
101
+ scenariosDir,
102
+ ...(parsed.values["scenario"] ? { scenarioName: parsed.values["scenario"] as string } : {}),
103
+ });
104
+ } catch (err) {
105
+ process.stderr.write(`load: ${(err as Error).message}\n`);
106
+ return 2;
107
+ }
108
+
109
+ const harness = new Harness({
110
+ ...(serverUrl ? { serverUrl } : {}),
111
+ controlUrl,
112
+ });
113
+
114
+ const deadline = setTimeout(() => {
115
+ process.stderr.write(`conformance: timeout ${timeoutMs}ms exceeded\n`);
116
+ process.exit(1);
117
+ }, timeoutMs);
118
+ deadline.unref();
119
+
120
+ const report = await harness.runAll(scenarios, tag);
121
+
122
+ printReport(report);
123
+
124
+ return report.failed > 0 ? 1 : 0;
125
+ }
126
+
127
+ function resolveScenariosDir(flag: string | undefined): string | null {
128
+ if (flag) return resolve(flag);
129
+ const repo = process.env["LUMENCAST_PROTOCOL_REPO"];
130
+ if (repo) return resolve(repo, "conformance/v1/scenarios");
131
+ // Fallback: try several candidates so the CLI works whether it is
132
+ // invoked from the lumencast-js root, from inside the lumencast-protocol
133
+ // checkout (e.g. interop/run-matrix.sh), or from a parallel directory
134
+ // structure. Returns the first candidate that resolves to an existing
135
+ // directory ; falls back to the canonical sibling layout so the error
136
+ // message still points at the expected path.
137
+ const cwd = process.cwd();
138
+ const candidates = [
139
+ // Already inside a lumencast-protocol checkout (cwd is interop/, scripts/, …).
140
+ resolve(cwd, "../conformance/v1/scenarios"),
141
+ resolve(cwd, "conformance/v1/scenarios"),
142
+ // Sibling of the monorepo (the original heuristic).
143
+ resolve(cwd, "../lumencast-protocol/conformance/v1/scenarios"),
144
+ // Parent-of-parent sibling (helps when invoked from a deeper dist/ shim).
145
+ resolve(cwd, "../../lumencast-protocol/conformance/v1/scenarios"),
146
+ ];
147
+ for (const candidate of candidates) {
148
+ if (existsSync(candidate)) return candidate;
149
+ }
150
+ return candidates[2] ?? candidates[0] ?? null;
151
+ }
152
+
153
+ function printReport(rep: Report): void {
154
+ process.stdout.write(
155
+ `Conformance report — ${rep.total} total, ${rep.passed} passed, ${rep.failed} failed, ${rep.skipped} skipped\n`,
156
+ );
157
+ for (const r of rep.results) {
158
+ if (r.outcome === "PASS") {
159
+ process.stdout.write(` PASS ${r.name} [${r.tag}/${r.target}]\n`);
160
+ } else if (r.outcome === "SKIP") {
161
+ process.stdout.write(` SKIP ${r.name} — ${r.reason ?? "filtered"}\n`);
162
+ } else {
163
+ process.stdout.write(` FAIL ${r.name} [${r.tag}/${r.target}] — ${r.reason ?? "unknown"}\n`);
164
+ }
165
+ }
166
+ }
167
+
168
+ main(process.argv.slice(2)).then(
169
+ (code) => {
170
+ process.exitCode = code;
171
+ },
172
+ (err) => {
173
+ process.stderr.write(`lumencast-js: ${(err as Error).stack ?? String(err)}\n`);
174
+ process.exitCode = 1;
175
+ },
176
+ );