@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,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
|
-
}
|