@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,218 +0,0 @@
1
- import { join, relative } from 'node:path';
2
- import {
3
- basePathOf,
4
- modulePathOf,
5
- sessionPaths,
6
- type SessionModule,
7
- } from '../sessions.ts';
8
- import {
9
- diffSpecs,
10
- isSpecApproved,
11
- parseModuleSpec,
12
- readSpecText,
13
- SPEC_FILE,
14
- } from '../spec.ts';
15
- import { SandboxSetupError } from '../workspace-root.ts';
16
- import { newPackages, planModuleFiles } from './steps.ts';
17
- import type { DeliveryContext, DeliveryModulePlan } from './types.ts';
18
-
19
- export const GATES = [
20
- 'spec-schema',
21
- 'module-schema',
22
- 'dependencies',
23
- 'typecheck',
24
- 'tests',
25
- 'format',
26
- 'auto-review',
27
- ] as const;
28
-
29
- const NUMBERED_MIGRATION = /^migrations\/\d{4}_[^/]+\.(?:up|down)\.sql$/;
30
-
31
- function assertExistingMigrationsUnchanged(
32
- module: SessionModule,
33
- overwrites: readonly string[],
34
- removes: readonly string[],
35
- ): void {
36
- const changed = [...overwrites, ...removes].filter((file) =>
37
- NUMBERED_MIGRATION.test(file),
38
- );
39
- if (changed.length === 0) return;
40
- throw new SandboxSetupError(
41
- 'EJECT_MIGRATION_IMMUTABLE',
42
- `${module.id} changes an existing migration (${changed.join(', ')}). Restore the original bytes and add a new numbered migration instead.`,
43
- );
44
- }
45
-
46
- export function assertEjectCapability(context: DeliveryContext): void {
47
- if (!context.capabilities.includes('sandbox.modules.eject')) {
48
- throw new SandboxSetupError(
49
- 'EJECT_SCOPE_MISSING',
50
- 'The sandbox grant does not include sandbox.modules.eject.',
51
- );
52
- }
53
- }
54
-
55
- /* A change that landed without a specification behind it is how a module and
56
- its contract drift apart. Before an edited module is delivered its
57
- specification has to cover the change: the version moves, and at least one
58
- acceptance scenario is new or different. A module whose base copy carries no
59
- specification has nothing to compare, so it is left alone. */
60
- async function assertSpecCoversChange(
61
- module: SessionModule,
62
- sourcePath: string,
63
- basePath: string,
64
- changedFiles: number,
65
- ): Promise<void> {
66
- const specPath = `modules/${module.directory}/${SPEC_FILE}`;
67
- const baseText = await readSpecText(basePath);
68
- if (baseText === null) return;
69
- const draftText = await readSpecText(sourcePath);
70
- if (draftText === null) {
71
- throw new SandboxSetupError(
72
- 'EJECT_SPEC_MISSING',
73
- `${module.id} changed, but ${specPath} is gone from the session. Restore it before delivering.`,
74
- );
75
- }
76
- const changes = diffSpecs(
77
- parseModuleSpec(baseText),
78
- parseModuleSpec(draftText),
79
- );
80
- const reasons: string[] = [];
81
- if (!changes.some((change) => change.field === 'specVersion')) {
82
- reasons.push(
83
- `specVersion is unchanged (bump it: patch for a fix, minor for an endpoint, table, column, screen, widget or permission)`,
84
- );
85
- }
86
- if (
87
- !changes.some(
88
- (change) =>
89
- change.field === 'acceptanceScenarios' && change.kind !== 'removed',
90
- )
91
- ) {
92
- reasons.push(
93
- 'no acceptance scenario was added or changed (state the new behaviour as given, when, then)',
94
- );
95
- }
96
- if (reasons.length === 0) return;
97
- throw new SandboxSetupError(
98
- reasons[0]!.startsWith('specVersion')
99
- ? 'EJECT_SPEC_VERSION_UNCHANGED'
100
- : 'EJECT_SPEC_SCENARIOS_UNCHANGED',
101
- `${module.id} changed ${changedFiles} ${
102
- changedFiles === 1 ? 'file' : 'files'
103
- }, but ${specPath} does not cover the change: ${reasons.join(
104
- ', and ',
105
- )}. Ask the business manager for the delta, approve it, then deliver again.`,
106
- );
107
- }
108
-
109
- /* Delivery repeats the turn gate. A plan can be opened hours after a turn, and
110
- both local eject and the pull-request target use this function, so this is
111
- the last shared boundary before either target writes or pushes anything. */
112
- async function assertSpecApproved(
113
- module: SessionModule,
114
- sourcePath: string,
115
- ): Promise<void> {
116
- const text = await readSpecText(sourcePath);
117
- const specPath = `modules/${module.directory}/${SPEC_FILE}`;
118
- if (text === null) {
119
- throw new SandboxSetupError(
120
- 'EJECT_SPEC_MISSING',
121
- `${module.id} has no ${specPath} to approve before delivery.`,
122
- );
123
- }
124
- if (!isSpecApproved(module, text)) {
125
- throw new SandboxSetupError(
126
- 'EJECT_SPEC_NOT_APPROVED',
127
- `${module.id} has a draft or stale specification. Review and approve the current ${specPath} before delivery.`,
128
- );
129
- }
130
- }
131
-
132
- /* What every module of the session changes in modules/ of this workspace. Both
133
- targets start from this; only where the files go differs. */
134
- export async function planSessionModules(
135
- context: DeliveryContext,
136
- ): Promise<readonly DeliveryModulePlan[]> {
137
- const paths = sessionPaths(
138
- context.workspaceRoot,
139
- context.session.id,
140
- context.session.moduleSuffix,
141
- );
142
- const modules: DeliveryModulePlan[] = [];
143
- for (const module of context.session.modules) {
144
- const sourcePath = modulePathOf(paths, module.directory);
145
- const targetPath = join(context.workspaceRoot, 'modules', module.directory);
146
- await assertSpecApproved(module, sourcePath);
147
- const changes = await planModuleFiles(
148
- sourcePath,
149
- targetPath,
150
- module.kind === 'edit' ? basePathOf(paths, module.directory) : null,
151
- );
152
- if (changes.files.length === 0) {
153
- throw new SandboxSetupError(
154
- 'EJECT_EMPTY',
155
- `The session workspace holds no files for ${module.id} yet.`,
156
- );
157
- }
158
- assertExistingMigrationsUnchanged(
159
- module,
160
- changes.overwrites,
161
- changes.removes,
162
- );
163
- const changedFiles =
164
- changes.additions.length +
165
- changes.overwrites.length +
166
- changes.removes.length;
167
- if (module.kind === 'edit' && changedFiles > 0) {
168
- await assertSpecCoversChange(
169
- module,
170
- sourcePath,
171
- basePathOf(paths, module.directory),
172
- changedFiles,
173
- );
174
- }
175
- modules.push({
176
- id: module.id,
177
- directory: module.directory,
178
- kind: module.kind,
179
- targetPath: relative(context.workspaceRoot, targetPath),
180
- ...changes,
181
- newPackages: await newPackages(
182
- context.workspaceRoot,
183
- sourcePath,
184
- targetPath,
185
- ),
186
- enable: module.kind === 'new',
187
- });
188
- }
189
- return modules;
190
- }
191
-
192
- /* Every draft file plus every removal, as the workspace target reports it. */
193
- export function countDeliveredFiles(
194
- modules: readonly DeliveryModulePlan[],
195
- ): number {
196
- return modules.reduce(
197
- (total, module) =>
198
- total +
199
- module.files.filter((file) => !module.overwrites.includes(file)).length +
200
- module.overwrites.length +
201
- module.removes.length,
202
- 0,
203
- );
204
- }
205
-
206
- /* Only what differs from the target: new files, changed files, removals. */
207
- export function countChangedFiles(
208
- modules: readonly DeliveryModulePlan[],
209
- ): number {
210
- return modules.reduce(
211
- (total, module) =>
212
- total +
213
- module.additions.length +
214
- module.overwrites.length +
215
- module.removes.length,
216
- 0,
217
- );
218
- }
@@ -1,239 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import type { DeliveryBudget } from './types.ts';
4
-
5
- export type PolicyValue =
6
- | string
7
- | number
8
- | boolean
9
- | null
10
- | readonly string[]
11
- | PolicyMap;
12
-
13
- export interface PolicyMap {
14
- readonly [key: string]: PolicyValue;
15
- }
16
-
17
- interface Line {
18
- readonly indent: number;
19
- readonly text: string;
20
- }
21
-
22
- function scalar(raw: string): PolicyValue {
23
- const value = raw.trim();
24
- if (value === '') return null;
25
- if (value === 'true') return true;
26
- if (value === 'false') return false;
27
- if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
28
- if (value.startsWith('[') && value.endsWith(']')) {
29
- return value
30
- .slice(1, -1)
31
- .split(',')
32
- .map((item) => unquote(item.trim()))
33
- .filter((item) => item !== '');
34
- }
35
- return unquote(value);
36
- }
37
-
38
- function unquote(value: string): string {
39
- if (
40
- (value.startsWith('"') && value.endsWith('"')) ||
41
- (value.startsWith("'") && value.endsWith("'"))
42
- ) {
43
- return value.slice(1, -1);
44
- }
45
- return value;
46
- }
47
-
48
- function stripComment(line: string): string {
49
- const hash = line.search(/(^|\s)#/);
50
- return hash < 0 ? line : line.slice(0, hash);
51
- }
52
-
53
- function splitKey(text: string): { key: string; rest: string } | null {
54
- if (text.endsWith(':')) return { key: text.slice(0, -1).trim(), rest: '' };
55
- const separator = text.indexOf(': ');
56
- if (separator < 0) return null;
57
- return {
58
- key: text.slice(0, separator).trim(),
59
- rest: text.slice(separator + 2).trim(),
60
- };
61
- }
62
-
63
- /* Reads the subset of YAML the policy files use: nested mappings, scalars,
64
- flow sequences (also one that continues on the next lines), and block
65
- sequences of scalars. Anchors, multi-line strings, and nested sequences are
66
- out of scope; the files are reviewed by hand and stay this simple. */
67
- export function parsePolicyYaml(text: string): PolicyMap {
68
- const lines: Line[] = [];
69
- for (const raw of text.split('\n')) {
70
- const stripped = stripComment(raw).replace(/\s+$/, '');
71
- if (stripped.trim() === '') continue;
72
- lines.push({
73
- indent: stripped.length - stripped.trimStart().length,
74
- text: stripped.trim(),
75
- });
76
- }
77
- let index = 0;
78
-
79
- function readMap(indent: number): PolicyMap {
80
- const map: Record<string, PolicyValue> = {};
81
- while (index < lines.length) {
82
- const line = lines[index]!;
83
- if (line.indent < indent) break;
84
- if (line.indent > indent || line.text.startsWith('- ')) {
85
- index += 1;
86
- continue;
87
- }
88
- const entry = splitKey(line.text);
89
- index += 1;
90
- if (!entry) continue;
91
- if (entry.rest !== '') {
92
- map[entry.key] = readFlow(entry.rest);
93
- continue;
94
- }
95
- const next = lines[index];
96
- if (!next || next.indent <= indent) {
97
- map[entry.key] = null;
98
- } else if (next.text.startsWith('- ')) {
99
- map[entry.key] = readSequence(next.indent);
100
- } else if (next.text.startsWith('[')) {
101
- index += 1;
102
- map[entry.key] = readFlow(next.text);
103
- } else {
104
- map[entry.key] = readMap(next.indent);
105
- }
106
- }
107
- return map;
108
- }
109
-
110
- function readFlow(start: string): PolicyValue {
111
- let value = start;
112
- while (
113
- value.startsWith('[') &&
114
- !value.endsWith(']') &&
115
- index < lines.length
116
- ) {
117
- value = `${value} ${lines[index]!.text}`;
118
- index += 1;
119
- }
120
- return scalar(value);
121
- }
122
-
123
- function readSequence(indent: number): readonly string[] {
124
- const items: string[] = [];
125
- while (index < lines.length) {
126
- const line = lines[index]!;
127
- if (line.indent !== indent || !line.text.startsWith('- ')) break;
128
- items.push(unquote(line.text.slice(2).trim()));
129
- index += 1;
130
- }
131
- return items;
132
- }
133
-
134
- return readMap(lines[0]?.indent ?? 0);
135
- }
136
-
137
- async function readPolicy(root: string, file: string): Promise<PolicyMap> {
138
- try {
139
- return parsePolicyYaml(
140
- await readFile(join(root, '.ai', 'policies', file), 'utf8'),
141
- );
142
- } catch {
143
- return {};
144
- }
145
- }
146
-
147
- function asMap(value: PolicyValue | undefined): PolicyMap {
148
- return typeof value === 'object' && value !== null && !Array.isArray(value)
149
- ? (value as PolicyMap)
150
- : {};
151
- }
152
-
153
- function asInteger(value: PolicyValue | undefined): number | undefined {
154
- return typeof value === 'number' && Number.isInteger(value) && value >= 0
155
- ? value
156
- : undefined;
157
- }
158
-
159
- export interface PathOwner {
160
- readonly pattern: string;
161
- readonly owner: string;
162
- }
163
-
164
- export interface PathOwnership {
165
- /* In file order; the first matching pattern answers for a path. */
166
- readonly owners: readonly PathOwner[];
167
- readonly requireReviewer: boolean;
168
- }
169
-
170
- export async function loadPathOwnership(root: string): Promise<PathOwnership> {
171
- const policy = await readPolicy(root, 'path-ownership.yaml');
172
- const owners: PathOwner[] = [];
173
- for (const [pattern, owner] of Object.entries(asMap(policy.owners))) {
174
- if (typeof owner === 'string') owners.push({ pattern, owner });
175
- }
176
- return {
177
- owners,
178
- requireReviewer: asMap(policy.crossOwnerChanges).requireReviewer === true,
179
- };
180
- }
181
-
182
- export interface TaskBudgets {
183
- readonly defaults: Partial<DeliveryBudget>;
184
- readonly overrides: Readonly<Record<string, Partial<DeliveryBudget>>>;
185
- }
186
-
187
- function budgetOf(value: PolicyValue | undefined): Partial<DeliveryBudget> {
188
- const map = asMap(value);
189
- const maxChangedFiles = asInteger(map.maxChangedFiles);
190
- const maxNewDependencies = asInteger(map.maxNewDependencies);
191
- return {
192
- ...(maxChangedFiles !== undefined ? { maxChangedFiles } : {}),
193
- ...(maxNewDependencies !== undefined ? { maxNewDependencies } : {}),
194
- };
195
- }
196
-
197
- export async function loadTaskBudgets(root: string): Promise<TaskBudgets> {
198
- const policy = await readPolicy(root, 'task-budgets.yaml');
199
- const overrides: Record<string, Partial<DeliveryBudget>> = {};
200
- for (const [kind, value] of Object.entries(asMap(policy.overrides))) {
201
- overrides[kind] = budgetOf(value);
202
- }
203
- return { defaults: budgetOf(policy.defaults), overrides };
204
- }
205
-
206
- const patternCache = new Map<string, RegExp>();
207
-
208
- /* Globs as the policy files write them: `**` spans directories, `*` stays in
209
- one segment, `{module}` binds one segment. */
210
- function patternRegExp(pattern: string): RegExp {
211
- let regExp = patternCache.get(pattern);
212
- if (regExp) return regExp;
213
- const source = pattern
214
- .split(/(\*\*|\*|\{module\})/)
215
- .map((part) => {
216
- if (part === '**') return '.*';
217
- if (part === '*') return '[^/]*';
218
- if (part === '{module}') return '([^/]+)';
219
- return part.replace(/[.+?^$()|[\]\\]/g, '\\$&');
220
- })
221
- .join('');
222
- regExp = new RegExp(`^${source}$`);
223
- if (patternCache.size >= 256) patternCache.clear();
224
- patternCache.set(pattern, regExp);
225
- return regExp;
226
- }
227
-
228
- export function matchesPath(path: string, pattern: string): boolean {
229
- return patternRegExp(pattern).test(path);
230
- }
231
-
232
- export function ownerOf(path: string, ownership: PathOwnership): string | null {
233
- for (const { pattern, owner } of ownership.owners) {
234
- const match = patternRegExp(pattern).exec(path);
235
- if (!match) continue;
236
- return match[1] ? owner.replace('{module}', match[1]) : owner;
237
- }
238
- return null;
239
- }
@@ -1,41 +0,0 @@
1
- import { readFile, rename, writeFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import type { EjectTarget } from './types.ts';
4
-
5
- /* What a delivery left behind that the session record does not carry: the
6
- branch and the pull request, so a session can be reopened to its review. */
7
- export interface DeliveryRecord {
8
- readonly target: EjectTarget;
9
- readonly deliveredAt: number;
10
- readonly modules: readonly string[];
11
- readonly branch: string | null;
12
- readonly pullRequestUrl: string | null;
13
- readonly compareUrl: string | null;
14
- }
15
-
16
- function recordPath(sessionRoot: string): string {
17
- return join(sessionRoot, 'delivery.json');
18
- }
19
-
20
- export async function writeDeliveryRecord(
21
- sessionRoot: string,
22
- record: DeliveryRecord,
23
- ): Promise<DeliveryRecord> {
24
- const path = recordPath(sessionRoot);
25
- const staging = `${path}.${process.pid}`;
26
- await writeFile(staging, `${JSON.stringify(record, null, '\t')}\n`, 'utf8');
27
- await rename(staging, path);
28
- return record;
29
- }
30
-
31
- export async function readDeliveryRecord(
32
- sessionRoot: string,
33
- ): Promise<DeliveryRecord | null> {
34
- try {
35
- return JSON.parse(
36
- await readFile(recordPath(sessionRoot), 'utf8'),
37
- ) as DeliveryRecord;
38
- } catch {
39
- return null;
40
- }
41
- }