engineering-memory 1.11.20 → 1.11.22

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 (31) hide show
  1. package/bin/engineering-memory.mjs +14 -24
  2. package/install/localization.generated.mjs +18 -0
  3. package/package.json +1 -1
  4. package/runtime/build.json +1 -1
  5. package/runtime/dist/src/auth/browser-auth.js +15 -4
  6. package/runtime/dist/src/config.js +1 -0
  7. package/runtime/dist/src/git/git-inspector.js +10 -0
  8. package/runtime/dist/src/localization/catalogue.generated.js +786 -0
  9. package/runtime/dist/src/mcp/delivery-tools.js +15 -48
  10. package/runtime/dist/src/mcp/onboarding-tools.js +289 -618
  11. package/runtime/dist/src/mcp/questionnaire-tools.js +23 -26
  12. package/runtime/dist/src/mcp/status-meaning-tools.js +12 -86
  13. package/runtime/dist/src/mcp/status-remap-tools.js +252 -0
  14. package/runtime/dist/src/mcp/tool-annotations.js +1 -0
  15. package/runtime/dist/src/mcp/tool-definitions.js +33 -55
  16. package/runtime/dist/src/mcp/workflow-tools.js +17 -108
  17. package/runtime/dist/src/mcp/worktree-tools.js +168 -250
  18. package/runtime/dist/src/runtime/bridge-service.js +123 -94
  19. package/runtime/dist/src/runtime/create-bridge-service.js +4 -2
  20. package/runtime/dist/src/runtime/language-store.js +6 -0
  21. package/runtime/dist/src/runtime/release-notes.js +6 -6
  22. package/runtime/dist/src/runtime/release-report.js +30 -30
  23. package/runtime/dist/src/runtime/task-start.js +75 -73
  24. package/runtime/dist/src/runtime/texts.js +135 -0
  25. package/runtime/dist/src/runtime/worktree-pool.js +30 -5
  26. package/runtime/dist/src/runtime/worktree-preparation.js +3 -2
  27. package/runtime/dist/src/utilities/local-mutex.js +5 -2
  28. package/runtime/dist/src/utilities/process.js +7 -0
  29. package/skill/references/lifecycle.md +22 -4
  30. package/skill/references/project-onboarding.md +5 -2
  31. package/skill/references/questionnaires.md +3 -1
@@ -2,6 +2,7 @@ import * as z from 'zod/v4';
2
2
  import { sha256, stableStringify } from '../utilities/hash.js';
3
3
  import { restrictedValueKind } from '../runtime/privacy-detector.js';
4
4
  import { languageTag } from '../runtime/questionnaire-store.js';
5
+ import { copies, format, texts } from '../runtime/texts.js';
5
6
  import { answerChoice, answerData, askFailed, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
6
7
  const uuid = z.string().uuid();
7
8
  const hash = z.string().regex(/^[a-f0-9]{64}$/);
@@ -18,58 +19,6 @@ const json = z.lazy(() => z.union([
18
19
  z.record(z.string(), json),
19
20
  ]));
20
21
  const object = z.record(z.string(), json);
21
- const inspectionCopy = {
22
- tr: {
23
- modes: {
24
- refresh: 'Projenin hafızasını güncel kodla karşılaştırıp incelemeyi başlatalım mı?',
25
- adopt: 'Mevcut projeyi mimarisiyle birlikte öğrenmeye başlayalım mı?',
26
- rework: 'Rework planından önce mevcut projeyi incelemeye başlayalım mı?',
27
- },
28
- source: 'Kaynak',
29
- scope: 'Mimari, state yönetimi, servisler, network katmanı, veri modelleri, ekranlar ve akışlar incelenecek.',
30
- localChanges: 'Uygulama kodu değiştirilmeyecek. Commit edilmemiş değişiklikler ortak hafızaya alınmayacak.',
31
- review: 'Bu onay incelemeyi başlatır. Kalıcı hafıza değişiklikleri ayrıca onayına sunulacak.',
32
- rework: {
33
- preserve: 'Rework hedefi: mevcut davranışı korumak.',
34
- redesign: 'Rework hedefi: ürünü yeniden tasarlamak. Uygulama öncesinde hedef ayrıca onaylanacak.',
35
- },
36
- why: {
37
- refresh: 'İnceleme, hafızadaki kayıtları bu kaynaktaki kodla karşılaştırır ve neyin değiştiğini çıkarır.',
38
- adopt: 'Proje ilk kez hafızaya alınıyor. İnceleme, bu kaynaktaki kodu okuyup projenin nasıl çalıştığını çıkarır.',
39
- rework: 'Yeniden yazım planının yazılabilmesi için önce projenin bugünkü hâli okunmalı.',
40
- },
41
- example: 'İnceleme bitince her ekran, servis ve akış için birer hafıza kaydı önerilir; hangilerinin yayınlanacağına sen karar verirsin.',
42
- start: 'İncelemeyi başlat',
43
- startDetail: 'İnceleme şimdi başlar. Kod değişmez; çıkan kayıtlar sonunda tek tek onayına gelir.',
44
- defer: 'Şimdilik başlatma',
45
- deferDetail: 'İnceleme başlamaz, hiçbir şey yazılmaz. Cevabın kaydedilir; bu kaynağı sonra incelemek yeni bir istek olur.',
46
- },
47
- en: {
48
- modes: {
49
- refresh: 'Start comparing project memory with the current code?',
50
- adopt: 'Start learning the existing project and its architecture?',
51
- rework: 'Start inspecting the existing project before planning its rework?',
52
- },
53
- source: 'Source',
54
- scope: 'Inspect architecture, state, services, network, data models, screens and flows.',
55
- localChanges: 'Application code will stay unchanged. Uncommitted changes will not enter shared memory.',
56
- review: 'This approval starts the inspection. Permanent memory changes will be submitted for separate approval.',
57
- rework: {
58
- preserve: 'Rework goal: preserve existing behavior.',
59
- redesign: 'Rework goal: redesign the product. The target needs separate approval before implementation.',
60
- },
61
- why: {
62
- refresh: 'The inspection compares the records already in memory with the code at that source and works out what changed.',
63
- adopt: 'The project enters memory for the first time. The inspection reads the code at that source and works out how the project behaves.',
64
- rework: 'The rework plan can only be written once the project as it stands today is read.',
65
- },
66
- example: 'When the inspection finishes, a memory record is proposed for each screen, service and flow, and you decide which of them get published.',
67
- start: 'Start inspection',
68
- startDetail: 'The inspection starts now. The code stays unchanged and every record it writes comes back for your approval.',
69
- defer: 'Not now',
70
- deferDetail: 'Nothing is inspected and nothing is written. Your answer is recorded, so inspecting this source later is a new request.',
71
- },
72
- };
73
22
  const reference = z.object({
74
23
  proposalId: uuid,
75
24
  contentHash: hash,
@@ -94,82 +43,47 @@ const kind = z.enum([
94
43
  'quality_gate',
95
44
  'architecture_template',
96
45
  ]);
97
- const batchCopy = {
98
- en: {
99
- one: 'One memory record is ready to be published.',
100
- many: (count) => `${count} memory records are ready to be published.`,
101
- context: 'Publishing replaces the current text of these records for everyone who reads this memory. Earlier revisions stay in the history.',
102
- unversioned: (titles) => ` Written without a source inspection: ${titles}. Tasks that read memory by source version will not see them until an inspection retains them.`,
103
- example: 'After publishing, the next session that reads one of these records gets the new text instead of the old one.',
104
- untitled: 'untitled',
105
- scope: { project: 'project', organization: 'organization', product: 'product' },
106
- newRecord: 'new record',
107
- revision: (from) => `revision ${from} to ${from + 1}`,
108
- rebase: 'written against an older revision',
109
- why: 'Why',
110
- approved: ['Publish all of them', 'Every listed record takes its new text now.'],
111
- rejected: [
112
- 'Reject all of them',
113
- 'Nothing changes. The proposals are marked rejected and have to be written again.',
114
- ],
115
- oneByOne: [
116
- 'Let me look at them one by one',
117
- 'Nothing changes now. Each record is shown in full and decided on its own.',
118
- ],
119
- defer: ['Decide later', 'Nothing changes. The proposals stay pending.'],
120
- },
121
- tr: {
122
- one: 'Bir hafıza kaydı yayına hazır.',
123
- many: (count) => `${count} hafıza kaydı yayına hazır.`,
124
- context: 'Yayınlarsan bu kayıtların mevcut metni, bu hafızayı okuyan herkes için değişir. Eski sürümler geçmişte kalır.',
125
- unversioned: (titles) => ` Kaynak incelemesi yapılmadan yazılanlar: ${titles}. Hafızayı kaynak sürümüne göre okuyan görevler, bir inceleme bunları saklayana kadar görmez.`,
126
- example: 'Yayınladıktan sonra bu kayıtlardan birini okuyan bir sonraki oturum eski metni değil yenisini görür.',
127
- untitled: 'başlıksız',
128
- scope: { project: 'proje', organization: 'organizasyon', product: 'ürün' },
129
- newRecord: 'yeni kayıt',
130
- revision: (from) => `sürüm ${from} → ${from + 1}`,
131
- rebase: 'eski bir sürüme göre yazılmış',
132
- why: 'Nedeni',
133
- approved: ['Hepsini yayınla', 'Listedeki her kayıt şimdi yeni metnine geçer.'],
134
- rejected: [
135
- 'Hepsini reddet',
136
- 'Hiçbir şey değişmez. Öneriler reddedilir ve yeniden yazılmaları gerekir.',
137
- ],
138
- oneByOne: [
139
- 'Tek tek bakayım',
140
- 'Şimdi hiçbir şey değişmez. Her kayıt tam metniyle gösterilir ve ayrı ayrı karara bağlanır.',
141
- ],
142
- defer: ['Şimdilik karar verme', 'Hiçbir şey değişmez. Öneriler beklemede kalır.'],
143
- },
144
- };
145
- const kindInTurkish = {
146
- engineering_rule: 'mühendislik kuralı',
147
- project_profile: 'proje profili',
148
- service_contract: 'servis sözleşmesi',
149
- localization_contract: 'yerelleştirme sözleşmesi',
150
- navigation_contract: 'gezinme sözleşmesi',
151
- state_contract: 'durum yönetimi sözleşmesi',
152
- screen_logic: 'ekran mantığı',
153
- flow_logic: 'akış mantığı',
154
- module_logic: 'modül mantığı',
155
- data_model: 'veri modeli',
156
- api_endpoint: 'API endpoint kaydı',
157
- component_mapping: 'bileşen eşlemesi',
158
- figma_mapping: 'Figma eşlemesi',
159
- figma_reference: 'Figma referansı',
160
- current_deviation: 'kayıtlı sapma',
161
- quality_gate: 'kalite kapısı',
162
- architecture_template: 'mimari şablonu',
163
- };
164
- function batchQuestion(turkish, proposals) {
165
- const copy = batchCopy[turkish ? 'tr' : 'en'];
46
+ const text = z.string().min(1);
47
+ const option = z.strictObject({ label: text, description: text });
48
+ const inspectionWording = z.strictObject({
49
+ modes: z.strictObject({ refresh: text, adopt: text, rework: text }),
50
+ source: text,
51
+ scope: text,
52
+ localChanges: text,
53
+ review: text,
54
+ rework: z.strictObject({ preserve: text, redesign: text }),
55
+ why: z.strictObject({ refresh: text, adopt: text, rework: text }),
56
+ example: text,
57
+ start: text,
58
+ startDetail: text,
59
+ defer: text,
60
+ deferDetail: text,
61
+ });
62
+ const reviewWording = z.strictObject({
63
+ ready: text,
64
+ context: text,
65
+ unversioned: text,
66
+ example: text,
67
+ untitled: text,
68
+ scope: z.strictObject({ project: text, organization: text, product: text }),
69
+ newRecord: text,
70
+ revision: text,
71
+ rebase: text,
72
+ why: text,
73
+ approved: option,
74
+ rejected: option,
75
+ oneByOne: option,
76
+ defer: option,
77
+ kinds: z.record(kind, text),
78
+ });
79
+ function batchQuestion(copy, language, proposals) {
166
80
  const clean = (text) => (restrictedValueKind(text, 'message', true) ? undefined : text);
167
81
  const named = proposals.map((proposal) => {
168
- const kind = (turkish && kindInTurkish[proposal.kind]) || proposal.kind.replaceAll('_', ' ');
82
+ const kind = copy.kinds[proposal.kind] ?? proposal.kind.replaceAll('_', ' ');
169
83
  return {
170
84
  proposal,
171
85
  kind,
172
- title: clean(proposal.title.slice(0, 70)) ?? `${copy.untitled} ${kind}`,
86
+ title: clean(proposal.title.slice(0, 70)) ?? format(copy.untitled, language, { kind }),
173
87
  };
174
88
  });
175
89
  const unversioned = named
@@ -178,21 +92,22 @@ function batchQuestion(turkish, proposals) {
178
92
  .join(', ');
179
93
  return {
180
94
  message: [
181
- proposals.length === 1 ? copy.one : copy.many(proposals.length),
95
+ format(copy.ready, language, { count: proposals.length }),
182
96
  ...named.map(({ proposal, kind, title }, index) => {
183
- const line = `${index + 1}. ${title} (${kind}, ${copy.scope[proposal.scope]}, ${proposal.currentRevisionNumber === 0
184
- ? copy.newRecord
185
- : copy.revision(proposal.currentRevisionNumber)}${proposal.rebaseRequired ? `, ${copy.rebase}` : ''})`;
97
+ const from = proposal.currentRevisionNumber;
98
+ const line = `${index + 1}. ${title} (${kind}, ${copy.scope[proposal.scope]}, ${from === 0 ? copy.newRecord : format(copy.revision, language, { from, to: from + 1 })}${proposal.rebaseRequired ? `, ${copy.rebase}` : ''})`;
186
99
  return clean(`${line} ${copy.why}: ${proposal.reasonPreview.slice(0, 60)}`) ?? line;
187
100
  }),
188
101
  ].join('\n'),
189
- context: copy.context + (unversioned ? copy.unversioned(unversioned) : ''),
102
+ context: unversioned
103
+ ? `${copy.context} ${format(copy.unversioned, language, { titles: unversioned })}`
104
+ : copy.context,
190
105
  example: copy.example,
191
106
  options: [
192
- { id: 'approved', label: copy.approved[0], description: copy.approved[1] },
193
- { id: 'rejected', label: copy.rejected[0], description: copy.rejected[1] },
194
- { id: 'one_by_one', label: copy.oneByOne[0], description: copy.oneByOne[1] },
195
- { id: 'defer', label: copy.defer[0], description: copy.defer[1] },
107
+ { id: 'approved', label: copy.approved.label, description: copy.approved.description },
108
+ { id: 'rejected', label: copy.rejected.label, description: copy.rejected.description },
109
+ { id: 'one_by_one', label: copy.oneByOne.label, description: copy.oneByOne.description },
110
+ { id: 'defer', label: copy.defer.label, description: copy.defer.description },
196
111
  ],
197
112
  };
198
113
  }
@@ -254,78 +169,27 @@ const onboardingInput = z.strictObject({
254
169
  language: languageTag.optional(),
255
170
  reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
256
171
  });
257
- const moveCopy = {
258
- tr: {
259
- ask: (name) => `"${name}" projesini bu depoya taşıyalım mı?`,
260
- from: 'Projenin şu anki deposu',
261
- to: 'Bu klasörün deposu',
262
- unknown: 'adresi kayıtlı değil',
263
- keeps: (tasks, records, items) => `Proje olduğu gibi devam eder: ${tasks} görev, ${records} hafıza kaydı ve ${items} iş kalemi projede kalır, hiçbir şey silinmez ve yeniden öğrenilmez.`,
264
- after: 'Eski depo projenin geçmişinde saklanır. Hâlâ eski depoda çalışan ekip arkadaşları açık görevlerini bitirebilir, yeni görevi yeni depodan açar. Proje ileride yeniden taşınabilir.',
265
- why: 'Projenin kodu başka bir depoya taşınmış. Bu klasör projenin evi sayılmadığı için burada yeni iş başlatılamıyor.',
266
- example: 'Taşıdıktan sonra bu klasörde açtığın görev aynı projeye ait olur ve aynı hafızayı okur.',
267
- approve: [
268
- 'Projeyi bu depoya taşı',
269
- 'Proje bundan sonra bu depoyu gösterir; burada hemen çalışmaya başlayabilirsin.',
270
- ],
271
- decline: [
272
- 'Taşıma',
273
- 'Hiçbir şey değişmez. Proje mevcut deposunda kalır ve bu klasörde iş başlatılamaz.',
274
- ],
275
- },
276
- en: {
277
- ask: (name) => `Move the project "${name}" to this repository?`,
278
- from: 'The project lives in',
279
- to: 'This folder points at',
280
- unknown: 'address not recorded',
281
- keeps: (tasks, records, items) => `The project continues as it is: ${tasks} tasks, ${records} memory records and ${items} work items stay with it; nothing is deleted or learned again.`,
282
- after: 'The old repository is kept in the project history. Teammates still working in it can finish their open tasks and start new ones from the new repository. The project can be moved again later.',
283
- why: 'The code of this project moved to another repository. This folder is not recognised as the home of the project, so no new work can start here.',
284
- example: 'After the move, a task started in this folder belongs to the same project and reads the same memory as before.',
285
- approve: [
286
- 'Move the project here',
287
- 'The project points at this repository from now on and work can start here right away.',
288
- ],
289
- decline: [
290
- 'Do not move',
291
- 'Nothing changes. The project keeps its current repository and no work can start in this folder.',
292
- ],
293
- },
294
- };
295
- const planCopy = {
296
- tr: {
297
- one: 'Bu yeniden yazım planındaki tek aşama için iş kalemi oluşturalım mı?',
298
- many: (count) => `Bu yeniden yazım planındaki ${count} aşama için iş kalemleri oluşturalım mı?`,
299
- branch: 'dal',
300
- more: (count) => `ve ${count} aşama daha`,
301
- context: 'Onaylarsan plandaki her aşama için bağımlılık sırasına göre birer iş kalemi oluşturulur; başka hiçbir şey olmaz. Dal açılmaz, koda dokunulmaz. Her aşama daha sonra kendi görevi, kendi dalı ve kendi worktree ile ayrı ayrı yapılır.',
302
- example: 'Onaydan sonra ilk aşama başlatılabilir bir iş kalemi olarak görünür; ona bağlı aşamalar o bitene kadar bekler.',
303
- approve: [
304
- 'Bu iş kalemlerini oluştur',
305
- 'İş kalemleri listelendiği gibi şimdi oluşturulur. Aşamaların kodlanması yine ayrı ayrı başlar.',
306
- ],
307
- defer: [
308
- 'Plan beklemede kalsın',
309
- 'Hiçbir şey değişmez. İş kalemi oluşturulmaz, planı sonra onaylayabilirsin.',
310
- ],
311
- },
312
- en: {
313
- one: 'Create the work item for the single phase of this rework plan?',
314
- many: (count) => `Create the work items for the ${count} phases of this rework plan?`,
315
- branch: 'branch',
316
- more: (count) => `and ${count} more phases`,
317
- context: 'Approving creates one work item per phase, in the dependency order of the plan, and nothing else. No branch is created and no code is changed. Each phase is implemented later as its own task, with its own branch and worktree.',
318
- example: 'After approval the first phase appears as a work item that can be started, and the phases that depend on it wait until it is done.',
319
- approve: [
320
- 'Create these work items',
321
- 'The work items are created now, exactly as listed. Implementing each phase still starts separately.',
322
- ],
323
- defer: [
324
- 'Keep the plan for further review',
325
- 'Nothing changes. No work item is created and the plan can be approved later.',
326
- ],
327
- },
328
- };
172
+ const moveWording = z.strictObject({
173
+ ask: text,
174
+ from: text,
175
+ to: text,
176
+ unknown: text,
177
+ keeps: text,
178
+ after: text,
179
+ why: text,
180
+ example: text,
181
+ approve: option,
182
+ decline: option,
183
+ });
184
+ const reworkWording = z.strictObject({
185
+ phases: text,
186
+ branch: text,
187
+ more: text,
188
+ context: text,
189
+ example: text,
190
+ approve: option,
191
+ defer: option,
192
+ });
329
193
  export function registerOnboardingTools(server, service) {
330
194
  server.registerTool('project.move_repository', {
331
195
  description: 'Move an existing project to the repository this checkout points at, after its code moved (another host, a new remote, a history restart). Project owners only. Opens ONE durable native form naming the old and the new repository; on approval the project keeps every task, memory record, work item and open session, the old repository is kept in its history, and this checkout is bound. Use it when project.resolve with bind refuses with recovery project.move_repository; never create a second project for moved code. Pass language as the BCP-47 tag of the language the user writes in; it is remembered. Reconsider a declined move with a new decisionAttempt; retries keep the same attempt.',
@@ -337,7 +201,7 @@ export function registerOnboardingTools(server, service) {
337
201
  presentation: z.enum(['host_native']).optional(),
338
202
  }),
339
203
  }, async (input, context) => {
340
- const language = (await service.language(input.language))?.startsWith('tr') ? 'tr' : 'en';
204
+ const wording = copies(moveWording, 'move', await service.language(input.language));
341
205
  const preflight = await service.projectRepositoryMovePreflight(input);
342
206
  if (!preflight.ok)
343
207
  return output(preflight);
@@ -371,46 +235,41 @@ export function registerOnboardingTools(server, service) {
371
235
  version: flight.history.lockVersion,
372
236
  ...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
373
237
  }));
374
- const definitions = ['tr', 'en'].map((displayLanguage) => {
375
- const copy = moveCopy[displayLanguage];
376
- return {
377
- questionnaireId,
378
- language: displayLanguage,
379
- message: [
380
- copy.ask(name),
381
- `${copy.from}: ${from ?? copy.unknown}`,
382
- `${copy.to}: ${to ?? copy.unknown}`,
383
- ].join('\n'),
384
- context: [
385
- copy.why,
386
- copy.keeps(records.tasks, records.memoryRecords, records.workItems),
387
- copy.after,
388
- ].join(' '),
389
- example: copy.example,
390
- options: [
391
- { id: 'approve', label: copy.approve[0], description: copy.approve[1] },
392
- { id: 'decline', label: copy.decline[0], description: copy.decline[1] },
393
- ],
394
- };
395
- });
396
- const previousWording = ['tr', 'en'].map((displayLanguage) => {
397
- const copy = moveCopy[displayLanguage];
398
- return {
399
- questionnaireId,
400
- language: displayLanguage,
401
- message: [
402
- copy.ask(name),
403
- `${copy.from}: ${from ?? copy.unknown}`,
404
- `${copy.to}: ${to ?? copy.unknown}`,
405
- copy.keeps(records.tasks, records.memoryRecords, records.workItems),
406
- copy.after,
407
- ].join('\n'),
408
- options: [
409
- { id: 'approve', label: copy.approve[0] },
410
- { id: 'decline', label: copy.decline[0] },
411
- ],
412
- };
413
- });
238
+ const counts = {
239
+ tasks: records.tasks,
240
+ records: records.memoryRecords,
241
+ items: records.workItems,
242
+ };
243
+ const definitions = wording.map(({ language, copy }) => ({
244
+ questionnaireId,
245
+ language,
246
+ message: [
247
+ format(copy.ask, language, { name }),
248
+ `${copy.from}: ${from ?? copy.unknown}`,
249
+ `${copy.to}: ${to ?? copy.unknown}`,
250
+ ].join('\n'),
251
+ context: [copy.why, format(copy.keeps, language, counts), copy.after].join(' '),
252
+ example: copy.example,
253
+ options: [
254
+ { id: 'approve', label: copy.approve.label, description: copy.approve.description },
255
+ { id: 'decline', label: copy.decline.label, description: copy.decline.description },
256
+ ],
257
+ }));
258
+ const previousWording = wording.map(({ language, copy }) => ({
259
+ questionnaireId,
260
+ language,
261
+ message: [
262
+ format(copy.ask, language, { name }),
263
+ `${copy.from}: ${from ?? copy.unknown}`,
264
+ `${copy.to}: ${to ?? copy.unknown}`,
265
+ format(copy.keeps, language, counts),
266
+ copy.after,
267
+ ].join('\n'),
268
+ options: [
269
+ { id: 'approve', label: copy.approve.label },
270
+ { id: 'decline', label: copy.decline.label },
271
+ ],
272
+ }));
414
273
  const binding = { decision: questionnaireId };
415
274
  const bound = definitions.map((entry) => ({ ...entry, binding }));
416
275
  const asked = {
@@ -422,7 +281,7 @@ export function registerOnboardingTools(server, service) {
422
281
  const form = stored?.binding && stableStringify(stored.binding) === stableStringify(binding)
423
282
  ? await resumeQuestionnaire(server, service, asked, context)
424
283
  : await askQuestionnaire(server, service, {
425
- ...bound.find((entry) => entry.language === language),
284
+ ...bound[0],
426
285
  repoRoot: input.repoRoot,
427
286
  presentation: input.presentation,
428
287
  }, context, [...bound, ...definitions, ...previousWording], {
@@ -550,36 +409,33 @@ export function registerOnboardingTools(server, service) {
550
409
  }),
551
410
  }, async (input, context) => {
552
411
  const { repoRoot: root, language: told, ...plan } = input;
553
- const language = (await service.language(told))?.startsWith('tr') ? 'tr' : 'en';
554
412
  const digest = sha256(stableStringify(plan));
555
413
  const questionnaireId = 'rework-' + digest;
556
414
  const shown = plan.phases.slice(0, 8);
557
- const definitions = ['tr', 'en'].map((displayLanguage) => {
558
- const copy = planCopy[displayLanguage];
559
- return {
560
- questionnaireId,
561
- language: displayLanguage,
562
- message: [
563
- plan.phases.length === 1 ? copy.one : copy.many(plan.phases.length),
564
- ...shown.map((phase, index) => {
565
- const branch = `${copy.branch}: ${phase.branch.slice(0, 60)}`;
566
- const line = `${index + 1}. ${phase.title.slice(0, 70)} (${branch})`;
567
- return restrictedValueKind(line, 'message', true) ? `${index + 1}. ${branch}` : line;
568
- }),
569
- ...(plan.phases.length > shown.length
570
- ? [copy.more(plan.phases.length - shown.length)]
571
- : []),
572
- ].join('\n'),
573
- context: copy.context,
574
- example: copy.example,
575
- options: [
576
- { id: 'approve', label: copy.approve[0], description: copy.approve[1] },
577
- { id: 'defer', label: copy.defer[0], description: copy.defer[1] },
578
- ],
579
- binding: { planDigest: digest },
580
- };
581
- });
582
- const answer = await askQuestionnaire(server, service, { ...definitions.find((entry) => entry.language === language), repoRoot: root }, context, [
415
+ const wording = copies(reworkWording, 'reworkPlan', await service.language(told));
416
+ const definitions = wording.map(({ language, copy }) => ({
417
+ questionnaireId,
418
+ language,
419
+ message: [
420
+ format(copy.phases, language, { count: plan.phases.length }),
421
+ ...shown.map((phase, index) => {
422
+ const branch = `${copy.branch}: ${phase.branch.slice(0, 60)}`;
423
+ const line = `${index + 1}. ${phase.title.slice(0, 70)} (${branch})`;
424
+ return restrictedValueKind(line, 'message', true) ? `${index + 1}. ${branch}` : line;
425
+ }),
426
+ ...(plan.phases.length > shown.length
427
+ ? [format(copy.more, language, { count: plan.phases.length - shown.length })]
428
+ : []),
429
+ ].join('\n'),
430
+ context: copy.context,
431
+ example: copy.example,
432
+ options: [
433
+ { id: 'approve', label: copy.approve.label, description: copy.approve.description },
434
+ { id: 'defer', label: copy.defer.label, description: copy.defer.description },
435
+ ],
436
+ binding: { planDigest: digest },
437
+ }));
438
+ const answer = await askQuestionnaire(server, service, { ...definitions[0], repoRoot: root }, context, [
583
439
  ...definitions,
584
440
  {
585
441
  questionnaireId,
@@ -644,52 +500,45 @@ export function registerOnboardingTools(server, service) {
644
500
  },
645
501
  });
646
502
  const { language: told, ...request } = input;
647
- const language = (await service.language(told))?.startsWith('tr') ? 'tr' : 'en';
503
+ const wording = copies(inspectionWording, 'inspection', await service.language(told));
648
504
  const pinned = { ...request, ref: commit };
649
505
  const digest = sha256(stableStringify(pinned));
650
506
  const reference = input.ref ?? 'HEAD';
651
507
  const sourceLabel = /^[0-9a-f]{40,64}$/i.test(reference)
652
508
  ? commit.slice(0, 12)
653
509
  : `${reference.slice(0, 80)} · ${commit.slice(0, 12)}`;
654
- const definitions = ['tr', 'en'].map((displayLanguage) => {
655
- const copy = inspectionCopy[displayLanguage];
656
- return {
657
- questionnaireId: 'sync-start-' + digest,
658
- language: displayLanguage,
659
- message: [
660
- copy.modes[input.mode],
661
- `${copy.source}: ${sourceLabel}`,
662
- ...(input.reworkBehavior ? [copy.rework[input.reworkBehavior]] : []),
663
- ].join('\n\n'),
664
- context: [copy.why[input.mode], copy.scope, copy.localChanges, copy.review].join(' '),
665
- example: copy.example,
666
- options: [
667
- { id: 'approve', label: copy.start, description: copy.startDetail },
668
- { id: 'defer', label: copy.defer, description: copy.deferDetail },
669
- ],
670
- };
671
- });
672
- const previousWording = ['tr', 'en'].map((displayLanguage) => {
673
- const copy = inspectionCopy[displayLanguage];
674
- return {
675
- questionnaireId: 'sync-start-' + digest,
676
- language: displayLanguage,
677
- message: [
678
- copy.modes[input.mode],
679
- `${copy.source}: ${sourceLabel}`,
680
- copy.scope,
681
- copy.localChanges,
682
- copy.review,
683
- ...(input.reworkBehavior ? [copy.rework[input.reworkBehavior]] : []),
684
- ].join('\n\n'),
685
- options: [
686
- { id: 'approve', label: copy.start },
687
- { id: 'defer', label: copy.defer },
688
- ],
689
- };
690
- });
691
- const definition = definitions.find((entry) => entry.language === language);
692
- const question = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, [
510
+ const definitions = wording.map(({ language, copy }) => ({
511
+ questionnaireId: 'sync-start-' + digest,
512
+ language,
513
+ message: [
514
+ copy.modes[input.mode],
515
+ `${copy.source}: ${sourceLabel}`,
516
+ ...(input.reworkBehavior ? [copy.rework[input.reworkBehavior]] : []),
517
+ ].join('\n\n'),
518
+ context: [copy.why[input.mode], copy.scope, copy.localChanges, copy.review].join(' '),
519
+ example: copy.example,
520
+ options: [
521
+ { id: 'approve', label: copy.start, description: copy.startDetail },
522
+ { id: 'defer', label: copy.defer, description: copy.deferDetail },
523
+ ],
524
+ }));
525
+ const previousWording = wording.map(({ language, copy }) => ({
526
+ questionnaireId: 'sync-start-' + digest,
527
+ language,
528
+ message: [
529
+ copy.modes[input.mode],
530
+ `${copy.source}: ${sourceLabel}`,
531
+ copy.scope,
532
+ copy.localChanges,
533
+ copy.review,
534
+ ...(input.reworkBehavior ? [copy.rework[input.reworkBehavior]] : []),
535
+ ].join('\n\n'),
536
+ options: [
537
+ { id: 'approve', label: copy.start },
538
+ { id: 'defer', label: copy.defer },
539
+ ],
540
+ }));
541
+ const question = await askQuestionnaire(server, service, { ...definitions[0], repoRoot: input.repoRoot }, context, [
693
542
  ...definitions,
694
543
  ...previousWording,
695
544
  {
@@ -875,7 +724,7 @@ export function registerOnboardingTools(server, service) {
875
724
  questionnaireId: 'memory-review-' + digest + '-' + input.reviewAttempt,
876
725
  };
877
726
  const binding = body({ projectId: input.projectId, entries });
878
- const language = (await service.language(input.language))?.startsWith('tr') ? 'tr' : 'en';
727
+ const { language, copy } = copies(reviewWording, 'reviewBatch', await service.language(input.language))[0];
879
728
  const stored = await service.questionnaireResume(question).catch(() => undefined);
880
729
  let result;
881
730
  if (stored && stableStringify(stored.binding) === stableStringify(binding))
@@ -908,7 +757,7 @@ export function registerOnboardingTools(server, service) {
908
757
  recovery: 'memory.list_proposals',
909
758
  },
910
759
  });
911
- result = await askQuestionnaire(server, service, { ...question, language, ...batchQuestion(language === 'tr', proposals), binding }, context);
760
+ result = await askQuestionnaire(server, service, { ...question, language, ...batchQuestion(copy, language, proposals), binding }, context);
912
761
  }
913
762
  const decision = answerChoice(result);
914
763
  if (decision === 'defer' || decision === 'one_by_one')
@@ -934,285 +783,110 @@ export function registerOnboardingTools(server, service) {
934
783
  })));
935
784
  });
936
785
  }
937
- const profileFieldNames = {
938
- tr: {
939
- stack: 'Uygulama teknolojisi',
940
- runtime: 'Çalışma ortamı ve araç sürümleri',
941
- architecture: 'Uygulama mimarisi',
942
- environments: 'Ortamlar ve konfigürasyon',
943
- serviceReadiness: 'Servislerin hazır olup olmadığı',
944
- apiContract: 'API sözleşmesi',
945
- authentication: 'Kimlik doğrulama',
946
- storage: 'Veri saklama',
947
- backendProjectIds: 'Bağlı backend projeleri',
948
- migrations: 'Migration stratejisi',
949
- figma: 'Figma kaynağı',
950
- colorPalette: 'Renk paleti',
951
- typography: 'Yazı tipleri ve metin stilleri',
952
- designSystem: 'Tasarım sistemi',
953
- navigation: 'Gezinme',
954
- platforms: 'Platformlar',
955
- localization: 'Desteklenen diller',
956
- },
957
- en: {
958
- stack: 'Application stack',
959
- runtime: 'Runtime and toolchain contract',
960
- architecture: 'Application architecture',
961
- environments: 'Environments and configuration',
962
- serviceReadiness: 'Service readiness',
963
- apiContract: 'API contract',
964
- authentication: 'Authentication',
965
- storage: 'Storage and persistence',
966
- backendProjectIds: 'Existing backend project links',
967
- migrations: 'Migration strategy',
968
- figma: 'Figma source',
969
- colorPalette: 'Color palette',
970
- typography: 'Font families and text styles',
971
- designSystem: 'Design system',
972
- navigation: 'Navigation',
973
- platforms: 'Platforms',
974
- localization: 'Localization languages',
975
- },
976
- };
977
- const onboardingCopy = {
978
- tr: {
979
- mode: {
980
- message: 'Bu proje Engineering Memory hafızasına nasıl alınsın?',
981
- context: 'Cevabın ilk adımı belirler: proje sıfırdan mı kurulacak, mevcut proje olduğu gibi okunup korunacak mı, yoksa önce okunup sonra aşama aşama mı yeniden yazılacak. Mevcut bir projeyi okumak ne kodu ne de mimarisini değiştirir.',
982
- example: 'Mevcut projeyi öğrenmeyi seçersen bir sonraki adım kodu okuyup nasıl çalıştığını kayda geçirir; tek satır bile değişmez.',
983
- greenfield: [
984
- 'Yeni ya da yeni başlamış bir proje kur',
985
- 'Proje birazdan onaylayacağın profille başlar; mevcut kod okunmaz.',
986
- ],
987
- adopt: [
988
- 'Mevcut mimariyi öğren ve koru',
989
- 'Kod olduğu gibi okunup hafızaya yazılır; hiçbir şey yeniden tasarlanmaz.',
990
- ],
991
- rework: [
992
- 'Önce öğren, sonra aşama aşama yeniden yazmayı planla',
993
- 'Önce kod okunur; yeniden yazım planı ayrıca onaylanmadan hiçbir şey değişmez.',
994
- ],
995
- refresh: [
996
- 'Hafızayı güncel projeyle tazele',
997
- 'Hafızadaki kayıtlar güncel kodla karşılaştırılır, farklar öneri olarak önüne gelir.',
998
- ],
999
- },
1000
- behavior: {
1001
- message: 'Yeniden yazım, ürünün bugünkü davranışını koruyacak mı yoksa ürünü yeniden mi tasarlayacak?',
1002
- context: 'Bu seçim hem incelemenin hem de planın hedefini belirler. Korumak, kullanıcının gördüğü ekranlar ve akışlar aynı kalırken altındaki kodun değişmesi demek. Yeniden tasarlamak ürünün kendisinin değişmesi demek; o zaman hedef ekranlar, akışlar ve sözleşmeler uygulamaya geçilmeden önce ayrıca onaylanır.',
1003
- example: 'Davranışı korursan, bugün seri numarası soran ekran yeniden yazımdan sonra da seri numarası sorar.',
1004
- preserve: [
1005
- 'Bugünkü davranışı koru',
1006
- 'Yeniden yazım yalnızca kodun yapısını değiştirir; kullanıcının gördüğü aynı kalır.',
1007
- ],
1008
- redesign: [
1009
- 'Ürünü açıkça yeniden tasarla',
1010
- 'Hedef ekranlar, akışlar ve sözleşmeler onaylanmadan hiçbir şey uygulanmaz.',
1011
- ],
1012
- },
1013
- field: {
1014
- value: 'Önerilen değer',
1015
- empty: 'henüz belirlenmedi',
1016
- context: 'Bu değer kurulum seçimleriyle birlikte kaydedilir; sonraki görevler tahmin etmek yerine buradan okur. Olduğu gibi bırakabilir, farklı bir değer yazabilir ya da şimdilik boş bırakabilirsin; boş bıraktığın bir değer sessizce kaydedilmez.',
1017
- example: 'Çalışma ortamı Node 22 olarak kalırsa sonraki görev makinede ne varsa ona göre değil, Node 22 üzerinde derleyip test eder.',
1018
- keep: ['Bu değeri olduğu gibi kaydet', 'Yukarıdaki değer aynen kaydedilir.'],
1019
- change: (value) => `${value} olarak değiştir`,
1020
- changeDetail: (value) => `Proje ${value} olarak kaydedilir ve kalan sorular buna göre gelir.`,
1021
- edit: [
1022
- 'Aşağıdaki alana farklı bir değer yaz',
1023
- 'Aşağıya yazdığın metin, yukarıdaki değerin yerine kaydedilir.',
1024
- ],
1025
- defer: [
1026
- 'Bu değeri şimdilik boş bırak',
1027
- 'Hiçbir şey kaydedilmez. Kurulum burada durur; karar verdiğinde bu soruya dönersin.',
1028
- ],
1029
- editTitle: 'Yeni değer',
1030
- idsTitle: 'Proje ID listesi (JSON dizisi)',
1031
- },
1032
- git: {
1033
- development: {
1034
- message: 'Yeni görev dalları hangi daldan başlasın?',
1035
- context: 'Bundan sonra açtığın her görev bu daldan dallanır ve bu dalla karşılaştırılır. Henüz var olmayan bir depo için ilk dalın alacağı ismi yaz. Dal, depo origin adresine bağlandıktan sonra orada kullanılır.',
1036
- example: 'Buraya main yazarsan sonraki görev main üzerinden dallanır ve pull request main dalına açılır.',
1037
- second: [
1038
- 'Şimdilik kuruluma başlama',
1039
- 'Hiçbir şey kaydedilmez, kurulum burada durur; istediğinde devam edebilirsin.',
1040
- ],
1041
- },
1042
- production: {
1043
- message: 'Bu projenin canlı sürümü hangi daldan çıkıyor?',
1044
- context: 'Bir kez kaydedilir; sonraki işler biten bir değişikliğin nereye gideceğini buradan bilir. Böyle bir dal henüz yoksa bunu açıkça söyle, bir daha sorulmaz.',
1045
- example: 'Canlı dal main ise, biten bir görevin nereye gideceği sana tekrar sorulmaz.',
1046
- second: [
1047
- 'Henüz canlı dal yok',
1048
- 'Yok olarak kaydedilir ve bir daha sorulmaz. İleride belirleyebilirsin.',
1049
- ],
1050
- },
1051
- test: {
1052
- message: 'Bu proje hangi dalda test ediliyor?',
1053
- context: 'Bir kez kaydedilir; sonraki işler biten bir değişikliğin nerede test edileceğini buradan bilir. Böyle bir dal henüz yoksa bunu açıkça söyle, bir daha sorulmaz.',
1054
- example: 'Test dalı develop ise, testlerin hangi dalda koştuğu sana tekrar sorulmaz.',
1055
- second: [
1056
- 'Henüz test dalı yok',
1057
- 'Yok olarak kaydedilir ve bir daha sorulmaz. İleride belirleyebilirsin.',
1058
- ],
1059
- },
1060
- set: ['Aşağıya yazdığım dalı kullan', 'Yazdığın dal bu proje için kaydedilir.'],
1061
- branchTitle: 'Dal adı',
1062
- },
1063
- save: {
1064
- message: 'Bu kurulum seçimlerini kaydedelim mi?',
1065
- routeLabel: 'Seçilen yol',
1066
- route: {
1067
- greenfield: 'yeni bir proje kur',
1068
- adopt: 'mevcut projeyi öğren ve koru',
1069
- rework: 'önce öğren, sonra aşama aşama yeniden yazmayı planla',
1070
- refresh: 'hafızayı güncel projeyle tazele',
1071
- },
1072
- context: 'Verdiğin cevaplar bu projenin kurulum kaydı olur; sonraki işler teknolojiyi, mimariyi ve kalan seçimleri oradan okur. Hafıza kayıtları ve proje bağlantıları ayrıca onaylanır, burada hafızaya hiçbir şey yayınlanmaz.',
1073
- git: ' Bu klasörde Git hazırlanır; mevcut dosyalar olduğu gibi kalır, hiçbiri silinmez veya değiştirilmez.',
1074
- example: 'Kaydettikten sonra sonraki görev teknolojiyi ve mimariyi sana tekrar sormadan buradan okur.',
1075
- approve: ['Bu seçimleri kaydet', 'Seçimler kaydedilir. Kodda hiçbir şey değişmez.'],
1076
- approveGit: 'Seçimler kaydedilir ve bu klasörde Git hazırlanır.',
1077
- defer: [
1078
- 'Seçimleri beklemede bırak',
1079
- 'Hiçbir şey kaydedilmez. Soruları sonra yeniden geçebilirsin.',
1080
- ],
1081
- },
1082
- },
1083
- en: {
1084
- mode: {
1085
- message: 'How should this project enter Engineering Memory?',
1086
- context: 'The answer decides what happens first: the project is set up from scratch, the existing one is read and preserved as it is, or it is read first and then reworked phase by phase. Reading an existing project never changes its code or its architecture.',
1087
- example: 'Choosing to learn the existing project means the next step reads the code and writes down how it works, without changing a line.',
1088
- greenfield: [
1089
- 'Set up a new or barely-started project',
1090
- 'The project starts from the profile you approve next; no existing code is read.',
1091
- ],
1092
- adopt: [
1093
- 'Learn and preserve the existing architecture',
1094
- 'The code is read as it stands and written into memory; nothing is redesigned.',
1095
- ],
1096
- rework: [
1097
- 'Learn first, then plan a phased rework',
1098
- 'The code is read first, and nothing changes until the rework plan is approved separately.',
1099
- ],
1100
- refresh: [
1101
- 'Refresh memory against the current project',
1102
- 'The records already in memory are compared with the current code and the differences come back as proposals.',
1103
- ],
1104
- },
1105
- behavior: {
1106
- message: 'Should the rework keep the product behaving as it does today, or redesign it?',
1107
- context: 'This decides what both the inspection and the plan aim at. Preserving means the screens and flows a user sees stay the same while the code underneath changes. Redesigning means the product itself changes, and the target screens, flows and contracts are approved before any implementation starts.',
1108
- example: 'If the behavior is preserved, a screen that asks for a serial number today still asks for it after the rework.',
1109
- preserve: [
1110
- 'Preserve current behavior',
1111
- 'The rework only changes how the code is built; what the user sees stays the same.',
1112
- ],
1113
- redesign: [
1114
- 'Redesign the product explicitly',
1115
- 'Nothing is implemented until the target screens, flows and contracts are approved.',
1116
- ],
1117
- },
1118
- field: {
1119
- value: 'Proposed value',
1120
- empty: 'not set yet',
1121
- context: 'This value is saved with the onboarding choices and later work reads it from there instead of guessing. Keep it, write a different one, or leave it undecided; a value left undecided is never saved quietly.',
1122
- example: 'If the runtime stays Node 22, a later task builds and tests on Node 22 instead of whatever the machine happens to have.',
1123
- keep: ['Keep this exact value', 'The value above is saved as it stands.'],
1124
- change: (value) => `Change to ${value}`,
1125
- changeDetail: (value) => `The project is saved as ${value} and the remaining questions follow it.`,
1126
- edit: [
1127
- 'Edit this value in the field below',
1128
- 'What you write below is saved instead of the value above.',
1129
- ],
1130
- defer: [
1131
- 'Defer this value explicitly',
1132
- 'Nothing is saved. Onboarding stops here and you come back to this question once you have decided.',
1133
- ],
1134
- editTitle: 'Edited value',
1135
- idsTitle: 'Project ID JSON array',
1136
- },
1137
- git: {
1138
- development: {
1139
- message: 'Which branch should new task branches start from?',
1140
- context: 'Every task you start later branches off this one and is compared against it. For a repository that does not exist yet, write the name its first branch will have. The branch is used on the origin remote once that remote is connected.',
1141
- example: 'With main here, a later task branches off main and its pull request targets main.',
1142
- second: [
1143
- 'Do not start setup yet',
1144
- 'Nothing is saved and setup stops here; you can continue whenever you want.',
1145
- ],
1146
- },
1147
- production: {
1148
- message: 'Which branch is this project released from?',
1149
- context: 'It is recorded once so later work knows where a finished change goes. If there is no such branch yet, say so explicitly and you are not asked again.',
1150
- example: 'With main as the release branch, where a finished task goes is never asked again.',
1151
- second: [
1152
- 'There is no release branch yet',
1153
- 'It is recorded as absent and you are not asked again. You can set it later.',
1154
- ],
1155
- },
1156
- test: {
1157
- message: 'Which branch is this project tested on?',
1158
- context: 'It is recorded once so later work knows where a finished change is tested. If there is no such branch yet, say so explicitly and you are not asked again.',
1159
- example: 'With develop as the test branch, where the tests run is never asked again.',
1160
- second: [
1161
- 'There is no test branch yet',
1162
- 'It is recorded as absent and you are not asked again. You can set it later.',
1163
- ],
1164
- },
1165
- set: ['Use the branch named below', 'That branch is saved for this project.'],
1166
- branchTitle: 'Branch name',
1167
- },
1168
- save: {
1169
- message: 'Save the onboarding choices you just went through?',
1170
- routeLabel: 'Chosen route',
1171
- route: {
1172
- greenfield: 'set up a new project',
1173
- adopt: 'learn and preserve the existing project',
1174
- rework: 'learn first, then plan a phased rework',
1175
- refresh: 'refresh memory against the current project',
1176
- },
1177
- context: 'Your answers become the onboarding record of this project, and later work reads the stack, the architecture and the remaining choices from there. Memory records and project links are approved separately; nothing is published to memory here.',
1178
- git: ' Git is prepared in this folder: existing files stay as they are and none of them is deleted or replaced.',
1179
- example: 'After saving, a later task reads the stack and the architecture from here instead of asking you again.',
1180
- approve: [
1181
- 'Save these exact onboarding choices',
1182
- 'The choices are saved. Nothing in the code changes.',
1183
- ],
1184
- approveGit: 'The choices are saved and Git is prepared in this folder.',
1185
- defer: [
1186
- 'Keep the choices pending',
1187
- 'Nothing is saved. You can go through the questions again later.',
1188
- ],
1189
- },
1190
- },
1191
- };
786
+ const profileField = z.enum([
787
+ 'stack',
788
+ 'runtime',
789
+ 'architecture',
790
+ 'environments',
791
+ 'serviceReadiness',
792
+ 'apiContract',
793
+ 'authentication',
794
+ 'storage',
795
+ 'backendProjectIds',
796
+ 'migrations',
797
+ 'figma',
798
+ 'colorPalette',
799
+ 'typography',
800
+ 'designSystem',
801
+ 'navigation',
802
+ 'platforms',
803
+ 'localization',
804
+ ]);
805
+ const branchQuestion = z.strictObject({
806
+ message: text,
807
+ context: text,
808
+ example: text,
809
+ second: option,
810
+ });
811
+ const onboardingWording = z.strictObject({
812
+ mode: z.strictObject({
813
+ message: text,
814
+ context: text,
815
+ example: text,
816
+ greenfield: option,
817
+ adopt: option,
818
+ rework: option,
819
+ refresh: option,
820
+ }),
821
+ behavior: z.strictObject({
822
+ message: text,
823
+ context: text,
824
+ example: text,
825
+ preserve: option,
826
+ redesign: option,
827
+ }),
828
+ field: z.strictObject({
829
+ value: text,
830
+ empty: text,
831
+ context: text,
832
+ example: text,
833
+ keep: option,
834
+ change: text,
835
+ changeDetail: text,
836
+ edit: option,
837
+ defer: option,
838
+ editTitle: text,
839
+ idsTitle: text,
840
+ }),
841
+ git: z.strictObject({
842
+ development: branchQuestion,
843
+ production: branchQuestion,
844
+ test: branchQuestion,
845
+ set: option,
846
+ branchTitle: text,
847
+ }),
848
+ save: z.strictObject({
849
+ message: text,
850
+ routeLabel: text,
851
+ route: z.record(onboardingMode, text),
852
+ context: text,
853
+ git: text,
854
+ example: text,
855
+ approve: option,
856
+ approveGit: text,
857
+ defer: option,
858
+ }),
859
+ fieldNames: z.record(profileField, text),
860
+ });
1192
861
  async function approveOnboarding(server, service, input, context, initialize) {
1193
862
  const preparation = !input.projectId;
1194
863
  const scope = { repoRoot: input.repoRoot, preparation };
1195
- const language = (await service.language(input.language))?.startsWith('tr') ? 'tr' : 'en';
864
+ const wording = copies(onboardingWording, 'onboarding', await service.language(input.language));
865
+ const english = onboardingWording.parse(texts('onboarding', 'en'));
1196
866
  const modeId = 'onboard-mode-' + sha256(input.requestKey);
1197
- const modeDefinitions = ['tr', 'en'].map((displayLanguage) => {
1198
- const copy = onboardingCopy[displayLanguage].mode;
867
+ const modeDefinitions = wording.map((entry) => {
868
+ const copy = entry.copy.mode;
1199
869
  return {
1200
870
  questionnaireId: modeId,
1201
- language: displayLanguage,
871
+ language: entry.language,
1202
872
  message: copy.message,
1203
873
  context: copy.context,
1204
874
  example: copy.example,
1205
875
  options: [
1206
- { id: 'greenfield', label: copy.greenfield[0], description: copy.greenfield[1] },
1207
- { id: 'adopt', label: copy.adopt[0], description: copy.adopt[1] },
1208
- { id: 'rework', label: copy.rework[0], description: copy.rework[1] },
876
+ {
877
+ id: 'greenfield',
878
+ label: copy.greenfield.label,
879
+ description: copy.greenfield.description,
880
+ },
881
+ { id: 'adopt', label: copy.adopt.label, description: copy.adopt.description },
882
+ { id: 'rework', label: copy.rework.label, description: copy.rework.description },
1209
883
  ...(input.projectId
1210
- ? [{ id: 'refresh', label: copy.refresh[0], description: copy.refresh[1] }]
884
+ ? [{ id: 'refresh', label: copy.refresh.label, description: copy.refresh.description }]
1211
885
  : []),
1212
886
  ],
1213
887
  };
1214
888
  });
1215
- const mode = await askQuestionnaire(server, service, { ...scope, ...modeDefinitions.find((entry) => entry.language === language) }, context, [
889
+ const mode = await askQuestionnaire(server, service, { ...scope, ...modeDefinitions[0] }, context, [
1216
890
  ...modeDefinitions,
1217
891
  {
1218
892
  questionnaireId: modeId,
@@ -1240,21 +914,21 @@ async function approveOnboarding(server, service, input, context, initialize) {
1240
914
  });
1241
915
  if (selected === 'rework') {
1242
916
  const behaviorId = 'rework-behavior-' + sha256(input.requestKey);
1243
- const behaviorDefinitions = ['tr', 'en'].map((displayLanguage) => {
1244
- const copy = onboardingCopy[displayLanguage].behavior;
917
+ const behaviorDefinitions = wording.map((entry) => {
918
+ const copy = entry.copy.behavior;
1245
919
  return {
1246
920
  questionnaireId: behaviorId,
1247
- language: displayLanguage,
921
+ language: entry.language,
1248
922
  message: copy.message,
1249
923
  context: copy.context,
1250
924
  example: copy.example,
1251
925
  options: [
1252
- { id: 'preserve', label: copy.preserve[0], description: copy.preserve[1] },
1253
- { id: 'redesign', label: copy.redesign[0], description: copy.redesign[1] },
926
+ { id: 'preserve', label: copy.preserve.label, description: copy.preserve.description },
927
+ { id: 'redesign', label: copy.redesign.label, description: copy.redesign.description },
1254
928
  ],
1255
929
  };
1256
930
  });
1257
- const behavior = await askQuestionnaire(server, service, { ...scope, ...behaviorDefinitions.find((entry) => entry.language === language) }, context, [
931
+ const behavior = await askQuestionnaire(server, service, { ...scope, ...behaviorDefinitions[0] }, context, [
1258
932
  ...behaviorDefinitions,
1259
933
  {
1260
934
  questionnaireId: behaviorId,
@@ -1310,18 +984,15 @@ async function approveOnboarding(server, service, input, context, initialize) {
1310
984
  requiredForChoice: 'edit_value',
1311
985
  ...(field === 'backendProjectIds' ? { valueType: 'uuid_list' } : {}),
1312
986
  };
1313
- const fieldDefinitions = ['tr', 'en'].map((displayLanguage) => {
1314
- const copy = onboardingCopy[displayLanguage].field;
987
+ const fieldDefinitions = wording.map((entry) => {
988
+ const copy = entry.copy.field;
1315
989
  return {
1316
990
  questionnaireId: fieldId,
1317
- language: displayLanguage,
1318
- message: [
1319
- profileFieldNames[displayLanguage][field],
1320
- `${copy.value}: ${written || copy.empty}`,
1321
- ].join('\n'),
991
+ language: entry.language,
992
+ message: [entry.copy.fieldNames[field], `${copy.value}: ${written || copy.empty}`].join('\n'),
1322
993
  context: copy.context,
1323
994
  example: copy.example,
1324
- options: profileFieldOptions(field, currentValue, copy),
995
+ options: profileFieldOptions(field, currentValue, copy, entry.language),
1325
996
  ...(textField
1326
997
  ? {
1327
998
  textField: {
@@ -1332,13 +1003,13 @@ async function approveOnboarding(server, service, input, context, initialize) {
1332
1003
  : {}),
1333
1004
  };
1334
1005
  });
1335
- const response = await askQuestionnaire(server, service, { ...scope, ...fieldDefinitions.find((entry) => entry.language === language) }, context, [
1006
+ const response = await askQuestionnaire(server, service, { ...scope, ...fieldDefinitions[0] }, context, [
1336
1007
  ...fieldDefinitions,
1337
1008
  {
1338
1009
  questionnaireId: fieldId,
1339
1010
  message: 'Review this onboarding choice before it is persisted.\n' +
1340
1011
  'Field: ' +
1341
- profileFieldNames.en[field] +
1012
+ english.fieldNames[field] +
1342
1013
  '\nCurrent value:\n' +
1343
1014
  JSON.stringify(currentValue, null, 2) +
1344
1015
  '\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.',
@@ -1378,27 +1049,27 @@ async function approveOnboarding(server, service, input, context, initialize) {
1378
1049
  gitPreferences = {};
1379
1050
  for (const role of ['development', 'production', 'test']) {
1380
1051
  const branchId = 'onboard-git-' + role + '-' + sha256(input.requestKey);
1381
- const branchDefinitions = ['tr', 'en'].map((displayLanguage) => {
1382
- const copy = onboardingCopy[displayLanguage].git;
1052
+ const branchDefinitions = wording.map((entry) => {
1053
+ const copy = entry.copy.git;
1383
1054
  return {
1384
1055
  questionnaireId: branchId,
1385
- language: displayLanguage,
1056
+ language: entry.language,
1386
1057
  message: copy[role].message,
1387
1058
  context: copy[role].context,
1388
1059
  example: copy[role].example,
1389
1060
  options: [
1390
- { id: 'set', label: copy.set[0], description: copy.set[1] },
1061
+ { id: 'set', label: copy.set.label, description: copy.set.description },
1391
1062
  {
1392
1063
  id: role === 'development' ? 'defer' : 'none',
1393
- label: copy[role].second[0],
1394
- description: copy[role].second[1],
1064
+ label: copy[role].second.label,
1065
+ description: copy[role].second.description,
1395
1066
  },
1396
1067
  ],
1397
1068
  textField: { title: copy.branchTitle, maxLength: 255, requiredForChoice: 'set' },
1398
1069
  allowFreeText: false,
1399
1070
  };
1400
1071
  });
1401
- const response = await askQuestionnaire(server, service, { ...scope, ...branchDefinitions.find((entry) => entry.language === language) }, context, [
1072
+ const response = await askQuestionnaire(server, service, { ...scope, ...branchDefinitions[0] }, context, [
1402
1073
  ...branchDefinitions,
1403
1074
  {
1404
1075
  questionnaireId: branchId,
@@ -1438,26 +1109,26 @@ async function approveOnboarding(server, service, input, context, initialize) {
1438
1109
  };
1439
1110
  const digest = sha256(stableStringify(choice));
1440
1111
  const questionnaireId = 'onboard-profile-' + digest + (initialize ? '-initialize' : '-save');
1441
- const saveDefinitions = ['tr', 'en'].map((displayLanguage) => {
1442
- const copy = onboardingCopy[displayLanguage].save;
1112
+ const saveDefinitions = wording.map((entry) => {
1113
+ const copy = entry.copy.save;
1443
1114
  return {
1444
1115
  questionnaireId,
1445
- language: displayLanguage,
1116
+ language: entry.language,
1446
1117
  message: [copy.message, `${copy.routeLabel}: ${copy.route[input.mode]}`].join('\n'),
1447
- context: copy.context + (initialize ? copy.git : ''),
1118
+ context: initialize ? `${copy.context} ${copy.git}` : copy.context,
1448
1119
  example: copy.example,
1449
1120
  options: [
1450
1121
  {
1451
1122
  id: 'approve',
1452
- label: copy.approve[0],
1453
- description: initialize ? copy.approveGit : copy.approve[1],
1123
+ label: copy.approve.label,
1124
+ description: initialize ? copy.approveGit : copy.approve.description,
1454
1125
  },
1455
- { id: 'defer', label: copy.defer[0], description: copy.defer[1] },
1126
+ { id: 'defer', label: copy.defer.label, description: copy.defer.description },
1456
1127
  ],
1457
1128
  binding: { choiceDigest: digest },
1458
1129
  };
1459
1130
  });
1460
- const approval = await askQuestionnaire(server, service, { ...scope, ...saveDefinitions.find((entry) => entry.language === language) }, context, [
1131
+ const approval = await askQuestionnaire(server, service, { ...scope, ...saveDefinitions[0] }, context, [
1461
1132
  ...saveDefinitions,
1462
1133
  {
1463
1134
  questionnaireId,
@@ -1519,26 +1190,26 @@ function parseEditedProfileValue(field, value) {
1519
1190
  return z.array(uuid).max(10).parse(parsed);
1520
1191
  }
1521
1192
  const stackValues = ['Flutter', 'React', 'Nest', 'Spring'];
1522
- function profileFieldOptions(field, currentValue, copy) {
1193
+ function profileFieldOptions(field, currentValue, copy, language) {
1523
1194
  if (field === 'stack') {
1524
1195
  return [
1525
- { id: 'keep_current', label: copy.keep[0], description: copy.keep[1] },
1196
+ { id: 'keep_current', label: copy.keep.label, description: copy.keep.description },
1526
1197
  ...stackValues
1527
1198
  .filter((value) => value !== currentValue)
1528
1199
  .map((value) => ({
1529
1200
  id: value,
1530
- label: copy.change(value),
1531
- description: copy.changeDetail(value),
1201
+ label: format(copy.change, language, { value }),
1202
+ description: format(copy.changeDetail, language, { value }),
1532
1203
  })),
1533
- { id: 'defer_current', label: copy.defer[0], description: copy.defer[1] },
1204
+ { id: 'defer_current', label: copy.defer.label, description: copy.defer.description },
1534
1205
  ];
1535
1206
  }
1536
1207
  return [
1537
1208
  ...(currentValue === null
1538
1209
  ? []
1539
- : [{ id: 'keep_current', label: copy.keep[0], description: copy.keep[1] }]),
1540
- { id: 'edit_value', label: copy.edit[0], description: copy.edit[1] },
1541
- { id: 'defer_current', label: copy.defer[0], description: copy.defer[1] },
1210
+ : [{ id: 'keep_current', label: copy.keep.label, description: copy.keep.description }]),
1211
+ { id: 'edit_value', label: copy.edit.label, description: copy.edit.description },
1212
+ { id: 'defer_current', label: copy.defer.label, description: copy.defer.description },
1542
1213
  ];
1543
1214
  }
1544
1215
  function legacyProfileFieldOptions(field, currentValue) {