engineering-memory 1.10.2 → 1.10.4

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.
@@ -0,0 +1,663 @@
1
+ import * as z from 'zod/v4';
2
+ import { sha256, stableStringify } from '../utilities/hash.js';
3
+ import { askQuestionnaire } from './questionnaire-tools.js';
4
+ const uuid = z.string().uuid();
5
+ const hash = z.string().regex(/^[a-f0-9]{64}$/);
6
+ const project = { projectId: uuid };
7
+ const run = { ...project, runId: uuid };
8
+ const page = { afterId: uuid.optional(), limit: z.number().int().min(1).max(100).optional() };
9
+ const repoRoot = z.string().min(1);
10
+ const json = z.lazy(() => z.union([
11
+ z.string(),
12
+ z.number(),
13
+ z.boolean(),
14
+ z.null(),
15
+ z.array(json),
16
+ z.record(z.string(), json),
17
+ ]));
18
+ const object = z.record(z.string(), json);
19
+ const reference = z.object({
20
+ proposalId: uuid,
21
+ contentHash: hash,
22
+ baseRevision: z.number().int().min(0),
23
+ });
24
+ const kind = z.enum([
25
+ 'engineering_rule',
26
+ 'project_profile',
27
+ 'service_contract',
28
+ 'localization_contract',
29
+ 'navigation_contract',
30
+ 'state_contract',
31
+ 'screen_logic',
32
+ 'flow_logic',
33
+ 'module_logic',
34
+ 'data_model',
35
+ 'api_endpoint',
36
+ 'component_mapping',
37
+ 'figma_mapping',
38
+ 'figma_reference',
39
+ 'current_deviation',
40
+ 'quality_gate',
41
+ 'architecture_template',
42
+ ]);
43
+ const output = (result) => ({
44
+ content: [{ type: 'text', text: JSON.stringify(result) }],
45
+ ...(!result.ok ? { isError: true } : {}),
46
+ });
47
+ const body = (value) => JSON.parse(JSON.stringify(value));
48
+ const onboardingMode = z.enum(['greenfield', 'adopt', 'rework', 'refresh']);
49
+ const profile = z
50
+ .strictObject({
51
+ stack: z.enum(['Flutter', 'React', 'Nest', 'Spring']),
52
+ runtime: z.string().min(1).max(160),
53
+ architecture: z.string().min(1).max(240),
54
+ serviceReadiness: z.string().min(1).max(160),
55
+ apiContract: z.string().min(1).max(160),
56
+ authentication: z.string().min(1).max(160),
57
+ storage: z.string().min(1).max(160),
58
+ environments: z.string().min(1).max(160),
59
+ backendProjectIds: z
60
+ .array(uuid)
61
+ .max(10)
62
+ .refine((ids) => new Set(ids).size === ids.length),
63
+ figma: z.string().min(1).max(160).optional(),
64
+ colorPalette: z.string().min(1).max(240).optional(),
65
+ typography: z.string().min(1).max(240).optional(),
66
+ designSystem: z.string().min(1).max(160).optional(),
67
+ navigation: z.string().min(1).max(160).optional(),
68
+ platforms: z.string().min(1).max(160).optional(),
69
+ localization: z.string().min(1).max(160).optional(),
70
+ migrations: z.string().min(1).max(160).optional(),
71
+ })
72
+ .superRefine((value, context) => {
73
+ const required = ['Flutter', 'React'].includes(value.stack)
74
+ ? [
75
+ 'figma',
76
+ 'colorPalette',
77
+ 'typography',
78
+ 'designSystem',
79
+ 'navigation',
80
+ 'platforms',
81
+ 'localization',
82
+ ]
83
+ : ['migrations'];
84
+ for (const key of required)
85
+ if (!value[key])
86
+ context.addIssue({
87
+ code: 'custom',
88
+ path: [key],
89
+ message: 'Collect this choice through the native onboarding form. An explicit absence or deferral must be stated.',
90
+ });
91
+ });
92
+ const onboardingInput = z.strictObject({
93
+ repoRoot,
94
+ requestKey: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/),
95
+ mode: onboardingMode,
96
+ profile,
97
+ projectId: uuid.optional(),
98
+ reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
99
+ });
100
+ export function registerOnboardingTools(server, service) {
101
+ server.registerTool('project.inspect', {
102
+ description: 'Read-only maturity evidence for missing, empty, starter or established projects. Inspect architecture, state, services, network and persistence as well as screens; recommendations never override the user. Follow with a native mode questionnaire.',
103
+ inputSchema: z.object({ repoRoot }),
104
+ }, async (input) => output(await service.projectInspect(input)));
105
+ server.registerTool('project.onboard', {
106
+ description: 'Choose greenfield, adopt, rework or refresh through durable native forms, then approve a stack-specific profile: palette, fonts/text styles, Figma, navigation, ready services and accessible backend links, or backend API/storage/migrations/environment choices. The agent proposes values from evidence and user answers; never invent defaults. The exact choice persists outside the repository and, with projectId, in the project onboarding record. This does not activate an engineering-memory revision.',
107
+ inputSchema: onboardingInput,
108
+ }, async (input, context) => approveOnboarding(server, service, input, context, false));
109
+ server.registerTool('project.initialize', {
110
+ description: 'Choose the onboarding mode and profile through native forms before creating a folder or Git. Only an approved greenfield choice initializes Git; preserves existing files, including barely-started projects. Other choices return without changing source. Use the exact same requestKey/profile for retries.',
111
+ inputSchema: onboardingInput,
112
+ }, async (input, context) => approveOnboarding(server, service, input, context, true));
113
+ server.registerTool('project.onboarding_status', {
114
+ description: 'Read persistent project onboarding choices and rework plans. Old projects have an empty list until onboarding is explicitly chosen.',
115
+ inputSchema: z
116
+ .strictObject({
117
+ projectId: uuid.optional(),
118
+ onboardingId: uuid.optional(),
119
+ repoRoot: repoRoot.optional(),
120
+ requestKey: z
121
+ .string()
122
+ .regex(/^[A-Za-z0-9_-]{1,80}$/)
123
+ .optional(),
124
+ afterId: uuid.optional(),
125
+ limit: z.number().int().min(1).max(100).optional(),
126
+ })
127
+ .refine((input) => Boolean(input.projectId || (input.repoRoot && input.requestKey)), {
128
+ message: 'Supply projectId for the project record or repoRoot/requestKey to resume a pre-Git local choice.',
129
+ }),
130
+ }, async (input) => input.projectId
131
+ ? output(await service.onboardingRequest('onboarding_status', body({
132
+ projectId: input.projectId,
133
+ onboardingId: input.onboardingId,
134
+ afterId: input.afterId,
135
+ limit: input.limit,
136
+ })))
137
+ : output(await service.onboardingChoice({
138
+ repoRoot: input.repoRoot,
139
+ requestKey: input.requestKey,
140
+ preparation: true,
141
+ })));
142
+ server.registerTool('project.rework_plan', {
143
+ description: 'After completed deep learning and approved target-architecture revisions, approve an exact phased plan through a native form. Materializes real dependency-ordered WorkItems atomically and idempotently. Each phase names inspected units and a branch; later implementation uses existing task.branch/worktrees and each repository lifecycle. No source changes happen here.',
144
+ inputSchema: z.strictObject({
145
+ ...run,
146
+ repoRoot,
147
+ onboardingId: uuid,
148
+ requestKey: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/),
149
+ targetRevisions: z
150
+ .array(z.strictObject({ resourceId: uuid, revisionId: uuid, contentHash: hash }))
151
+ .min(1)
152
+ .max(100),
153
+ phases: z
154
+ .array(z.strictObject({
155
+ key: z.string().min(1).max(120),
156
+ title: z.string().min(1).max(160),
157
+ description: z.string().min(1).max(4000),
158
+ acceptanceCriteria: z.array(z.string().min(1).max(4000)).min(1).max(50),
159
+ unitKeys: z.array(z.string().min(1).max(240)).min(1).max(100),
160
+ dependsOn: z.array(z.string().min(1).max(120)).max(100),
161
+ branch: z.string().min(1).max(255),
162
+ }))
163
+ .min(1)
164
+ .max(100),
165
+ }),
166
+ }, async (input, context) => {
167
+ const { repoRoot: root, ...plan } = input;
168
+ const digest = sha256(stableStringify(plan));
169
+ const answer = await askQuestionnaire(server, service, {
170
+ repoRoot: root,
171
+ questionnaireId: 'rework-' + digest,
172
+ message: 'Approve the phased rework plan already presented in full, including target revisions, units, acceptance criteria and branches? Digest: ' +
173
+ digest +
174
+ '. Creates work items only; each implementation needs its normal branch/worktree lifecycle.',
175
+ options: [
176
+ { id: 'approve', label: 'Create these exact phases' },
177
+ { id: 'defer', label: 'Keep the plan for further review' },
178
+ ],
179
+ }, context);
180
+ if (answerChoice(answer) !== 'approve')
181
+ return answer;
182
+ return output(await service.onboardingRequest('rework', body(plan)));
183
+ });
184
+ server.registerTool('memory.source', {
185
+ description: 'Read a bounded page of a committed Git manifest, or one file at an exact commit. Never reads dirty worktree content into shared memory. Exclusions carry reasons. Use afterPath for every remaining manifest page. File reads return bounded character chunks; follow nextOffset until null. maxBytes permits regular files up to 16 MiB; larger sources require Git reads at the same pinned commit.',
186
+ inputSchema: z.object({
187
+ repoRoot,
188
+ ref: z.string().optional(),
189
+ commit: z.string().optional(),
190
+ previousCommit: z.string().optional(),
191
+ path: z.string().optional(),
192
+ afterPath: z.string().optional(),
193
+ offset: z.number().int().min(0).optional(),
194
+ maxCharacters: z.number().int().min(1).max(100000).optional(),
195
+ maxBytes: z.number().int().min(1).max(16777216).optional(),
196
+ limit: z.number().int().min(1).max(1000).optional(),
197
+ }),
198
+ }, async (input) => output(await service.memorySource(input)));
199
+ server.registerTool('memory.sync_start', {
200
+ description: 'Start or resume an idempotent committed-source inspection after native mode approval. Uploads all manifest pages, including exclusions. Request key and source are immutable. Refresh references a completed earlier run; rework requires preserve/redesign. Analyze with the current agent; no backend LLM.',
201
+ inputSchema: z.object({
202
+ ...project,
203
+ repoRoot,
204
+ requestKey: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
205
+ mode: z.enum(['adopt', 'refresh', 'rework']),
206
+ ref: z.string().optional(),
207
+ previousRunId: uuid.optional(),
208
+ reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
209
+ }),
210
+ }, async (input, context) => {
211
+ const source = await service.memorySource({
212
+ repoRoot: input.repoRoot,
213
+ ref: input.ref ?? 'HEAD',
214
+ limit: 1,
215
+ });
216
+ if (!source.ok)
217
+ return output(source);
218
+ const commit = source.data.commit;
219
+ if (typeof commit !== 'string')
220
+ return output({
221
+ ok: false,
222
+ error: {
223
+ kind: 'bridge_error',
224
+ message: 'The source has no commit identity.',
225
+ retryable: false,
226
+ recovery: 'memory.sync_status',
227
+ },
228
+ });
229
+ const pinned = { ...input, ref: commit };
230
+ const digest = sha256(stableStringify(pinned));
231
+ const question = await askQuestionnaire(server, service, {
232
+ repoRoot: input.repoRoot,
233
+ questionnaireId: 'sync-start-' + digest,
234
+ message: 'Start ' +
235
+ input.mode +
236
+ ' for committed source ' +
237
+ commit +
238
+ '? Inspect architecture, state, services, network, data, screens and flows; dirty changes stay local. ' +
239
+ (input.reworkBehavior ? 'Rework target: ' + input.reworkBehavior + '. ' : '') +
240
+ 'Exact request: ' +
241
+ digest,
242
+ options: [
243
+ { id: 'approve', label: 'Start this exact inspection' },
244
+ { id: 'defer', label: 'Keep inspection unstarted' },
245
+ ],
246
+ }, context);
247
+ if (answerChoice(question) !== 'approve')
248
+ return question;
249
+ return output(await service.memorySyncStart({ ...pinned, questionnaireId: 'sync-start-' + digest }));
250
+ });
251
+ server.registerTool('memory.sync_status', {
252
+ description: 'List resumable inspections for a project, or page the units and coverage counters of one run. Tokens are not exposed here; resume an owned claim through memory.sync_next with its saved token.',
253
+ inputSchema: z.object({
254
+ ...project,
255
+ ...page,
256
+ runId: uuid.optional(),
257
+ repoRoot: repoRoot.optional(),
258
+ }),
259
+ }, async (input) => output(await service.memorySyncStatus(input)));
260
+ server.registerTool('memory.sync_inventory', {
261
+ description: 'Upload an immutable manifest page. Prefer sync_start which derives it from real Git. Seal only when all pages match the pinned hash and file count.',
262
+ inputSchema: z.object({
263
+ ...run,
264
+ files: z
265
+ .array(z.object({
266
+ path: z.string().min(1).max(1000),
267
+ blobHash: z.string().regex(/^([a-f0-9]{40}|[a-f0-9]{64})$/),
268
+ category: z.enum([
269
+ 'source',
270
+ 'test',
271
+ 'config',
272
+ 'docs',
273
+ 'assets',
274
+ 'generated',
275
+ 'vendor',
276
+ 'secret',
277
+ 'submodule',
278
+ 'binary',
279
+ 'symlink',
280
+ ]),
281
+ excludedReason: z.string().min(1).max(1000).optional(),
282
+ }))
283
+ .max(1000),
284
+ seal: z.boolean().optional(),
285
+ }),
286
+ }, async (input) => output(await service.onboardingRequest('inventory', body(input))));
287
+ server.registerTool('memory.sync_plan', {
288
+ description: 'Plan module, state, service, network, data, screen and cross-module flow units. Every included file and removed behavior needs coverage. Preserve source facts separately from inference or unknown intent. dependsOn names units affected when dependencies change. Sealing requires a coverage map for architecture, state, services, network, persistence, screens, navigation and flows. Map each area to compatible unit keys and inventory evidence, or document its absence; declarations do not prove semantic correctness.',
289
+ inputSchema: z.object({
290
+ ...run,
291
+ units: z
292
+ .array(z.object({
293
+ unitKey: z.string().regex(/^[a-zA-Z0-9_./-]{1,240}$/),
294
+ moduleKey: z.string().regex(/^[a-zA-Z0-9_./-]{1,240}$/),
295
+ kind,
296
+ paths: z.array(z.string().min(1).max(1000)).min(1).max(500),
297
+ dependsOn: z.array(z.string().max(240)).max(100),
298
+ }))
299
+ .max(100),
300
+ coverage: z
301
+ .strictObject({
302
+ architecture: z.strictObject({
303
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
304
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
305
+ notApplicableReason: z.string().min(1).max(2000).optional(),
306
+ }),
307
+ state: z.strictObject({
308
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
309
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
310
+ notApplicableReason: z.string().min(1).max(2000).optional(),
311
+ }),
312
+ services: z.strictObject({
313
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
314
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
315
+ notApplicableReason: z.string().min(1).max(2000).optional(),
316
+ }),
317
+ network: z.strictObject({
318
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
319
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
320
+ notApplicableReason: z.string().min(1).max(2000).optional(),
321
+ }),
322
+ persistence: z.strictObject({
323
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
324
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
325
+ notApplicableReason: z.string().min(1).max(2000).optional(),
326
+ }),
327
+ screens: z.strictObject({
328
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
329
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
330
+ notApplicableReason: z.string().min(1).max(2000).optional(),
331
+ }),
332
+ navigation: z.strictObject({
333
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
334
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
335
+ notApplicableReason: z.string().min(1).max(2000).optional(),
336
+ }),
337
+ flows: z.strictObject({
338
+ unitKeys: z.array(z.string().min(1).max(240)).max(10000),
339
+ evidencePaths: z.array(z.string().min(1).max(1000)).max(10000),
340
+ notApplicableReason: z.string().min(1).max(2000).optional(),
341
+ }),
342
+ })
343
+ .optional(),
344
+ seal: z.boolean().optional(),
345
+ }),
346
+ }, async (input) => output(await service.onboardingRequest('plan', body(input))));
347
+ server.registerTool('memory.sync_next', {
348
+ description: 'Claim the next unit for fifteen minutes. Fresh parallel calls use distinct requestKeys; retry the same requestKey after response loss. Save the returned claimToken and use it to resume or renew the same unit. Analysis happens outside the transaction.',
349
+ inputSchema: z
350
+ .object({
351
+ ...run,
352
+ claimToken: uuid.optional(),
353
+ requestKey: z
354
+ .string()
355
+ .regex(/^[A-Za-z0-9_-]{1,120}$/)
356
+ .optional(),
357
+ })
358
+ .refine((input) => Boolean(input.claimToken || input.requestKey), {
359
+ message: 'Use a unique requestKey for a fresh claim or the saved claimToken to resume.',
360
+ }),
361
+ }, async (input) => output(await service.onboardingRequest('next', body(input))));
362
+ server.registerTool('memory.sync_submit', {
363
+ description: 'Submit immutable source/execution evidence, behavior facets, gaps, and exact proposal/unchanged revision references. Each source/execution witness declares evidence.facets; behavior facet references must point to witnesses for that facet. Facets: entry, exit, authority, effects, errors, cancellation, retry, persistence, dependencies. Proposals must carry provenance {sourceCommit,runId,unitKey}. No historical tasks or motivations may be invented.',
364
+ inputSchema: z.object({ ...run, unitId: uuid, claimToken: uuid, submission: object }),
365
+ }, async (input) => output(await service.onboardingRequest('submit', body(input))));
366
+ server.registerTool('memory.sync_reopen', {
367
+ description: 'Reopen an unfinished unit after rejected proposals or incomplete evidence, using its current lockVersion as expectedVersion. Completed runs are immutable: refresh instead.',
368
+ inputSchema: z.object({ ...run, unitId: uuid, expectedVersion: z.number().int().min(1) }),
369
+ }, async (input) => output(await service.onboardingRequest('reopen', body(input))));
370
+ server.registerTool('memory.sync_delta', {
371
+ description: 'Page previous units whose source, memory revisions or transitive dependencies changed. Uses content hashes even after squash/history loss. Re-plan deletions for reviewed archival; compare unchanged units against exact current revisions without making no-op proposals.',
372
+ inputSchema: z.strictObject({ ...run, ...page, repoRoot: repoRoot.optional() }),
373
+ }, async (input) => output(await service.memorySyncDelta(input)));
374
+ server.registerTool('memory.sync_verify', {
375
+ description: 'Verify sealed inventory, semantic file coverage, complete behavior evidence, absence of unresolved gaps and approved immutable memory references. This inspection gate does not replace or weaken task.verify for code changes.',
376
+ inputSchema: z.object(run),
377
+ }, async (input) => output(await service.onboardingRequest('verify', body(input))));
378
+ server.registerTool('memory.catalog', {
379
+ description: 'Page accessible memory resource headers by id. Read all pages using the last id when a page is full. includeArchived exposes retired identities for refresh and rename preservation.',
380
+ inputSchema: z.object({
381
+ ...project,
382
+ ...page,
383
+ includeArchived: z.boolean().optional(),
384
+ includeLinked: z.boolean().optional(),
385
+ kinds: z.array(kind).max(100).optional(),
386
+ }),
387
+ }, async (input) => output(await service.onboardingRequest('catalog', body(input))));
388
+ server.registerTool('memory.read_revisions', {
389
+ description: 'Read selected immutable revisions in an authorized context session and record receipts. Bounds keep large project learning independent of normal task context limits.',
390
+ inputSchema: z.object({
391
+ ...project,
392
+ sessionId: uuid,
393
+ revisionIds: z.array(uuid).min(1).max(100),
394
+ }),
395
+ }, async (input) => output(await service.onboardingRequest('read_revisions', body(input))));
396
+ server.registerTool('memory.review_batch', {
397
+ description: 'After presenting full module proposal contents, evidence, uncertainties and archive actions through memory.list_proposals, collect native approval for these exact proposal ids, hashes and base revisions. Pending/dismissed forms never write. All reviews commit atomically; stale revisions fail.',
398
+ inputSchema: z.strictObject({
399
+ ...project,
400
+ repoRoot,
401
+ entries: z.array(reference).min(1).max(20),
402
+ reviewAttempt: z.number().int().min(0).max(10000).default(0),
403
+ }),
404
+ }, async (input, context) => {
405
+ const entries = [...input.entries].sort((a, b) => a.proposalId.localeCompare(b.proposalId));
406
+ const digest = sha256(stableStringify({ projectId: input.projectId, entries }));
407
+ const result = await askQuestionnaire(server, service, {
408
+ repoRoot: input.repoRoot,
409
+ questionnaireId: 'memory-batch-' + digest + '-' + input.reviewAttempt,
410
+ message: 'Review the exact module batch already presented in full. Batch digest: ' +
411
+ digest +
412
+ '\nProposals:\n' +
413
+ entries.map((e) => e.proposalId).join('\n') +
414
+ '\nApprove publishes the listed contents; reject leaves active memory unchanged.',
415
+ options: [
416
+ { id: 'approved', label: 'Approve this exact batch' },
417
+ { id: 'rejected', label: 'Reject this exact batch' },
418
+ { id: 'defer', label: 'Leave these proposals pending' },
419
+ ],
420
+ }, context);
421
+ const decision = answerChoice(result);
422
+ if (decision === 'defer')
423
+ return output({
424
+ ok: true,
425
+ data: {
426
+ status: 'deferred',
427
+ digest,
428
+ reviewAttempt: input.reviewAttempt,
429
+ nextReviewAttempt: input.reviewAttempt + 1,
430
+ nextAction: 'When the user is ready to review again, call memory.review_batch with the same exact entries and nextReviewAttempt as reviewAttempt. Retrying this attempt preserves the deferral.',
431
+ },
432
+ });
433
+ if (decision !== 'approved' && decision !== 'rejected')
434
+ return result;
435
+ return output(await service.onboardingRequest('review_batch', body({
436
+ projectId: input.projectId,
437
+ entries,
438
+ decision,
439
+ note: 'Native module-batch review ' + digest,
440
+ })));
441
+ });
442
+ }
443
+ function answerChoice(result) {
444
+ return answerData(result)?.choice;
445
+ }
446
+ function answerData(result) {
447
+ const response = result;
448
+ const text = response.content?.find((item) => item.type === 'text')?.text;
449
+ if (!text)
450
+ return undefined;
451
+ const value = JSON.parse(text);
452
+ return value.ok && value.data?.status === 'answered' && value.data.answerAvailable
453
+ ? value.data.answer
454
+ : undefined;
455
+ }
456
+ async function approveOnboarding(server, service, input, context, initialize) {
457
+ const preparation = !input.projectId;
458
+ const scope = { repoRoot: input.repoRoot, preparation };
459
+ const mode = await askQuestionnaire(server, service, {
460
+ ...scope,
461
+ questionnaireId: 'onboard-mode-' + sha256(input.requestKey),
462
+ message: 'Based on project.inspect, choose how this project should enter Engineering Memory. Choosing an existing architecture never rewrites it.',
463
+ options: [
464
+ { id: 'greenfield', label: 'Set up a new or barely-started project' },
465
+ { id: 'adopt', label: 'Learn and preserve the existing architecture' },
466
+ { id: 'rework', label: 'Learn first, then plan a phased rework' },
467
+ ...(input.projectId
468
+ ? [{ id: 'refresh', label: 'Refresh memory against the current project' }]
469
+ : []),
470
+ ],
471
+ }, context);
472
+ const selected = answerChoice(mode);
473
+ if (!selected)
474
+ return mode;
475
+ if (selected !== input.mode)
476
+ return output({
477
+ ok: true,
478
+ data: {
479
+ mode: selected,
480
+ nextAction: 'Resume project.onboard with this exact mode; the native profile questionnaire will continue with the same requestKey. No source was changed.',
481
+ },
482
+ });
483
+ if (selected === 'rework') {
484
+ const behavior = await askQuestionnaire(server, service, {
485
+ ...scope,
486
+ questionnaireId: 'rework-behavior-' + sha256(input.requestKey),
487
+ message: 'Should rework preserve current observable behavior or redesign the product? Redesign requires approved target screens, flows and contracts before implementation.',
488
+ options: [
489
+ { id: 'preserve', label: 'Preserve current behavior' },
490
+ { id: 'redesign', label: 'Redesign the product explicitly' },
491
+ ],
492
+ }, context);
493
+ const chosen = answerChoice(behavior);
494
+ if (!chosen)
495
+ return behavior;
496
+ if (chosen !== input.reworkBehavior)
497
+ return output({
498
+ ok: true,
499
+ data: {
500
+ mode: selected,
501
+ reworkBehavior: chosen,
502
+ nextAction: 'Resume project.onboard with this exact behavior choice; native profile questions will continue.',
503
+ },
504
+ });
505
+ }
506
+ const effectiveProfile = body(input.profile);
507
+ const processedFields = new Set();
508
+ let fields = profileFields(effectiveProfile);
509
+ for (let index = 0; index < fields.length; index += 1) {
510
+ const [field, label] = fields[index];
511
+ if (processedFields.has(field))
512
+ continue;
513
+ const currentValue = effectiveProfile[field] ?? null;
514
+ const digest = sha256(stableStringify({
515
+ requestKey: input.requestKey,
516
+ mode: input.mode,
517
+ reworkBehavior: input.reworkBehavior ?? null,
518
+ field,
519
+ currentValue,
520
+ }));
521
+ const response = await askQuestionnaire(server, service, {
522
+ ...scope,
523
+ questionnaireId: 'onboard-field-' + field + '-' + digest,
524
+ message: 'Review this onboarding choice before it is persisted.\n' +
525
+ 'Field: ' +
526
+ label +
527
+ '\nCurrent value:\n' +
528
+ JSON.stringify(currentValue, null, 2) +
529
+ '\nKeep it, edit it in the bounded field below, or defer this field explicitly. A deferred field remains pending and is never silently saved as the current value.',
530
+ options: profileFieldOptions(field, currentValue),
531
+ ...(field === 'stack'
532
+ ? {}
533
+ : {
534
+ textField: {
535
+ title: field === 'backendProjectIds' ? 'Project ID JSON array' : 'Edited value',
536
+ maxLength: field === 'backendProjectIds'
537
+ ? 2000
538
+ : ['architecture', 'colorPalette', 'typography'].includes(field)
539
+ ? 240
540
+ : 160,
541
+ requiredForChoice: 'edit_value',
542
+ ...(field === 'backendProjectIds' ? { valueType: 'uuid_list' } : {}),
543
+ },
544
+ }),
545
+ }, context);
546
+ const answer = answerData(response);
547
+ const choice = answer?.choice;
548
+ if (!choice)
549
+ return response;
550
+ if (choice === 'defer_current')
551
+ return deferredProfile(input.requestKey, field);
552
+ if (field === 'stack' && stackValues.includes(choice)) {
553
+ effectiveProfile.stack = choice;
554
+ processedFields.add(field);
555
+ fields = profileFields(effectiveProfile);
556
+ continue;
557
+ }
558
+ if (choice === 'edit_value') {
559
+ if (!answer.text)
560
+ return response;
561
+ effectiveProfile[field] = parseEditedProfileValue(field, answer.text);
562
+ }
563
+ processedFields.add(field);
564
+ fields = profileFields(effectiveProfile);
565
+ }
566
+ const choice = {
567
+ requestKey: input.requestKey,
568
+ mode: input.mode,
569
+ profile: body(onboardingInput.parse({ ...input, profile: effectiveProfile }).profile),
570
+ ...(input.reworkBehavior ? { reworkBehavior: input.reworkBehavior } : {}),
571
+ };
572
+ const digest = sha256(stableStringify(choice));
573
+ const questionnaireId = 'onboard-profile-' + digest + (initialize ? '-initialize' : '-save');
574
+ const approval = await askQuestionnaire(server, service, {
575
+ ...scope,
576
+ questionnaireId,
577
+ message: 'Save the selected mode and the profile choices just reviewed? Mode: ' +
578
+ input.mode +
579
+ '. Exact choice digest: ' +
580
+ digest +
581
+ '. ' +
582
+ (initialize ? 'Greenfield will prepare Git without replacing existing files. ' : '') +
583
+ 'Engineering-memory revisions and project links require their own review/authority.',
584
+ options: [
585
+ { id: 'approve', label: 'Save these exact onboarding choices' },
586
+ { id: 'defer', label: 'Keep the choices pending' },
587
+ ],
588
+ }, context);
589
+ if (answerChoice(approval) === 'defer')
590
+ return deferredProfile(input.requestKey, 'final-profile');
591
+ if (answerChoice(approval) !== 'approve')
592
+ return approval;
593
+ const saved = initialize
594
+ ? await service.projectInitialize({
595
+ ...scope,
596
+ requestKey: input.requestKey,
597
+ choice,
598
+ questionnaireId,
599
+ })
600
+ : await service.onboardingChoice({
601
+ ...scope,
602
+ requestKey: input.requestKey,
603
+ choice,
604
+ questionnaireId,
605
+ });
606
+ if (!saved.ok || !input.projectId)
607
+ return output(saved);
608
+ return output(await service.onboardingRequest('onboarding_choose', body({ projectId: input.projectId, ...choice })));
609
+ }
610
+ function deferredProfile(requestKey, field) {
611
+ return output({
612
+ ok: true,
613
+ data: {
614
+ status: 'deferred',
615
+ field,
616
+ nextRequestKey: 'reconsider-' + sha256(requestKey).slice(0, 48),
617
+ nextAction: 'No onboarding choice was saved. Retrying this request preserves the deferral. When the user explicitly resumes this decision, call project.onboard or project.initialize with nextRequestKey to collect fresh native choices; do not infer approval.',
618
+ },
619
+ });
620
+ }
621
+ function parseEditedProfileValue(field, value) {
622
+ if (field !== 'backendProjectIds')
623
+ return value.trim();
624
+ const parsed = JSON.parse(value);
625
+ return z.array(uuid).max(10).parse(parsed);
626
+ }
627
+ const stackValues = ['Flutter', 'React', 'Nest', 'Spring'];
628
+ function profileFieldOptions(field, currentValue) {
629
+ if (field === 'stack') {
630
+ return [
631
+ { id: 'keep_current', label: 'Keep this exact value' },
632
+ ...stackValues
633
+ .filter((value) => value !== currentValue)
634
+ .map((value) => ({ id: value, label: `Change to ${value}` })),
635
+ { id: 'defer_current', label: 'Defer this value explicitly' },
636
+ ];
637
+ }
638
+ return [
639
+ ...(currentValue === null ? [] : [{ id: 'keep_current', label: 'Keep this exact value' }]),
640
+ { id: 'edit_value', label: 'Edit this value in the field below' },
641
+ { id: 'defer_current', label: 'Defer this value explicitly' },
642
+ ];
643
+ }
644
+ function profileFields(profileValue) {
645
+ const fields = [
646
+ ['stack', 'Application stack'],
647
+ ['runtime', 'Runtime and toolchain contract'],
648
+ ['architecture', 'Application architecture'],
649
+ ['environments', 'Environments and configuration'],
650
+ ['serviceReadiness', 'Service readiness'],
651
+ ['apiContract', 'API contract'],
652
+ ['authentication', 'Authentication'],
653
+ ['storage', 'Storage and persistence'],
654
+ ['backendProjectIds', 'Existing backend project links'],
655
+ ];
656
+ if (['Nest', 'Spring'].includes(profileValue.stack))
657
+ fields.push(['migrations', 'Migration strategy']);
658
+ if (['Flutter', 'React'].includes(profileValue.stack)) {
659
+ fields.push(['figma', 'Figma source'], ['colorPalette', 'Color palette'], ['typography', 'Font families and text styles'], ['designSystem', 'Design system'], ['navigation', 'Navigation'], ['platforms', 'Platforms'], ['localization', 'Localization languages']);
660
+ }
661
+ return fields;
662
+ }
663
+ //# sourceMappingURL=onboarding-tools.js.map