@flowdular/sdk 0.1.0 → 0.2.1

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 (175) hide show
  1. package/.ai/skills/test-hardening/SKILL.md +1 -1
  2. package/README.md +69 -12
  3. package/assets/flowdular-banner.png +0 -0
  4. package/modules/agents/package.json +1 -1
  5. package/modules/auth/package.json +1 -1
  6. package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
  7. package/modules/auth/src/server/endpoints.ts +3 -1
  8. package/modules/auth/src/server/runtime.ts +4 -0
  9. package/modules/automations/package.json +1 -1
  10. package/modules/automations-workflows-integration/package.json +1 -1
  11. package/modules/profile/package.json +1 -1
  12. package/modules/sandbox/package.json +1 -1
  13. package/modules/system/package.json +1 -1
  14. package/modules/users/package.json +1 -1
  15. package/modules/workflows/package.json +1 -1
  16. package/package.json +5 -13
  17. package/packages/ai-provider/package.json +1 -1
  18. package/packages/cli-protocol/package.json +1 -1
  19. package/packages/client/package.json +1 -1
  20. package/packages/client/src/routing.ts +39 -2
  21. package/packages/client/src/shell/BrandMark.tsrx +5 -1
  22. package/packages/client/src/shell/navigation.ts +3 -1
  23. package/packages/client/src/state.ts +6 -2
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/contracts/schemas/project.schema.json +16 -42
  26. package/packages/database/package.json +1 -1
  27. package/packages/database-pglite/package.json +2 -2
  28. package/packages/database-pglite/src/driver.ts +21 -3
  29. package/packages/database-testing/package.json +1 -1
  30. package/packages/dev-console/package.json +1 -1
  31. package/packages/harness/package.json +1 -1
  32. package/packages/kernel/package.json +1 -1
  33. package/packages/server/package.json +1 -1
  34. package/packages/server/src/application-routes.ts +80 -8
  35. package/packages/server/src/index.ts +4 -1
  36. package/packages/server/src/web-html.ts +9 -5
  37. package/packages/server/src/web.ts +59 -7
  38. package/packages/ui/package.json +1 -1
  39. package/packages/coding-agent/package.json +0 -44
  40. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  41. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  42. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  43. package/packages/coding-agent/src/index.ts +0 -56
  44. package/packages/coding-agent/src/registry.ts +0 -119
  45. package/packages/coding-agent/src/roles/contract.ts +0 -60
  46. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  47. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  48. package/packages/coding-agent/src/roles/registry.ts +0 -156
  49. package/packages/coding-agent/src/roles/skills.ts +0 -72
  50. package/packages/coding-agent/src/roles/sync.ts +0 -88
  51. package/packages/coding-agent/src/types.ts +0 -102
  52. package/packages/coding-agent/src/workspace.ts +0 -275
  53. package/packages/sandbox/README.md +0 -691
  54. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  55. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  56. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  57. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  84. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  85. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  86. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  87. package/packages/sandbox/dist/client/favicon.svg +0 -1
  88. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  89. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  90. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  91. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  92. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  93. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  94. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  95. package/packages/sandbox/dist/server/entry.js +0 -91671
  96. package/packages/sandbox/dist/server/favicon.svg +0 -1
  97. package/packages/sandbox/dist/server/index.html +0 -161
  98. package/packages/sandbox/index.html +0 -159
  99. package/packages/sandbox/octane.config.ts +0 -36
  100. package/packages/sandbox/package.json +0 -83
  101. package/packages/sandbox/public/favicon.svg +0 -1
  102. package/packages/sandbox/public/og.png +0 -0
  103. package/packages/sandbox/src/App.tsrx +0 -924
  104. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  105. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  106. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  107. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  108. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  109. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  110. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  111. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  112. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  113. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  114. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  115. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  116. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  117. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  118. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  119. package/packages/sandbox/src/client/api.ts +0 -797
  120. package/packages/sandbox/src/client/attachments.ts +0 -73
  121. package/packages/sandbox/src/client/dashboard.ts +0 -46
  122. package/packages/sandbox/src/client/github-settings.ts +0 -48
  123. package/packages/sandbox/src/client/highlight.ts +0 -105
  124. package/packages/sandbox/src/client/i18n.ts +0 -32
  125. package/packages/sandbox/src/client/locales/en.json +0 -466
  126. package/packages/sandbox/src/client/locales/pl.json +0 -466
  127. package/packages/sandbox/src/client/session-labels.ts +0 -77
  128. package/packages/sandbox/src/client/state.ts +0 -108
  129. package/packages/sandbox/src/index.ts +0 -10
  130. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  131. package/packages/sandbox/src/preview/load-module.ts +0 -16
  132. package/packages/sandbox/src/server/attachments.ts +0 -308
  133. package/packages/sandbox/src/server/auto-review.ts +0 -181
  134. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  135. package/packages/sandbox/src/server/config.ts +0 -460
  136. package/packages/sandbox/src/server/dashboard.ts +0 -245
  137. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  138. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  139. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  140. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  141. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  142. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  143. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  144. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  145. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  146. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  147. package/packages/sandbox/src/server/dependencies.ts +0 -88
  148. package/packages/sandbox/src/server/diff.ts +0 -230
  149. package/packages/sandbox/src/server/gates.ts +0 -388
  150. package/packages/sandbox/src/server/index.ts +0 -139
  151. package/packages/sandbox/src/server/path-guard.ts +0 -282
  152. package/packages/sandbox/src/server/planning.ts +0 -749
  153. package/packages/sandbox/src/server/platform-client.ts +0 -189
  154. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  155. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  156. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  157. package/packages/sandbox/src/server/preview-database.ts +0 -109
  158. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  159. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  160. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  161. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  162. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  163. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  164. package/packages/sandbox/src/server/reference.ts +0 -251
  165. package/packages/sandbox/src/server/routes.ts +0 -2258
  166. package/packages/sandbox/src/server/runtime.ts +0 -188
  167. package/packages/sandbox/src/server/session-owner.ts +0 -23
  168. package/packages/sandbox/src/server/sessions.ts +0 -849
  169. package/packages/sandbox/src/server/spec.ts +0 -471
  170. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  171. package/packages/sandbox/src/server/turns.ts +0 -993
  172. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  173. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  174. package/packages/sandbox/src/styles.css +0 -1573
  175. package/packages/sandbox/vite.config.ts +0 -133
@@ -1,471 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import type { SessionModule } from './sessions.ts';
5
-
6
- export const SPEC_FILE = 'spec/module.yaml';
7
-
8
- export interface SpecDependency {
9
- readonly id: string;
10
- readonly range: string;
11
- }
12
-
13
- export interface SpecPermission {
14
- readonly id: string;
15
- readonly description: string;
16
- }
17
-
18
- export interface SpecScenario {
19
- readonly id: string;
20
- readonly given: string;
21
- readonly when: string;
22
- readonly then: string;
23
- }
24
-
25
- /* The part of packages/contracts/schemas/module-spec.schema.json the sandbox
26
- renders and diffs. The spec-schema gate stays the authority on validity; this
27
- reader only has to be honest about what it could read. */
28
- export interface ModuleSpecDocument {
29
- readonly id: string;
30
- readonly name: string;
31
- readonly description: string;
32
- readonly specVersion: string;
33
- readonly status: string;
34
- readonly profile: string;
35
- readonly tenancy: string;
36
- readonly capabilities: readonly string[];
37
- readonly locales: readonly string[];
38
- readonly dependencies: readonly SpecDependency[];
39
- readonly invariants: readonly string[];
40
- readonly dataOwnership: readonly string[];
41
- readonly permissions: readonly SpecPermission[];
42
- readonly acceptanceScenarios: readonly SpecScenario[];
43
- }
44
-
45
- export type SpecChangeKind = 'added' | 'removed' | 'changed';
46
-
47
- export interface SpecFieldChange {
48
- readonly field: string;
49
- readonly kind: SpecChangeKind;
50
- /* The entry inside a list field: a permission, dependency or scenario id, or
51
- the sentence itself for a list of sentences. Null for a scalar field. */
52
- readonly key: string | null;
53
- readonly before: string | null;
54
- readonly after: string | null;
55
- }
56
-
57
- export interface ModuleSpecReview {
58
- /* The draft module directory the specification belongs to. */
59
- readonly module: string;
60
- readonly moduleId: string;
61
- readonly kind: 'new' | 'edit';
62
- /* Workspace-relative, so the browser never sees a host path. */
63
- readonly path: string;
64
- readonly present: boolean;
65
- readonly status: string | null;
66
- /* Null when the module has no specification to gate on yet. */
67
- readonly approved: boolean | null;
68
- readonly approvedAt: number | null;
69
- /* True when the draft differs from the copy the session started from. */
70
- readonly changed: boolean;
71
- readonly draft: ModuleSpecDocument | null;
72
- readonly base: ModuleSpecDocument | null;
73
- readonly changes: readonly SpecFieldChange[];
74
- }
75
-
76
- /* A specification is a small document. These bounds keep a pasted or generated
77
- file from turning a session view into an unbounded parse. */
78
- const MAX_SPEC_BYTES = 256 * 1024;
79
- const MAX_ITEMS = 200;
80
- const MAX_VALUE = 2_000;
81
-
82
- const KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*):[ \t]*(.*)$/;
83
- const NESTED_KEY = /^[ \t]+([A-Za-z_][A-Za-z0-9_]*):[ \t]*(.*)$/;
84
- const BULLET = /^[ \t]*-[ \t]+(.*)$/;
85
- const STATUS_LINE = /^status:[ \t]*(\S+)[ \t]*$/m;
86
-
87
- export function specPathOf(modulePath: string): string {
88
- return join(modulePath, SPEC_FILE);
89
- }
90
-
91
- export async function readSpecText(modulePath: string): Promise<string | null> {
92
- try {
93
- const text = await readFile(specPathOf(modulePath), 'utf8');
94
- return text.length > MAX_SPEC_BYTES ? null : text;
95
- } catch {
96
- return null;
97
- }
98
- }
99
-
100
- export function hashSpec(text: string): string {
101
- return createHash('sha256').update(text, 'utf8').digest('hex');
102
- }
103
-
104
- export function statusOf(text: string): string | null {
105
- return STATUS_LINE.exec(text)?.[1] ?? null;
106
- }
107
-
108
- /* The gate. A recorded hash is the operator's decision on that exact text, so
109
- any later edit re-opens it. The status line is presentation of that decision,
110
- not authority: only the approval route may record a hash. This keeps an
111
- agent, a workbench paste, or an old copied `status: approved` line from
112
- unblocking implementation or delivery. */
113
- export function isSpecApproved(module: SessionModule, text: string): boolean {
114
- return module.specHash !== undefined && module.specHash === hashSpec(text);
115
- }
116
-
117
- export interface SpecGate {
118
- /* Null when the module has no specification file: there is nothing to
119
- approve, and the routing sends the turn to the business manager anyway. */
120
- readonly approved: boolean | null;
121
- /* True when the draft differs from the copy the session started from, so a
122
- change is on the table for the operator to review. */
123
- readonly changed: boolean;
124
- }
125
-
126
- export async function readSpecGate(
127
- module: SessionModule,
128
- modulePath: string,
129
- basePath: string,
130
- ): Promise<SpecGate> {
131
- const [draft, base] = await Promise.all([
132
- readSpecText(modulePath),
133
- readSpecText(basePath),
134
- ]);
135
- return {
136
- approved: draft === null ? null : isSpecApproved(module, draft),
137
- changed: draft !== null && draft !== base,
138
- };
139
- }
140
-
141
- function unquote(value: string): string {
142
- const trimmed = value.trim().slice(0, MAX_VALUE);
143
- if (trimmed.length < 2) return trimmed;
144
- const quote = trimmed[0];
145
- if (quote !== "'" && quote !== '"') return trimmed;
146
- if (trimmed.at(-1) !== quote) return trimmed;
147
- const inner = trimmed.slice(1, -1);
148
- return quote === "'"
149
- ? inner.replace(/''/g, "'")
150
- : inner.replace(/\\(["\\])/g, '$1');
151
- }
152
-
153
- /* A comment ends the line, unless the hash is inside a quoted scalar. */
154
- function stripComment(line: string): string {
155
- if (/^[ \t]*#/.test(line)) return '';
156
- let quote = '';
157
- for (let index = 0; index < line.length; index += 1) {
158
- const character = line[index]!;
159
- if (quote) {
160
- if (character === quote) quote = '';
161
- continue;
162
- }
163
- if (character === "'" || character === '"') {
164
- quote = character;
165
- continue;
166
- }
167
- if (character === '#' && /[ \t]/.test(line[index - 1] ?? '')) {
168
- return line.slice(0, index);
169
- }
170
- }
171
- return line;
172
- }
173
-
174
- interface RawSpec {
175
- readonly scalars: Map<string, string>;
176
- readonly lists: Map<string, string[]>;
177
- readonly records: Map<string, Record<string, string>[]>;
178
- }
179
-
180
- /* The keys whose items are objects. Everything else under a key is a sentence,
181
- so an unquoted sentence that happens to start with a word and a colon stays a
182
- sentence instead of turning into a record. */
183
- const OBJECT_FIELDS = new Set([
184
- 'dependencies',
185
- 'permissions',
186
- 'acceptanceScenarios',
187
- ]);
188
-
189
- /* The specification subset the schema allows: top-level scalars, lists of
190
- sentences, and lists of flat objects. Anything else is ignored rather than
191
- guessed at. */
192
- function readSpec(text: string): RawSpec | null {
193
- const scalars = new Map<string, string>();
194
- const lists = new Map<string, string[]>();
195
- const records = new Map<string, Record<string, string>[]>();
196
- let list: string[] | null = null;
197
- let record: Record<string, string>[] | null = null;
198
- let current: Record<string, string> | null = null;
199
- for (const raw of text.split('\n')) {
200
- const line = stripComment(raw);
201
- if (!line.trim()) continue;
202
- const bullet = BULLET.exec(line);
203
- if (bullet) {
204
- const content = bullet[1]!;
205
- const pair = KEY_LINE.exec(content);
206
- if (record && pair) {
207
- current = { [pair[1]!]: unquote(pair[2]!) };
208
- if (record.length < MAX_ITEMS) record.push(current);
209
- continue;
210
- }
211
- if (list && list.length < MAX_ITEMS) list.push(unquote(content));
212
- continue;
213
- }
214
- const nested = NESTED_KEY.exec(line);
215
- if (nested && current) {
216
- current[nested[1]!] = unquote(nested[2]!);
217
- continue;
218
- }
219
- const pair = KEY_LINE.exec(line);
220
- if (!pair) continue;
221
- const key = pair[1]!;
222
- const value = pair[2]!.trim();
223
- list = null;
224
- record = null;
225
- current = null;
226
- if (value) {
227
- scalars.set(key, unquote(value));
228
- continue;
229
- }
230
- if (OBJECT_FIELDS.has(key)) {
231
- record = [];
232
- records.set(key, record);
233
- continue;
234
- }
235
- list = [];
236
- lists.set(key, list);
237
- }
238
- if (scalars.size === 0 && lists.size === 0 && records.size === 0) return null;
239
- return { scalars, lists, records };
240
- }
241
-
242
- function scalar(raw: RawSpec, key: string): string {
243
- return raw.scalars.get(key) ?? '';
244
- }
245
-
246
- function sentences(raw: RawSpec, key: string): readonly string[] {
247
- return raw.lists.get(key)?.filter(Boolean) ?? [];
248
- }
249
-
250
- function objects(
251
- raw: RawSpec,
252
- key: string,
253
- required: readonly string[],
254
- ): readonly Record<string, string>[] {
255
- return (raw.records.get(key) ?? []).filter((entry) =>
256
- required.every((field) => (entry[field] ?? '').length > 0),
257
- );
258
- }
259
-
260
- export function parseModuleSpec(text: string): ModuleSpecDocument | null {
261
- const raw = readSpec(text);
262
- if (!raw) return null;
263
- return {
264
- id: scalar(raw, 'id'),
265
- name: scalar(raw, 'name'),
266
- description: scalar(raw, 'description'),
267
- specVersion: scalar(raw, 'specVersion'),
268
- status: scalar(raw, 'status'),
269
- profile: scalar(raw, 'profile'),
270
- tenancy: scalar(raw, 'tenancy'),
271
- capabilities: sentences(raw, 'capabilities'),
272
- locales: sentences(raw, 'locales'),
273
- dependencies: objects(raw, 'dependencies', ['id', 'range']).map(
274
- (entry) => ({ id: entry.id!, range: entry.range! }),
275
- ),
276
- invariants: sentences(raw, 'invariants'),
277
- dataOwnership: sentences(raw, 'dataOwnership'),
278
- permissions: objects(raw, 'permissions', ['id', 'description']).map(
279
- (entry) => ({ id: entry.id!, description: entry.description! }),
280
- ),
281
- acceptanceScenarios: objects(raw, 'acceptanceScenarios', [
282
- 'id',
283
- 'given',
284
- 'when',
285
- 'then',
286
- ]).map((entry) => ({
287
- id: entry.id!,
288
- given: entry.given!,
289
- when: entry.when!,
290
- then: entry.then!,
291
- })),
292
- };
293
- }
294
-
295
- export function scenarioText(scenario: SpecScenario): string {
296
- return `Given ${scenario.given} When ${scenario.when} Then ${scenario.then}`;
297
- }
298
-
299
- const SCALAR_FIELDS = [
300
- 'specVersion',
301
- 'status',
302
- 'name',
303
- 'description',
304
- 'profile',
305
- 'tenancy',
306
- 'id',
307
- ] as const;
308
-
309
- const SENTENCE_FIELDS = [
310
- 'capabilities',
311
- 'locales',
312
- 'invariants',
313
- 'dataOwnership',
314
- ] as const;
315
-
316
- function scalarChanges(
317
- base: ModuleSpecDocument,
318
- draft: ModuleSpecDocument,
319
- ): readonly SpecFieldChange[] {
320
- const changes: SpecFieldChange[] = [];
321
- for (const field of SCALAR_FIELDS) {
322
- const before = base[field];
323
- const after = draft[field];
324
- if (before === after) continue;
325
- changes.push({
326
- field,
327
- kind: !before ? 'added' : !after ? 'removed' : 'changed',
328
- key: null,
329
- before: before || null,
330
- after: after || null,
331
- });
332
- }
333
- return changes;
334
- }
335
-
336
- function sentenceChanges(
337
- field: string,
338
- before: readonly string[],
339
- after: readonly string[],
340
- ): readonly SpecFieldChange[] {
341
- const had = new Set(before);
342
- const has = new Set(after);
343
- return [
344
- ...after
345
- .filter((value) => !had.has(value))
346
- .map((value) => ({
347
- field,
348
- kind: 'added' as const,
349
- key: value,
350
- before: null,
351
- after: value,
352
- })),
353
- ...before
354
- .filter((value) => !has.has(value))
355
- .map((value) => ({
356
- field,
357
- kind: 'removed' as const,
358
- key: value,
359
- before: value,
360
- after: null,
361
- })),
362
- ];
363
- }
364
-
365
- /* A keyed list diffs by identity, so a reworded permission or scenario reads as
366
- one change instead of a removal plus an addition. */
367
- function keyedChanges<T>(
368
- field: string,
369
- before: readonly T[],
370
- after: readonly T[],
371
- keyOf: (entry: T) => string,
372
- valueOf: (entry: T) => string,
373
- ): readonly SpecFieldChange[] {
374
- const had = new Map(before.map((entry) => [keyOf(entry), valueOf(entry)]));
375
- const has = new Map(after.map((entry) => [keyOf(entry), valueOf(entry)]));
376
- const changes: SpecFieldChange[] = [];
377
- for (const [key, value] of has) {
378
- const previous = had.get(key);
379
- if (previous === undefined) {
380
- changes.push({
381
- field,
382
- kind: 'added',
383
- key,
384
- before: null,
385
- after: value,
386
- });
387
- continue;
388
- }
389
- if (previous !== value) {
390
- changes.push({
391
- field,
392
- kind: 'changed',
393
- key,
394
- before: previous,
395
- after: value,
396
- });
397
- }
398
- }
399
- for (const [key, value] of had) {
400
- if (has.has(key)) continue;
401
- changes.push({ field, kind: 'removed', key, before: value, after: null });
402
- }
403
- return changes;
404
- }
405
-
406
- /* What this session changed in the specification, field by field. The order is
407
- stable: scalars first, then the lists in the order the document declares
408
- them, additions before removals. */
409
- export function diffSpecs(
410
- base: ModuleSpecDocument | null,
411
- draft: ModuleSpecDocument | null,
412
- ): readonly SpecFieldChange[] {
413
- if (!draft || !base) return [];
414
- return [
415
- ...scalarChanges(base, draft),
416
- ...SENTENCE_FIELDS.flatMap((field) =>
417
- sentenceChanges(field, base[field], draft[field]),
418
- ),
419
- ...keyedChanges(
420
- 'dependencies',
421
- base.dependencies,
422
- draft.dependencies,
423
- (entry) => entry.id,
424
- (entry) => entry.range,
425
- ),
426
- ...keyedChanges(
427
- 'permissions',
428
- base.permissions,
429
- draft.permissions,
430
- (entry) => entry.id,
431
- (entry) => entry.description,
432
- ),
433
- ...keyedChanges(
434
- 'acceptanceScenarios',
435
- base.acceptanceScenarios,
436
- draft.acceptanceScenarios,
437
- (entry) => entry.id,
438
- scenarioText,
439
- ),
440
- ];
441
- }
442
-
443
- /* Everything the review card renders for one module, and the gate state the
444
- turn refuses on: the draft, the copy the session started from, and the
445
- difference between them. */
446
- export async function readModuleSpecReview(
447
- module: SessionModule,
448
- modulePath: string,
449
- basePath: string,
450
- ): Promise<ModuleSpecReview> {
451
- const [draftText, baseText] = await Promise.all([
452
- readSpecText(modulePath),
453
- readSpecText(basePath),
454
- ]);
455
- const draft = draftText === null ? null : parseModuleSpec(draftText);
456
- const base = baseText === null ? null : parseModuleSpec(baseText);
457
- return {
458
- module: module.directory,
459
- moduleId: module.id,
460
- kind: module.kind,
461
- path: `modules/${module.directory}/${SPEC_FILE}`,
462
- present: draftText !== null,
463
- status: draftText === null ? null : statusOf(draftText),
464
- approved: draftText === null ? null : isSpecApproved(module, draftText),
465
- approvedAt: module.specApprovedAt ?? null,
466
- changed: draftText !== null && draftText !== baseText,
467
- draft,
468
- base,
469
- changes: diffSpecs(base, draft),
470
- };
471
- }
@@ -1,56 +0,0 @@
1
- import { realpathSync } from 'node:fs';
2
- import { SandboxSetupError } from './workspace-root.ts';
3
-
4
- export type TurnSubscriber = (event: string, payload: unknown) => void;
5
-
6
- export interface TurnChannel {
7
- readonly controller: AbortController;
8
- readonly finished: Promise<void>;
9
- readonly subscribers: Set<TurnSubscriber>;
10
- }
11
-
12
- const SLOT = Symbol.for('flowdular.sandbox.turn-channels');
13
- type Channels = Map<string, Map<string, TurnChannel>>;
14
-
15
- /** Route HMR replaces closures, not the detached writers those closures own. */
16
- export function processTurnChannels(
17
- workspaceRoot: string,
18
- ): Map<string, TurnChannel> {
19
- const state = globalThis as unknown as Record<symbol, Channels | undefined>;
20
- const workspaces = (state[SLOT] ??= new Map());
21
- const root = realpathSync(workspaceRoot);
22
- let channels = workspaces.get(root);
23
- if (!channels) {
24
- channels = new Map();
25
- workspaces.set(root, channels);
26
- }
27
- return channels;
28
- }
29
-
30
- /** A deadline refuses the operation; it never releases the live writer. */
31
- export async function waitForTurn(
32
- channel: TurnChannel,
33
- timeoutMs = 20_000,
34
- ): Promise<void> {
35
- let timer: ReturnType<typeof setTimeout> | undefined;
36
- try {
37
- await Promise.race([
38
- channel.finished,
39
- new Promise<never>((_resolve, reject) => {
40
- timer = setTimeout(
41
- () =>
42
- reject(
43
- new SandboxSetupError(
44
- 'SESSION_RUNNING',
45
- 'The previous turn is still stopping. Wait for it to finish before changing this session.',
46
- ),
47
- ),
48
- timeoutMs,
49
- );
50
- timer.unref?.();
51
- }),
52
- ]);
53
- } finally {
54
- clearTimeout(timer);
55
- }
56
- }