vibe-coding-master 0.4.41 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/api/task-routes.js +7 -0
- package/dist/backend/api/translation-routes.js +11 -0
- package/dist/backend/errors.js +2 -0
- package/dist/backend/gateway/gateway-service.js +31 -61
- package/dist/backend/server.js +11 -1
- package/dist/backend/services/round-service.js +34 -0
- package/dist/backend/services/task-launch-service.js +91 -0
- package/dist/backend/services/translation-service.js +87 -0
- package/dist/backend/services/translation-worker-service.js +228 -72
- package/dist-frontend/assets/index-BaDS9Ohj.js +96 -0
- package/dist-frontend/index.html +1 -1
- package/docs/ARCHITECTURE.md +123 -0
- package/docs/TESTING.md +121 -73
- package/docs/known-issues.md +155 -0
- package/package.json +1 -1
- package/dist-frontend/assets/index-CsxS5H0d.js +0 -96
- package/docs/claude-code-translation-plan.md +0 -1268
- package/docs/full-harness-baseline.md +0 -160
- package/docs/gate-review-gates.md +0 -132
- package/docs/gateway-design.md +0 -813
- package/docs/v0.2-implementation-plan.md +0 -408
- package/docs/v0.4-harness-optimization-plan.md +0 -664
|
@@ -13,9 +13,22 @@ const FILE_RUNTIME_JOBS_DIR = `${TRANSLATIONS_RUNTIME_DIR}/files/jobs`;
|
|
|
13
13
|
const BOOTSTRAP_INDEX_PATH = `${TRANSLATIONS_ROOT}/bootstrap/index.json`;
|
|
14
14
|
const BOOTSTRAP_RUNTIME_RUNS_DIR = `${TRANSLATIONS_RUNTIME_DIR}/bootstrap/runs`;
|
|
15
15
|
const CONVERSATION_RUNTIME_DIR = `${TRANSLATIONS_RUNTIME_DIR}/conversations`;
|
|
16
|
+
// Single shared, self-describing conversation output file: one folder + one file
|
|
17
|
+
// for all conversation translation. It carries the batch identity plus per-index
|
|
18
|
+
// results so crash recovery can re-associate the file with the active queue item
|
|
19
|
+
// without a unique per-task/per-batch path. See ConversationBatchResultFile.
|
|
20
|
+
const CONVERSATION_RESULT_PATH = `${CONVERSATION_RUNTIME_DIR}/result.json`;
|
|
16
21
|
const MEMORY_UPDATE_RUNTIME_DIR = `${TRANSLATIONS_RUNTIME_DIR}/memory-updates`;
|
|
17
22
|
const DEFAULT_PROFILE = "default";
|
|
18
23
|
const DEFAULT_CHUNK_SOURCE_TOKEN_TARGET = 80000;
|
|
24
|
+
// In-flight conversation queue items normally finalize when the Translator
|
|
25
|
+
// session's Stop/StopFailure hook reaches the backend. If that hook is lost
|
|
26
|
+
// (session crash, backend restart/reconnect) a conversation item with no result
|
|
27
|
+
// on disk would block the queue head forever. Treat such an item as stuck once it
|
|
28
|
+
// has been in-flight past this bound and release it so later items can dispatch.
|
|
29
|
+
// Kept comfortably above a normal short composer translation, so a genuinely
|
|
30
|
+
// running conversation turn is never released mid-flight.
|
|
31
|
+
const STALE_CONVERSATION_ITEM_MS = 90000;
|
|
19
32
|
const BOOTSTRAP_DEFAULT_LIMIT = 12;
|
|
20
33
|
const MEMORY_TOTAL_LIMIT_BYTES = 80 * 1024;
|
|
21
34
|
const MEMORY_INITIALIZED_MIN_FILES = 2;
|
|
@@ -183,12 +196,15 @@ export function createTranslationWorkerService(deps) {
|
|
|
183
196
|
if (!deps.runtime || !deps.sessionService) {
|
|
184
197
|
return;
|
|
185
198
|
}
|
|
186
|
-
|
|
199
|
+
let queue = await loadQueue(repoRoot);
|
|
187
200
|
const active = queue.activeItemId
|
|
188
201
|
? queue.items.find((item) => item.id === queue.activeItemId)
|
|
189
202
|
: undefined;
|
|
190
203
|
if (active && ["dispatching", "running", "validating"].includes(active.status)) {
|
|
191
|
-
|
|
204
|
+
if (!(await reconcileStuckActiveItem(repoRoot, active))) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
queue = await loadQueue(repoRoot);
|
|
192
208
|
}
|
|
193
209
|
const next = queue.items.find((item) => item.status === "queued");
|
|
194
210
|
if (!next) {
|
|
@@ -236,11 +252,14 @@ export function createTranslationWorkerService(deps) {
|
|
|
236
252
|
}
|
|
237
253
|
}
|
|
238
254
|
async function prepareConversationBatch(repoRoot, queue, leader) {
|
|
239
|
-
const candidates =
|
|
255
|
+
const candidates = collectConversationBatchItems(queue, leader);
|
|
240
256
|
const batchId = `batch-${Date.now()}-${createId().slice(0, 8)}`;
|
|
241
|
-
|
|
257
|
+
// Every batched item points at the single shared, self-describing result.json
|
|
258
|
+
// (carrying batchId + per-index results); each item keeps its unique
|
|
259
|
+
// expectedResultPath as a logical lookup key.
|
|
260
|
+
const batchResultPath = CONVERSATION_RESULT_PATH;
|
|
242
261
|
await deps.fs.ensureDir(path.dirname(resolveRepoPath(repoRoot, batchResultPath)));
|
|
243
|
-
const prompt =
|
|
262
|
+
const prompt = buildConversationBatchPrompt(repoRoot, candidates, batchResultPath, batchId);
|
|
244
263
|
const timestamp = now();
|
|
245
264
|
candidates.forEach((item, index) => {
|
|
246
265
|
item.status = "dispatching";
|
|
@@ -301,9 +320,11 @@ export function createTranslationWorkerService(deps) {
|
|
|
301
320
|
"Complete the request described in request.json, then stop."
|
|
302
321
|
].filter(Boolean).join("\n");
|
|
303
322
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
323
|
+
function loadConversationRequest(item) {
|
|
324
|
+
// Conversation source is carried inline on the queue item (queue.json); there
|
|
325
|
+
// is no per-job request.json to read.
|
|
326
|
+
const source = item.conversation;
|
|
327
|
+
const sourceText = typeof source?.sourceText === "string" ? source.sourceText : "";
|
|
307
328
|
if (!sourceText.trim()) {
|
|
308
329
|
throw new VcmError({
|
|
309
330
|
code: "TRANSLATION_INPUT_EMPTY",
|
|
@@ -311,9 +332,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
311
332
|
statusCode: 400
|
|
312
333
|
});
|
|
313
334
|
}
|
|
314
|
-
|
|
315
|
-
const resultRelativePath = item.expectedResultPath ?? job?.resultPath;
|
|
316
|
-
if (!resultRelativePath) {
|
|
335
|
+
if (!item.expectedResultPath) {
|
|
317
336
|
throw new VcmError({
|
|
318
337
|
code: "TRANSLATION_RESULT_PATH_MISSING",
|
|
319
338
|
message: "Conversation translation result path is missing.",
|
|
@@ -321,17 +340,16 @@ export function createTranslationWorkerService(deps) {
|
|
|
321
340
|
});
|
|
322
341
|
}
|
|
323
342
|
return {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
targetLanguage: typeof request.targetLanguage === "string" ? request.targetLanguage : job?.targetLanguage ?? item.targetLanguage,
|
|
343
|
+
direction: source?.direction ?? "cc-output-to-user",
|
|
344
|
+
sourceHash: source?.sourceHash,
|
|
345
|
+
sourceLanguage: source?.sourceLanguage ?? "auto",
|
|
346
|
+
targetLanguage: item.targetLanguage,
|
|
347
|
+
contextText: source?.contextText,
|
|
330
348
|
sourceText
|
|
331
349
|
};
|
|
332
350
|
}
|
|
333
|
-
|
|
334
|
-
const leaderRequest =
|
|
351
|
+
function collectConversationBatchItems(queue, leader) {
|
|
352
|
+
const leaderRequest = loadConversationRequest(leader);
|
|
335
353
|
const leaderIndex = queue.items.findIndex((item) => item.id === leader.id);
|
|
336
354
|
if (leaderIndex < 0) {
|
|
337
355
|
return [leader];
|
|
@@ -343,7 +361,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
343
361
|
}
|
|
344
362
|
const request = candidate.id === leader.id
|
|
345
363
|
? leaderRequest
|
|
346
|
-
:
|
|
364
|
+
: loadConversationRequest(candidate);
|
|
347
365
|
if (request.sourceLanguage !== leaderRequest.sourceLanguage ||
|
|
348
366
|
request.targetLanguage !== leaderRequest.targetLanguage ||
|
|
349
367
|
request.direction !== leaderRequest.direction) {
|
|
@@ -353,18 +371,27 @@ export function createTranslationWorkerService(deps) {
|
|
|
353
371
|
}
|
|
354
372
|
return items;
|
|
355
373
|
}
|
|
356
|
-
|
|
357
|
-
|
|
374
|
+
function buildConversationBatchPrompt(repoRoot, items, batchResultPath, batchId) {
|
|
375
|
+
// Instruct the Translator to write one self-describing result.json (shape
|
|
376
|
+
// ConversationBatchResultFile) to the shared result path, echoing the exact
|
|
377
|
+
// batchId so crash recovery can verify identity and map each result entry to
|
|
378
|
+
// a queue item by index.
|
|
379
|
+
const requests = items.map((item) => loadConversationRequest(item));
|
|
358
380
|
const first = requests[0];
|
|
359
381
|
const absoluteResultPath = resolveRepoPath(repoRoot, batchResultPath);
|
|
382
|
+
const resultTemplate = {
|
|
383
|
+
version: 1,
|
|
384
|
+
batchId,
|
|
385
|
+
results: requests.map((_, index) => ({
|
|
386
|
+
index: index + 1,
|
|
387
|
+
translatedText: "<translated text>"
|
|
388
|
+
}))
|
|
389
|
+
};
|
|
360
390
|
return [
|
|
361
391
|
`Translate each <VCM_TEXT> item from ${first.sourceLanguage} to ${first.targetLanguage}. Write all results to Result Path: ${absoluteResultPath}`,
|
|
362
392
|
"",
|
|
363
|
-
"
|
|
364
|
-
|
|
365
|
-
"translated text",
|
|
366
|
-
"<VCM_RESULT2>",
|
|
367
|
-
"translated text",
|
|
393
|
+
"Write a single JSON file in exactly this shape. Echo the batchId verbatim and provide one results entry per <VCM_TEXT> index:",
|
|
394
|
+
JSON.stringify(resultTemplate, null, 2),
|
|
368
395
|
"",
|
|
369
396
|
...requests.flatMap((request, index) => [
|
|
370
397
|
`<VCM_TEXT${index + 1}>`,
|
|
@@ -404,6 +431,106 @@ export function createTranslationWorkerService(deps) {
|
|
|
404
431
|
"When finished, ensure the four memory files together are within budget, then stop."
|
|
405
432
|
].join("\n");
|
|
406
433
|
}
|
|
434
|
+
// Recover an active queue item whose finalizing Stop/StopFailure hook never
|
|
435
|
+
// arrived. Returns true when the item was finalized (and `activeItemId`
|
|
436
|
+
// cleared) so the caller can dispatch the next queued item; returns false when
|
|
437
|
+
// the item is still legitimately in flight and the queue head must be held.
|
|
438
|
+
async function reconcileStuckActiveItem(repoRoot, active) {
|
|
439
|
+
if (await activeItemResultAvailable(repoRoot, active)) {
|
|
440
|
+
await validateActiveQueueItem(repoRoot);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
if (active.type === "conversation" && isStaleActiveItem(active)) {
|
|
444
|
+
await validateActiveQueueItem(repoRoot);
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
async function activeItemResultAvailable(repoRoot, item) {
|
|
450
|
+
// For conversation items, defer to the all-or-nothing result.json predicate
|
|
451
|
+
// so a torn write or a leftover previous-batch file is treated as absent
|
|
452
|
+
// rather than mis-attributed. Non-conversation items keep the simple
|
|
453
|
+
// expected-output path-exists check.
|
|
454
|
+
if (item.type === "conversation") {
|
|
455
|
+
return conversationResultAvailable(repoRoot, item);
|
|
456
|
+
}
|
|
457
|
+
const resultPath = item.expectedResultPath;
|
|
458
|
+
if (!resultPath) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
return deps.fs.pathExists(resolveRepoPath(repoRoot, resultPath));
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Reader-side all-or-nothing availability predicate for the shared
|
|
465
|
+
* conversation result.json. Returns true ONLY when the file exists, parses,
|
|
466
|
+
* its `batchId` equals the active item's `batchId`, AND every expected
|
|
467
|
+
* `batchIndex` of the active batch is present. Any failed clause ⇒ false
|
|
468
|
+
* (treated as "no result this cycle"), which guarantees a torn write or a
|
|
469
|
+
* stale previous-batch file is never mis-assigned to the active item. This is
|
|
470
|
+
* the safety core that preserves the issue #13 / KI-010 crash recovery under a
|
|
471
|
+
* single shared output file.
|
|
472
|
+
*/
|
|
473
|
+
async function conversationResultAvailable(repoRoot, active) {
|
|
474
|
+
const parsed = await readMatchingConversationResult(repoRoot, active);
|
|
475
|
+
if (!parsed) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const queue = await loadQueue(repoRoot);
|
|
479
|
+
const expectedIndexes = conversationBatchIndexes(queue, active.batchId);
|
|
480
|
+
if (expectedIndexes.length === 0) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
const presentIndexes = new Set(parsed.results.map((entry) => entry.index));
|
|
484
|
+
return expectedIndexes.every((index) => presentIndexes.has(index));
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Read and parse the shared conversation result.json for the active item and
|
|
488
|
+
* confirm its self-describing `batchId` matches. Returns undefined when the
|
|
489
|
+
* file is absent, unparseable/torn, or identity-mismatched (foreign batch) so
|
|
490
|
+
* every caller treats those cases uniformly as "no result available".
|
|
491
|
+
*/
|
|
492
|
+
async function readMatchingConversationResult(repoRoot, active) {
|
|
493
|
+
if (active.type !== "conversation" || !active.batchId || !active.batchResultPath) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const resultPath = resolveRepoPath(repoRoot, active.batchResultPath);
|
|
497
|
+
if (!(await deps.fs.pathExists(resultPath))) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
const parsed = parseConversationResultFile(await deps.fs.readText(resultPath));
|
|
501
|
+
if (!parsed || parsed.batchId !== active.batchId) {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
return parsed;
|
|
505
|
+
}
|
|
506
|
+
function conversationBatchIndexes(queue, batchId) {
|
|
507
|
+
if (!batchId) {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
return queue.items
|
|
511
|
+
.filter((item) => item.type === "conversation" &&
|
|
512
|
+
item.batchId === batchId &&
|
|
513
|
+
["dispatching", "running", "validating"].includes(item.status))
|
|
514
|
+
.map((item) => item.batchIndex ?? 0);
|
|
515
|
+
}
|
|
516
|
+
// Fallback read for a batched conversation item whose translatedText was not
|
|
517
|
+
// captured at finalize: parse the identity-matched shared result.json and pick
|
|
518
|
+
// the entry for this item's batchIndex. Returns "" when no matching result.
|
|
519
|
+
async function readBatchedConversationResult(repoRoot, item) {
|
|
520
|
+
const parsed = await readMatchingConversationResult(repoRoot, item);
|
|
521
|
+
if (!parsed) {
|
|
522
|
+
return "";
|
|
523
|
+
}
|
|
524
|
+
const entry = parsed.results.find((candidate) => candidate.index === item.batchIndex);
|
|
525
|
+
return entry?.translatedText ?? "";
|
|
526
|
+
}
|
|
527
|
+
function isStaleActiveItem(item) {
|
|
528
|
+
const updatedAtMs = Date.parse(item.updatedAt ?? "");
|
|
529
|
+
if (!Number.isFinite(updatedAtMs)) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
return Date.now() - updatedAtMs >= STALE_CONVERSATION_ITEM_MS;
|
|
533
|
+
}
|
|
407
534
|
async function validateActiveQueueItem(repoRoot) {
|
|
408
535
|
const queue = await loadQueue(repoRoot);
|
|
409
536
|
const active = queue.activeItemId
|
|
@@ -455,14 +582,22 @@ export function createTranslationWorkerService(deps) {
|
|
|
455
582
|
}
|
|
456
583
|
queue.updatedAt = validatingAt;
|
|
457
584
|
await saveQueue(repoRoot, queue);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
585
|
+
// Read+parse the shared result.json and only apply results whose self-
|
|
586
|
+
// describing batchId matches the active batch; foreign or torn content yields
|
|
587
|
+
// an empty map so no item is completed from it. A missing expected index here
|
|
588
|
+
// is a genuine per-item failure: this finalize runs either on a Translator
|
|
589
|
+
// Stop/StopFailure hook (definitively done) or after the all-or-nothing
|
|
590
|
+
// availability predicate already confirmed every index is present (reconcile),
|
|
591
|
+
// so an absent index is never a still-writing batch.
|
|
592
|
+
const parsed = await readMatchingConversationResult(repoRoot, active);
|
|
593
|
+
const resultsByIndex = new Map();
|
|
594
|
+
for (const entry of parsed?.results ?? []) {
|
|
595
|
+
resultsByIndex.set(entry.index, entry.translatedText);
|
|
596
|
+
}
|
|
462
597
|
const completedAt = now();
|
|
463
598
|
for (const item of batchItems) {
|
|
464
599
|
const index = item.batchIndex ?? 0;
|
|
465
|
-
const translatedText =
|
|
600
|
+
const translatedText = resultsByIndex.get(index)?.trim();
|
|
466
601
|
if (translatedText) {
|
|
467
602
|
item.status = "completed";
|
|
468
603
|
item.translatedText = translatedText;
|
|
@@ -470,7 +605,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
470
605
|
}
|
|
471
606
|
else {
|
|
472
607
|
item.status = "failed";
|
|
473
|
-
item.error = `Missing translated result for
|
|
608
|
+
item.error = `Missing translated result for batch index ${index || "?"}.`;
|
|
474
609
|
}
|
|
475
610
|
item.updatedAt = completedAt;
|
|
476
611
|
}
|
|
@@ -1163,6 +1298,12 @@ export function createTranslationWorkerService(deps) {
|
|
|
1163
1298
|
createdAt: timestamp,
|
|
1164
1299
|
updatedAt: timestamp
|
|
1165
1300
|
};
|
|
1301
|
+
// Carry the conversation source inline on the queue item instead of writing
|
|
1302
|
+
// a per-job request.json. `expectedResultPath` stays a UNIQUE per-job
|
|
1303
|
+
// logical lookup key (no file is written there); only `batchResultPath`
|
|
1304
|
+
// points at the shared physical CONVERSATION_RESULT_PATH, set in
|
|
1305
|
+
// prepareConversationBatch.
|
|
1306
|
+
const contextText = input.contextText?.trim() ? input.contextText : undefined;
|
|
1166
1307
|
const queueItem = await enqueue(repoRoot, {
|
|
1167
1308
|
id: `queue-${jobId}`,
|
|
1168
1309
|
type: "conversation",
|
|
@@ -1171,41 +1312,37 @@ export function createTranslationWorkerService(deps) {
|
|
|
1171
1312
|
taskSlug,
|
|
1172
1313
|
jobId,
|
|
1173
1314
|
requestPath: job.requestPath,
|
|
1174
|
-
expectedResultPath: job.resultPath
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
direction: input.direction,
|
|
1182
|
-
sourceLanguage: job.sourceLanguage,
|
|
1183
|
-
targetLanguage,
|
|
1184
|
-
translationProfile: input.translationProfile?.trim() || DEFAULT_PROFILE,
|
|
1185
|
-
sourceContentBoundary: "VCM_TEXT",
|
|
1186
|
-
sourceText,
|
|
1187
|
-
absolutePaths: {
|
|
1188
|
-
requestPath: resolveRepoPath(repoRoot, job.requestPath),
|
|
1189
|
-
resultPath: resolveRepoPath(repoRoot, job.resultPath)
|
|
1190
|
-
},
|
|
1191
|
-
outputContract: {
|
|
1192
|
-
resultPath: job.resultPath,
|
|
1193
|
-
absoluteResultPath: resolveRepoPath(repoRoot, job.resultPath),
|
|
1194
|
-
format: "plain-text"
|
|
1315
|
+
expectedResultPath: job.resultPath,
|
|
1316
|
+
conversation: {
|
|
1317
|
+
direction: input.direction,
|
|
1318
|
+
sourceLanguage: job.sourceLanguage,
|
|
1319
|
+
sourceHash,
|
|
1320
|
+
sourceText,
|
|
1321
|
+
contextText
|
|
1195
1322
|
}
|
|
1196
1323
|
});
|
|
1324
|
+
job.queueItemId = queueItem.id;
|
|
1197
1325
|
if (!input.deferDispatch) {
|
|
1198
1326
|
void dispatchNext(repoRoot);
|
|
1199
1327
|
}
|
|
1200
1328
|
return job;
|
|
1201
1329
|
},
|
|
1202
1330
|
async validateConversationResult(repoRoot, input) {
|
|
1331
|
+
// The item lookup stays keyed on the unique `expectedResultPath` (unchanged
|
|
1332
|
+
// interface contract). When a finalized item already carries translatedText
|
|
1333
|
+
// it is authoritative; otherwise the fallback parses the shared result.json
|
|
1334
|
+
// (item.batchResultPath) and selects the entry matching this item's
|
|
1335
|
+
// batchId + batchIndex. With no matching queue item we read the given path
|
|
1336
|
+
// directly as a standalone result.
|
|
1203
1337
|
const queue = await loadQueue(repoRoot);
|
|
1204
1338
|
const item = queue.items.find((candidate) => candidate.type === "conversation" &&
|
|
1205
1339
|
candidate.expectedResultPath === input.resultPath);
|
|
1206
1340
|
const resultPath = resolveRepoPath(repoRoot, item?.batchResultPath ?? input.resultPath);
|
|
1207
1341
|
assertInsideRepo(repoRoot, resultPath);
|
|
1208
|
-
const translatedText = item?.translatedText
|
|
1342
|
+
const translatedText = item?.translatedText
|
|
1343
|
+
?? (item
|
|
1344
|
+
? await readBatchedConversationResult(repoRoot, item)
|
|
1345
|
+
: await readStandaloneConversationResult(repoRoot, input.resultPath, deps.fs));
|
|
1209
1346
|
if (!translatedText.trim()) {
|
|
1210
1347
|
throw invalidResult("Conversation translation result is empty.");
|
|
1211
1348
|
}
|
|
@@ -1538,9 +1675,6 @@ function isBootstrapRun(value) {
|
|
|
1538
1675
|
typeof candidate.targetLanguage === "string" &&
|
|
1539
1676
|
Array.isArray(candidate.candidatePaths);
|
|
1540
1677
|
}
|
|
1541
|
-
function isPartialConversationJob(value) {
|
|
1542
|
-
return typeof value === "object" && value !== null;
|
|
1543
|
-
}
|
|
1544
1678
|
function isFileTranslationChunk(value) {
|
|
1545
1679
|
const candidate = value;
|
|
1546
1680
|
return typeof candidate?.index === "number" &&
|
|
@@ -1808,21 +1942,43 @@ async function readStandaloneConversationResult(repoRoot, resultRelativePath, fs
|
|
|
1808
1942
|
}
|
|
1809
1943
|
return fs.readText(resultPath);
|
|
1810
1944
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1945
|
+
/**
|
|
1946
|
+
* Parse the shared conversation result.json into its structured form. Returns
|
|
1947
|
+
* undefined when the text is empty, not valid JSON, or does not match the
|
|
1948
|
+
* ConversationBatchResultFile shape (e.g. a truncated / torn write). Callers
|
|
1949
|
+
* MUST treat undefined as "no result available" — never partially apply a
|
|
1950
|
+
* malformed file — so recovery degrades safely to stale-release.
|
|
1951
|
+
*/
|
|
1952
|
+
function parseConversationResultFile(text) {
|
|
1953
|
+
if (!text.trim()) {
|
|
1954
|
+
return undefined;
|
|
1955
|
+
}
|
|
1956
|
+
let parsed;
|
|
1957
|
+
try {
|
|
1958
|
+
parsed = JSON.parse(text);
|
|
1959
|
+
}
|
|
1960
|
+
catch {
|
|
1961
|
+
return undefined;
|
|
1824
1962
|
}
|
|
1825
|
-
return
|
|
1963
|
+
return isConversationBatchResultFile(parsed) ? parsed : undefined;
|
|
1964
|
+
}
|
|
1965
|
+
function isConversationBatchResultFile(value) {
|
|
1966
|
+
if (typeof value !== "object" || value === null) {
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
const candidate = value;
|
|
1970
|
+
return candidate.version === 1 &&
|
|
1971
|
+
typeof candidate.batchId === "string" &&
|
|
1972
|
+
candidate.batchId.length > 0 &&
|
|
1973
|
+
Array.isArray(candidate.results) &&
|
|
1974
|
+
candidate.results.every(isConversationBatchResultEntry);
|
|
1975
|
+
}
|
|
1976
|
+
function isConversationBatchResultEntry(value) {
|
|
1977
|
+
const candidate = value;
|
|
1978
|
+
return typeof candidate?.index === "number" &&
|
|
1979
|
+
Number.isInteger(candidate.index) &&
|
|
1980
|
+
candidate.index >= 1 &&
|
|
1981
|
+
typeof candidate.translatedText === "string";
|
|
1826
1982
|
}
|
|
1827
1983
|
function assertInsideRepo(repoRoot, absolutePath) {
|
|
1828
1984
|
const relative = toRepoRelativePath(repoRoot, absolutePath);
|