@flowdular/sdk 0.1.0 → 0.2.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 (174) 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 +1 -1
  28. package/packages/database-testing/package.json +1 -1
  29. package/packages/dev-console/package.json +1 -1
  30. package/packages/harness/package.json +1 -1
  31. package/packages/kernel/package.json +1 -1
  32. package/packages/server/package.json +1 -1
  33. package/packages/server/src/application-routes.ts +80 -8
  34. package/packages/server/src/index.ts +4 -1
  35. package/packages/server/src/web-html.ts +9 -5
  36. package/packages/server/src/web.ts +59 -7
  37. package/packages/ui/package.json +1 -1
  38. package/packages/coding-agent/package.json +0 -44
  39. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  40. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  41. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  42. package/packages/coding-agent/src/index.ts +0 -56
  43. package/packages/coding-agent/src/registry.ts +0 -119
  44. package/packages/coding-agent/src/roles/contract.ts +0 -60
  45. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  46. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  47. package/packages/coding-agent/src/roles/registry.ts +0 -156
  48. package/packages/coding-agent/src/roles/skills.ts +0 -72
  49. package/packages/coding-agent/src/roles/sync.ts +0 -88
  50. package/packages/coding-agent/src/types.ts +0 -102
  51. package/packages/coding-agent/src/workspace.ts +0 -275
  52. package/packages/sandbox/README.md +0 -691
  53. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  54. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  55. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  56. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  57. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  84. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  85. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  86. package/packages/sandbox/dist/client/favicon.svg +0 -1
  87. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  88. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  89. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  90. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  91. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  92. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  93. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  94. package/packages/sandbox/dist/server/entry.js +0 -91671
  95. package/packages/sandbox/dist/server/favicon.svg +0 -1
  96. package/packages/sandbox/dist/server/index.html +0 -161
  97. package/packages/sandbox/index.html +0 -159
  98. package/packages/sandbox/octane.config.ts +0 -36
  99. package/packages/sandbox/package.json +0 -83
  100. package/packages/sandbox/public/favicon.svg +0 -1
  101. package/packages/sandbox/public/og.png +0 -0
  102. package/packages/sandbox/src/App.tsrx +0 -924
  103. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  104. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  105. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  106. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  107. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  108. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  109. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  110. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  111. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  112. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  113. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  114. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  115. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  116. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  117. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  118. package/packages/sandbox/src/client/api.ts +0 -797
  119. package/packages/sandbox/src/client/attachments.ts +0 -73
  120. package/packages/sandbox/src/client/dashboard.ts +0 -46
  121. package/packages/sandbox/src/client/github-settings.ts +0 -48
  122. package/packages/sandbox/src/client/highlight.ts +0 -105
  123. package/packages/sandbox/src/client/i18n.ts +0 -32
  124. package/packages/sandbox/src/client/locales/en.json +0 -466
  125. package/packages/sandbox/src/client/locales/pl.json +0 -466
  126. package/packages/sandbox/src/client/session-labels.ts +0 -77
  127. package/packages/sandbox/src/client/state.ts +0 -108
  128. package/packages/sandbox/src/index.ts +0 -10
  129. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  130. package/packages/sandbox/src/preview/load-module.ts +0 -16
  131. package/packages/sandbox/src/server/attachments.ts +0 -308
  132. package/packages/sandbox/src/server/auto-review.ts +0 -181
  133. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  134. package/packages/sandbox/src/server/config.ts +0 -460
  135. package/packages/sandbox/src/server/dashboard.ts +0 -245
  136. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  137. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  138. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  139. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  140. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  141. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  142. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  143. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  144. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  145. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  146. package/packages/sandbox/src/server/dependencies.ts +0 -88
  147. package/packages/sandbox/src/server/diff.ts +0 -230
  148. package/packages/sandbox/src/server/gates.ts +0 -388
  149. package/packages/sandbox/src/server/index.ts +0 -139
  150. package/packages/sandbox/src/server/path-guard.ts +0 -282
  151. package/packages/sandbox/src/server/planning.ts +0 -749
  152. package/packages/sandbox/src/server/platform-client.ts +0 -189
  153. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  154. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  155. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  156. package/packages/sandbox/src/server/preview-database.ts +0 -109
  157. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  158. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  159. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  160. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  161. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  162. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  163. package/packages/sandbox/src/server/reference.ts +0 -251
  164. package/packages/sandbox/src/server/routes.ts +0 -2258
  165. package/packages/sandbox/src/server/runtime.ts +0 -188
  166. package/packages/sandbox/src/server/session-owner.ts +0 -23
  167. package/packages/sandbox/src/server/sessions.ts +0 -849
  168. package/packages/sandbox/src/server/spec.ts +0 -471
  169. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  170. package/packages/sandbox/src/server/turns.ts +0 -993
  171. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  172. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  173. package/packages/sandbox/src/styles.css +0 -1573
  174. 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
- }