@ikenga/contract 0.11.0 → 0.13.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.
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAE5D,gFAAgF;;YA/BhF,4DAA4D;;YAE5D,0DAA0D;;;;;;;;;QA+B1D;;;;;;WAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaH,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAIxD,eAAO,MAAM,mBAAmB;IAC9B,qBAAqB;;IAErB,qBAAqB;;IAErB;;;;;OAKG;;IAEH,4EAA4E;;IAE5E,2EAA2E;;IAE3E;;;;OAIG;;IAEH;;;;;;;OAOG;;;;;;;;;;;;;;;;;;EAEH,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,mBAAmB;;IAE9B,wDAAwD;;;QAnCxD,qBAAqB;;QAErB,qBAAqB;;QAErB;;;;;WAKG;;QAEH,4EAA4E;;QAE5E,2EAA2E;;QAE3E;;;;WAIG;;QAEH;;;;;;;WAOG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUH,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAE5D,gFAAgF;;YA/BhF,4DAA4D;;YAE5D,0DAA0D;;;;;;;;;QA+B1D;;;;;;WAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaH,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAIxD,eAAO,MAAM,mBAAmB;IAC9B,qBAAqB;;IAErB,qBAAqB;;IAErB;;;;;OAKG;;IAEH,4EAA4E;;IAE5E,2EAA2E;;IAE3E;;;;OAIG;;IAEH;;;;;;;OAOG;;;;;;;;;;;;;;;;;;EAEH,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,mBAAmB;;IAE9B,wDAAwD;;;QAnCxD,qBAAqB;;QAErB,qBAAqB;;QAErB;;;;;WAKG;;QAEH,4EAA4E;;QAE5E,2EAA2E;;QAE3E;;;;WAIG;;QAEH;;;;;;;WAOG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUH,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,128 @@
1
+ import { z } from 'zod';
2
+ /** Envelope + descriptor wire version. Bumped on non-additive change. */
3
+ export declare const WINDOW_CONTRACT_VERSION: 1;
4
+ /**
5
+ * What a window is.
6
+ * - `primary` — the original "main" window (full chrome).
7
+ * - `single-surface` — a thin detached window hosting one surface (Flavor C).
8
+ * - `pane-set` — a torn-off pane group (Flavor A, Phase 2).
9
+ * - `workspace` — a second full workspace window (Flavor B, Phase 3).
10
+ */
11
+ export declare const WindowKindSchema: z.ZodEnum<["primary", "single-surface", "pane-set", "workspace"]>;
12
+ export type WindowKind = z.infer<typeof WindowKindSchema>;
13
+ export declare const WindowDescriptorSchema: z.ZodObject<{
14
+ /** OS window label. "main" for the primary; "detached-<n>" otherwise. */
15
+ label: z.ZodString;
16
+ kind: z.ZodEnum<["primary", "single-surface", "pane-set", "workspace"]>;
17
+ /** Surface/route ids the window's FE entry mounts. */
18
+ surface_set: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
19
+ /** Project this window is bound to (Flavor B); null = follows primary. */
20
+ project_id: z.ZodDefault<z.ZodNullable<z.ZodString>>;
21
+ /**
22
+ * SQLite `layout_state` partition. A KEY-STRING suffix folded into
23
+ * `scopedKey()` (G-03) — NOT a new column. Usually equals `label`.
24
+ */
25
+ layout_key: z.ZodString;
26
+ }, "strict", z.ZodTypeAny, {
27
+ kind: "primary" | "single-surface" | "pane-set" | "workspace";
28
+ label: string;
29
+ surface_set: string[];
30
+ project_id: string | null;
31
+ layout_key: string;
32
+ }, {
33
+ kind: "primary" | "single-surface" | "pane-set" | "workspace";
34
+ label: string;
35
+ layout_key: string;
36
+ surface_set?: string[] | undefined;
37
+ project_id?: string | null | undefined;
38
+ }>;
39
+ export type WindowDescriptor = z.infer<typeof WindowDescriptorSchema>;
40
+ /**
41
+ * Where a window event goes. The DEFAULT for streaming/data channels is
42
+ * `broadcast` (every window filters its own subscription); only the channels
43
+ * that race (see WINDOW_TARGETED_CHANNELS) carry `window` targeting.
44
+ */
45
+ export declare const WindowEventTargetSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
46
+ kind: z.ZodLiteral<"broadcast">;
47
+ }, "strict", z.ZodTypeAny, {
48
+ kind: "broadcast";
49
+ }, {
50
+ kind: "broadcast";
51
+ }>, z.ZodObject<{
52
+ kind: z.ZodLiteral<"window">;
53
+ label: z.ZodString;
54
+ }, "strict", z.ZodTypeAny, {
55
+ kind: "window";
56
+ label: string;
57
+ }, {
58
+ kind: "window";
59
+ label: string;
60
+ }>]>;
61
+ export type WindowEventTarget = z.infer<typeof WindowEventTargetSchema>;
62
+ /**
63
+ * The base shape every cross-window event rides. `payload` is opaque at the
64
+ * contract layer (each topic refines it); the envelope itself is what the
65
+ * registry + windows agree on.
66
+ */
67
+ export declare const WindowEventEnvelopeSchema: z.ZodObject<{
68
+ v: z.ZodLiteral<1>;
69
+ /** Channel name, e.g. "window://opened". */
70
+ topic: z.ZodString;
71
+ /** Emitting window label, or "core" for the Rust core. */
72
+ source_label: z.ZodString;
73
+ target: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
74
+ kind: z.ZodLiteral<"broadcast">;
75
+ }, "strict", z.ZodTypeAny, {
76
+ kind: "broadcast";
77
+ }, {
78
+ kind: "broadcast";
79
+ }>, z.ZodObject<{
80
+ kind: z.ZodLiteral<"window">;
81
+ label: z.ZodString;
82
+ }, "strict", z.ZodTypeAny, {
83
+ kind: "window";
84
+ label: string;
85
+ }, {
86
+ kind: "window";
87
+ label: string;
88
+ }>]>;
89
+ payload: z.ZodUnknown;
90
+ }, "strict", z.ZodTypeAny, {
91
+ v: 1;
92
+ topic: string;
93
+ source_label: string;
94
+ target: {
95
+ kind: "broadcast";
96
+ } | {
97
+ kind: "window";
98
+ label: string;
99
+ };
100
+ payload?: unknown;
101
+ }, {
102
+ v: 1;
103
+ topic: string;
104
+ source_label: string;
105
+ target: {
106
+ kind: "broadcast";
107
+ } | {
108
+ kind: "window";
109
+ label: string;
110
+ };
111
+ payload?: unknown;
112
+ }>;
113
+ export type WindowEventEnvelope = z.infer<typeof WindowEventEnvelopeSchema>;
114
+ /** Canonical window-lifecycle channels emitted by the Rust core. */
115
+ export declare const WINDOW_TOPICS: {
116
+ readonly opened: "window://opened";
117
+ readonly closed: "window://closed";
118
+ readonly focusChanged: "window://focus-changed";
119
+ };
120
+ export type WindowTopic = (typeof WINDOW_TOPICS)[keyof typeof WINDOW_TOPICS];
121
+ /**
122
+ * Channels that MUST be window-targeted (`emit_to`) rather than broadcast,
123
+ * because a broadcast races across windows (first-reply-wins / wrong-window).
124
+ * Everything not listed here stays broadcast + per-window topic filtering.
125
+ * (plans/multi-window 03-research-internal: the ~25-channel routing table.)
126
+ */
127
+ export declare const WINDOW_TARGETED_CHANNELS: readonly ["screenshot://request", "screenshot://shortcut", "projects:active-changed"];
128
+ //# sourceMappingURL=window.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../src/window.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,yEAAyE;AACzE,eAAO,MAAM,uBAAuB,EAAG,CAAU,CAAC;AAIlD;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,mEAK3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,sBAAsB;IAEjC,yEAAyE;;;IAGzE,sDAAsD;;IAEtD,0EAA0E;;IAE1E;;;OAGG;;;;;;;;;;;;;;EAGK,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE;;;;GAIG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;IAGlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;IAGpC,4CAA4C;;IAE5C,0DAA0D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKlD,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,oEAAoE;AACpE,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAE7E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,uFAI3B,CAAC"}
package/dist/window.js ADDED
@@ -0,0 +1,86 @@
1
+ // Multi-window contract — mirrors the Rust source-of-truth in
2
+ // `royalti-io/ikenga` at `src-tauri/src/window/{descriptor,events}.rs`.
3
+ //
4
+ // This is the `G-WINDOW-MODEL` freeze-gate output (plans/multi-window WP-02).
5
+ // It has TWO co-equal halves: the `WindowDescriptor` (what a window IS) and the
6
+ // cross-window event envelope (how windows talk over the shared Rust core).
7
+ // Both are imported by every downstream consumer (the window registry, the thin
8
+ // detached FE entry, Flavor C). Field changes MUST be made in lockstep with the
9
+ // Rust structs; bump WINDOW_CONTRACT_VERSION on any non-additive change.
10
+ import { z } from 'zod';
11
+ /** Envelope + descriptor wire version. Bumped on non-additive change. */
12
+ export const WINDOW_CONTRACT_VERSION = 1;
13
+ // ---------- WindowDescriptor ----------
14
+ /**
15
+ * What a window is.
16
+ * - `primary` — the original "main" window (full chrome).
17
+ * - `single-surface` — a thin detached window hosting one surface (Flavor C).
18
+ * - `pane-set` — a torn-off pane group (Flavor A, Phase 2).
19
+ * - `workspace` — a second full workspace window (Flavor B, Phase 3).
20
+ */
21
+ export const WindowKindSchema = z.enum([
22
+ 'primary',
23
+ 'single-surface',
24
+ 'pane-set',
25
+ 'workspace',
26
+ ]);
27
+ export const WindowDescriptorSchema = z
28
+ .object({
29
+ /** OS window label. "main" for the primary; "detached-<n>" otherwise. */
30
+ label: z.string().min(1),
31
+ kind: WindowKindSchema,
32
+ /** Surface/route ids the window's FE entry mounts. */
33
+ surface_set: z.array(z.string()).default([]),
34
+ /** Project this window is bound to (Flavor B); null = follows primary. */
35
+ project_id: z.string().nullable().default(null),
36
+ /**
37
+ * SQLite `layout_state` partition. A KEY-STRING suffix folded into
38
+ * `scopedKey()` (G-03) — NOT a new column. Usually equals `label`.
39
+ */
40
+ layout_key: z.string().min(1),
41
+ })
42
+ .strict();
43
+ // ---------- Cross-window event envelope ----------
44
+ /**
45
+ * Where a window event goes. The DEFAULT for streaming/data channels is
46
+ * `broadcast` (every window filters its own subscription); only the channels
47
+ * that race (see WINDOW_TARGETED_CHANNELS) carry `window` targeting.
48
+ */
49
+ export const WindowEventTargetSchema = z.discriminatedUnion('kind', [
50
+ z.object({ kind: z.literal('broadcast') }).strict(),
51
+ z.object({ kind: z.literal('window'), label: z.string().min(1) }).strict(),
52
+ ]);
53
+ /**
54
+ * The base shape every cross-window event rides. `payload` is opaque at the
55
+ * contract layer (each topic refines it); the envelope itself is what the
56
+ * registry + windows agree on.
57
+ */
58
+ export const WindowEventEnvelopeSchema = z
59
+ .object({
60
+ v: z.literal(WINDOW_CONTRACT_VERSION),
61
+ /** Channel name, e.g. "window://opened". */
62
+ topic: z.string().min(1),
63
+ /** Emitting window label, or "core" for the Rust core. */
64
+ source_label: z.string().min(1),
65
+ target: WindowEventTargetSchema,
66
+ payload: z.unknown(),
67
+ })
68
+ .strict();
69
+ /** Canonical window-lifecycle channels emitted by the Rust core. */
70
+ export const WINDOW_TOPICS = {
71
+ opened: 'window://opened',
72
+ closed: 'window://closed',
73
+ focusChanged: 'window://focus-changed',
74
+ };
75
+ /**
76
+ * Channels that MUST be window-targeted (`emit_to`) rather than broadcast,
77
+ * because a broadcast races across windows (first-reply-wins / wrong-window).
78
+ * Everything not listed here stays broadcast + per-window topic filtering.
79
+ * (plans/multi-window 03-research-internal: the ~25-channel routing table.)
80
+ */
81
+ export const WINDOW_TARGETED_CHANNELS = [
82
+ 'screenshot://request',
83
+ 'screenshot://shortcut',
84
+ 'projects:active-changed',
85
+ ];
86
+ //# sourceMappingURL=window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window.js","sourceRoot":"","sources":["../src/window.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,wEAAwE;AACxE,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,yEAAyE;AAEzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,yEAAyE;AACzE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAU,CAAC;AAElD,yCAAyC;AAEzC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC;IACtC,SAAS;IACT,gBAAgB;IAChB,UAAU;IACV,WAAW;CACX,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC;KACrC,MAAM,CAAC;IACP,yEAAyE;IACzE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,IAAI,EAAE,gBAAgB;IACtB,sDAAsD;IACtD,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5C,0EAA0E;IAC1E,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC/C;;;OAGG;IACH,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC7B,CAAC;KACD,MAAM,EAAE,CAAC;AAGX,oDAAoD;AAEpD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;IACnE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;IACnD,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;CAC1E,CAAC,CAAC;AAGH;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC;KACxC,MAAM,CAAC;IACP,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,uBAAuB,CAAC;IACrC,4CAA4C;IAC5C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,0DAA0D;IAC1D,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,MAAM,EAAE,uBAAuB;IAC/B,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;CACpB,CAAC;KACD,MAAM,EAAE,CAAC;AAGX,oEAAoE;AACpE,MAAM,CAAC,MAAM,aAAa,GAAG;IAC5B,MAAM,EAAE,iBAAiB;IACzB,MAAM,EAAE,iBAAiB;IACzB,YAAY,EAAE,wBAAwB;CAC7B,CAAC;AAGX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACvC,sBAAsB;IACtB,uBAAuB;IACvB,yBAAyB;CAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikenga/contract",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Shared contract for the Ikenga pkg system: manifest schema, RPC types, Engine interface, capability scopes.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -55,6 +55,14 @@
55
55
  "./pa-actions": {
56
56
  "types": "./dist/pa-actions.d.ts",
57
57
  "import": "./dist/pa-actions.js"
58
+ },
59
+ "./window": {
60
+ "types": "./dist/window.d.ts",
61
+ "import": "./dist/window.js"
62
+ },
63
+ "./action-frontmatter": {
64
+ "types": "./dist/action-frontmatter.d.ts",
65
+ "import": "./dist/action-frontmatter.js"
58
66
  }
59
67
  },
60
68
  "files": [
@@ -86,7 +94,8 @@
86
94
  "react": "^19.0.0",
87
95
  "react-dom": "^19.0.0",
88
96
  "tsx": "^4.21.0",
89
- "typescript": "^5.4.0"
97
+ "typescript": "^5.4.0",
98
+ "yaml": "^1.10.3"
90
99
  },
91
100
  "publishConfig": {
92
101
  "access": "public"
@@ -0,0 +1,219 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ import YAML from 'yaml';
8
+
9
+ import {
10
+ ActionFrontmatter,
11
+ CapabilityEnum,
12
+ DomainEnum,
13
+ RunBinding,
14
+ SetupSpec,
15
+ Trigger,
16
+ UxModeEnum,
17
+ } from './action-frontmatter.js';
18
+
19
+ // ─── unit: sub-schemas ──────────────────────────────────────────────────────
20
+
21
+ test('DomainEnum: the eight domains + skill-core', () => {
22
+ for (const d of [
23
+ 'tasks',
24
+ 'mail',
25
+ 'outbound',
26
+ 'sales',
27
+ 'finance',
28
+ 'content',
29
+ 'research',
30
+ 'strategy',
31
+ 'skill-core',
32
+ ]) {
33
+ assert.equal(DomainEnum.parse(d), d);
34
+ }
35
+ assert.throws(() => DomainEnum.parse('marketing'));
36
+ });
37
+
38
+ test('UxModeEnum: exactly the five modes', () => {
39
+ for (const m of ['confirm', 'silent', 'form', 'streaming', 'approve']) {
40
+ assert.equal(UxModeEnum.parse(m), m);
41
+ }
42
+ assert.throws(() => UxModeEnum.parse('modal'));
43
+ });
44
+
45
+ test('RunBinding: chat_prompt requires a non-empty prompt', () => {
46
+ const r = RunBinding.parse({ kind: 'chat_prompt', prompt: '# do the thing' });
47
+ assert.equal(r.kind, 'chat_prompt');
48
+ assert.throws(() => RunBinding.parse({ kind: 'chat_prompt', prompt: '' }));
49
+ });
50
+
51
+ test('RunBinding: sidecar defaults args to []', () => {
52
+ const r = RunBinding.parse({ kind: 'sidecar', sidecar_id: 'pa-x' });
53
+ assert.deepEqual(r.kind === 'sidecar' ? r.args : null, []);
54
+ });
55
+
56
+ test('Trigger: schedule needs a cron; unknown kind rejected', () => {
57
+ const t = Trigger.parse({ kind: 'schedule', cron: '30 */4 * * *', label: 'x' });
58
+ assert.equal(t.kind, 'schedule');
59
+ assert.throws(() => Trigger.parse({ kind: 'timer', every: '5m' }));
60
+ });
61
+
62
+ test('CapabilityEnum: closed set', () => {
63
+ for (const c of ['sqlite', 'mcp', 'sidecar', 'network', 'fs', 'secrets', 'chat']) {
64
+ assert.equal(CapabilityEnum.parse(c), c);
65
+ }
66
+ assert.throws(() => CapabilityEnum.parse('gpu'));
67
+ });
68
+
69
+ test('SetupSpec: template_version must be a positive int', () => {
70
+ const s = SetupSpec.parse({ mode: 'interview', template_version: 1 });
71
+ assert.equal(s.template_version, 1);
72
+ assert.throws(() => SetupSpec.parse({ mode: 'interview', template_version: 0 }));
73
+ });
74
+
75
+ // ─── unit: ActionFrontmatter refinements ────────────────────────────────────
76
+
77
+ test('ActionFrontmatter: name must be kebab-case', () => {
78
+ const base = {
79
+ name: 'draft-reply',
80
+ description: 'x',
81
+ domain: 'mail',
82
+ ux_mode: 'approve',
83
+ run: { kind: 'chat_prompt', prompt: 'x' },
84
+ };
85
+ assert.equal(ActionFrontmatter.parse(base).name, 'draft-reply');
86
+ assert.throws(() => ActionFrontmatter.parse({ ...base, name: 'DraftReply' }));
87
+ });
88
+
89
+ test('ActionFrontmatter: .strict rejects unknown top-level keys', () => {
90
+ assert.throws(() =>
91
+ ActionFrontmatter.parse({
92
+ name: 'x',
93
+ description: 'x',
94
+ domain: 'mail',
95
+ ux_mode: 'approve',
96
+ run: { kind: 'chat_prompt', prompt: 'x' },
97
+ bogus: true,
98
+ }),
99
+ );
100
+ });
101
+
102
+ test('ActionFrontmatter: setup block only valid on the setup action', () => {
103
+ // Non-setup action carrying a setup block → rejected.
104
+ assert.throws(() =>
105
+ ActionFrontmatter.parse({
106
+ name: 'sweep',
107
+ description: 'x',
108
+ domain: 'tasks',
109
+ ux_mode: 'approve',
110
+ run: { kind: 'chat_prompt', prompt: 'x' },
111
+ setup: { mode: 'interview', template_version: 1 },
112
+ }),
113
+ );
114
+ // setup action missing its setup block → rejected.
115
+ assert.throws(() =>
116
+ ActionFrontmatter.parse({
117
+ name: 'setup',
118
+ description: 'x',
119
+ domain: 'skill-core',
120
+ ux_mode: 'streaming',
121
+ run: { kind: 'chat_prompt', prompt: 'x' },
122
+ }),
123
+ );
124
+ // setup action with its setup block → ok.
125
+ const ok = ActionFrontmatter.parse({
126
+ name: 'setup',
127
+ description: 'x',
128
+ domain: 'skill-core',
129
+ ux_mode: 'streaming',
130
+ run: { kind: 'chat_prompt', prompt: 'x' },
131
+ setup: { mode: 'ai_infer', template_version: 1, infer_sources: ['README.md'] },
132
+ });
133
+ assert.equal(ok.setup?.mode, 'ai_infer');
134
+ });
135
+
136
+ test('ActionFrontmatter: depends_on refuses non-skill-core targets', () => {
137
+ assert.throws(() =>
138
+ ActionFrontmatter.parse({
139
+ name: 'x',
140
+ description: 'x',
141
+ domain: 'mail',
142
+ ux_mode: 'approve',
143
+ run: { kind: 'chat_prompt', prompt: 'x' },
144
+ depends_on: ['mail'],
145
+ }),
146
+ );
147
+ });
148
+
149
+ // ─── conformance: parse every real installed action file ────────────────────
150
+
151
+ /** Extract the YAML frontmatter block between the leading `---` fences.
152
+ * Mirrors the Rust `extract_frontmatter` in skill_actions.rs. Returns null
153
+ * when the file does not open with a `---` fence (prose-only skill docs). */
154
+ function extractFrontmatter(body: string): string | null {
155
+ const src = body.replace(/^/, '');
156
+ const lines = src.split('\n');
157
+ if (lines[0]?.trim() !== '---') return null;
158
+ const close = lines.slice(1).findIndex((l) => l.replace(/\r$/, '').trim() === '---');
159
+ if (close === -1) return null;
160
+ return lines.slice(1, 1 + close).join('\n');
161
+ }
162
+
163
+ function collectActionFiles(): string[] {
164
+ const here = dirname(fileURLToPath(import.meta.url));
165
+ // contract/src → ikenga-pkgs/packages/skills (sibling repos under the workspace).
166
+ const skillsRoot = join(here, '..', '..', 'ikenga-pkgs', 'packages', 'skills');
167
+ if (!existsSync(skillsRoot)) return [];
168
+ const out: string[] = [];
169
+ for (const domain of readdirSync(skillsRoot, { withFileTypes: true })) {
170
+ if (!domain.isDirectory()) continue;
171
+ const skillsDir = join(skillsRoot, domain.name, 'skills');
172
+ if (!existsSync(skillsDir)) continue;
173
+ for (const skill of readdirSync(skillsDir, { withFileTypes: true })) {
174
+ if (!skill.isDirectory()) continue;
175
+ const actionsDir = join(skillsDir, skill.name, 'actions');
176
+ if (!existsSync(actionsDir)) continue;
177
+ for (const f of readdirSync(actionsDir)) {
178
+ if (f.endsWith('.md') && f.toLowerCase() !== 'readme.md') {
179
+ out.push(join(actionsDir, f));
180
+ }
181
+ }
182
+ }
183
+ }
184
+ return out.sort();
185
+ }
186
+
187
+ test('conformance: every real action file with frontmatter parses against ActionFrontmatter', () => {
188
+ const files = collectActionFiles();
189
+ // The real action files live in the SIBLING ikenga-pkgs repo, present when the
190
+ // workspace is checked out whole (local dev) but absent in this repo's isolated
191
+ // CI checkout. Skip cleanly there — the schema unit tests above already cover
192
+ // the contract; this is the cross-repo integration layer, best-effort.
193
+ if (files.length === 0) {
194
+ console.error('# skip: ikenga-pkgs sibling repo not present (isolated CI checkout)');
195
+ return;
196
+ }
197
+
198
+ let parsed = 0;
199
+ const failures: string[] = [];
200
+ for (const file of files) {
201
+ const fm = extractFrontmatter(readFileSync(file, 'utf8'));
202
+ if (fm === null) continue; // prose-only skill doc (groundwork/contribute) — not a structured action
203
+ const doc = YAML.parse(fm);
204
+ const res = ActionFrontmatter.safeParse(doc);
205
+ if (!res.success) {
206
+ failures.push(`${file}\n ${JSON.stringify(res.error.issues, null, 2)}`);
207
+ } else {
208
+ parsed += 1;
209
+ }
210
+ }
211
+
212
+ assert.equal(
213
+ failures.length,
214
+ 0,
215
+ `real action files failed the schema:\n${failures.join('\n')}`,
216
+ );
217
+ // Well above the "at least 3" bar — the full Atelier surface is ~32 structured actions.
218
+ assert.ok(parsed >= 3, `expected >= 3 structured action files, parsed ${parsed}`);
219
+ });