@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.
- package/.ai/skills/test-hardening/SKILL.md +1 -1
- package/README.md +69 -12
- package/assets/flowdular-banner.png +0 -0
- package/modules/agents/package.json +1 -1
- package/modules/auth/package.json +1 -1
- package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
- package/modules/auth/src/server/endpoints.ts +3 -1
- package/modules/auth/src/server/runtime.ts +4 -0
- package/modules/automations/package.json +1 -1
- package/modules/automations-workflows-integration/package.json +1 -1
- package/modules/profile/package.json +1 -1
- package/modules/sandbox/package.json +1 -1
- package/modules/system/package.json +1 -1
- package/modules/users/package.json +1 -1
- package/modules/workflows/package.json +1 -1
- package/package.json +5 -13
- package/packages/ai-provider/package.json +1 -1
- package/packages/cli-protocol/package.json +1 -1
- package/packages/client/package.json +1 -1
- package/packages/client/src/routing.ts +39 -2
- package/packages/client/src/shell/BrandMark.tsrx +5 -1
- package/packages/client/src/shell/navigation.ts +3 -1
- package/packages/client/src/state.ts +6 -2
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/schemas/project.schema.json +16 -42
- package/packages/database/package.json +1 -1
- package/packages/database-pglite/package.json +2 -2
- package/packages/database-pglite/src/driver.ts +21 -3
- package/packages/database-testing/package.json +1 -1
- package/packages/dev-console/package.json +1 -1
- package/packages/harness/package.json +1 -1
- package/packages/kernel/package.json +1 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/application-routes.ts +80 -8
- package/packages/server/src/index.ts +4 -1
- package/packages/server/src/web-html.ts +9 -5
- package/packages/server/src/web.ts +59 -7
- package/packages/ui/package.json +1 -1
- package/packages/coding-agent/package.json +0 -44
- package/packages/coding-agent/src/drivers/byok.ts +0 -322
- package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
- package/packages/coding-agent/src/drivers/codex.ts +0 -281
- package/packages/coding-agent/src/index.ts +0 -56
- package/packages/coding-agent/src/registry.ts +0 -119
- package/packages/coding-agent/src/roles/contract.ts +0 -60
- package/packages/coding-agent/src/roles/defaults.ts +0 -134
- package/packages/coding-agent/src/roles/handoff.ts +0 -41
- package/packages/coding-agent/src/roles/registry.ts +0 -156
- package/packages/coding-agent/src/roles/skills.ts +0 -72
- package/packages/coding-agent/src/roles/sync.ts +0 -88
- package/packages/coding-agent/src/types.ts +0 -102
- package/packages/coding-agent/src/workspace.ts +0 -275
- package/packages/sandbox/README.md +0 -691
- package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
- package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
- package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
- package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
- package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
- package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
- package/packages/sandbox/dist/client/favicon.svg +0 -1
- package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
- package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
- package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
- package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
- package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
- package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
- package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
- package/packages/sandbox/dist/server/entry.js +0 -91671
- package/packages/sandbox/dist/server/favicon.svg +0 -1
- package/packages/sandbox/dist/server/index.html +0 -161
- package/packages/sandbox/index.html +0 -159
- package/packages/sandbox/octane.config.ts +0 -36
- package/packages/sandbox/package.json +0 -83
- package/packages/sandbox/public/favicon.svg +0 -1
- package/packages/sandbox/public/og.png +0 -0
- package/packages/sandbox/src/App.tsrx +0 -924
- package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
- package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
- package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
- package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
- package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
- package/packages/sandbox/src/client/GateResults.tsrx +0 -60
- package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
- package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
- package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
- package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
- package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
- package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
- package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
- package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
- package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
- package/packages/sandbox/src/client/api.ts +0 -797
- package/packages/sandbox/src/client/attachments.ts +0 -73
- package/packages/sandbox/src/client/dashboard.ts +0 -46
- package/packages/sandbox/src/client/github-settings.ts +0 -48
- package/packages/sandbox/src/client/highlight.ts +0 -105
- package/packages/sandbox/src/client/i18n.ts +0 -32
- package/packages/sandbox/src/client/locales/en.json +0 -466
- package/packages/sandbox/src/client/locales/pl.json +0 -466
- package/packages/sandbox/src/client/session-labels.ts +0 -77
- package/packages/sandbox/src/client/state.ts +0 -108
- package/packages/sandbox/src/index.ts +0 -10
- package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
- package/packages/sandbox/src/preview/load-module.ts +0 -16
- package/packages/sandbox/src/server/attachments.ts +0 -308
- package/packages/sandbox/src/server/auto-review.ts +0 -181
- package/packages/sandbox/src/server/checkpoints.ts +0 -90
- package/packages/sandbox/src/server/config.ts +0 -460
- package/packages/sandbox/src/server/dashboard.ts +0 -245
- package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
- package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
- package/packages/sandbox/src/server/delivery/index.ts +0 -83
- package/packages/sandbox/src/server/delivery/local.ts +0 -151
- package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
- package/packages/sandbox/src/server/delivery/plan.ts +0 -218
- package/packages/sandbox/src/server/delivery/policies.ts +0 -239
- package/packages/sandbox/src/server/delivery/record.ts +0 -41
- package/packages/sandbox/src/server/delivery/steps.ts +0 -464
- package/packages/sandbox/src/server/delivery/types.ts +0 -158
- package/packages/sandbox/src/server/dependencies.ts +0 -88
- package/packages/sandbox/src/server/diff.ts +0 -230
- package/packages/sandbox/src/server/gates.ts +0 -388
- package/packages/sandbox/src/server/index.ts +0 -139
- package/packages/sandbox/src/server/path-guard.ts +0 -282
- package/packages/sandbox/src/server/planning.ts +0 -749
- package/packages/sandbox/src/server/platform-client.ts +0 -189
- package/packages/sandbox/src/server/preview-database-host.ts +0 -340
- package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
- package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
- package/packages/sandbox/src/server/preview-database.ts +0 -109
- package/packages/sandbox/src/server/preview-ipc.ts +0 -15
- package/packages/sandbox/src/server/preview-modules.ts +0 -83
- package/packages/sandbox/src/server/preview-revision.ts +0 -44
- package/packages/sandbox/src/server/preview-runtime.ts +0 -467
- package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
- package/packages/sandbox/src/server/preview-worker.ts +0 -168
- package/packages/sandbox/src/server/reference.ts +0 -251
- package/packages/sandbox/src/server/routes.ts +0 -2258
- package/packages/sandbox/src/server/runtime.ts +0 -188
- package/packages/sandbox/src/server/session-owner.ts +0 -23
- package/packages/sandbox/src/server/sessions.ts +0 -849
- package/packages/sandbox/src/server/spec.ts +0 -471
- package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
- package/packages/sandbox/src/server/turns.ts +0 -993
- package/packages/sandbox/src/server/workspace-install.ts +0 -266
- package/packages/sandbox/src/server/workspace-root.ts +0 -73
- package/packages/sandbox/src/styles.css +0 -1573
- 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
|
-
}
|