From 2a8ec2ef9f0662b9d6386d8d42d68dd082516f2c Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:30:36 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(proforge):=20audit=20&=20perfection=20?= =?UTF-8?q?cycle=20=E2=80=94=20apply=20edits,=20persistence,=208-stage=20s?= =?UTF-8?q?upervisor,=20semantic=20RAG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full audit of the ProForge pipeline (flag `enableProForge`, default off) surfaced latent correctness bugs, dead code, doc drift, and i18n/a11y gaps. This cycle fixes them and adds the requested enhancements. Local gate green: lint + typecheck + i18n parity (11 locales); 417 ProForge + 169 LoRA unit tests pass. Correctness: - Fix hook getState self-recursion + stale state — read live store via new app/storeRef.ts (lightweight, no reducer-graph import); live config getter; per-project orchestrator rebuild. - Reset AbortController in startPipeline so abort→restart works. - SupervisorAgent now detects the real isFallback flag (was a stale qualityScore===50 sentinel). - Extract headSnapshotId() helper (dedupes 4× inline lookup). Apply accepted edits (core gap): new services/proForge/applyReviewEdits.ts writes accepted ReviewItems back into the manuscript (offset-safe back-to-front, text-match fallback, stale-skip) in submitReview before snapshotting, editing stages only; redux-undo makes them reversible. Remove dead code: toolRegistry (zero consumers), StyleAdaptedWriterAgent (unwired), PipelineConfig.loraAdapterId, and their tests. Enhancements: - Persist run history per project to IDB (proForgeHistoryStore, cap 20); hydrate on load. - SupervisorAgent gates all 8 stages (was 3). - Feedback-injected retries: BaseAgent.setRetryFeedback prepends supervisor reasons + reflection. - Memory-bank semantic RAG: search honours ragMode (lexical/semantic/hybrid via MiniLM), keyword fallback. i18n/a11y: PipelineReviewPanel fully t()-ized (~60 proforge.* keys, en+de translated, 10 locales propagated) with aria-hidden emoji, sr-only severity, aria-pressed/expanded, useAnnounce on submit. Docs: correct PROFORGE-PIPELINE.md (SupervisionDecision shape, supervisor rules, removed tool-calling section) and CLAUDE.md; rebuild graphify. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 11 +- app/store.ts | 8 +- app/storeRef.ts | 12 + components/proForge/PipelineReviewPanel.tsx | 139 ++++---- docs/PROFORGE-PIPELINE.md | 90 +++-- features/proForge/types.ts | 2 - graphify-out/GRAPH_REPORT.md | 324 ++++++++--------- hooks/useProForgeOrchestrator.ts | 54 ++- locales/ar/common.json | 55 +++ locales/de/common.json | 55 +++ locales/el/common.json | 55 +++ locales/en/common.json | 55 +++ locales/es/common.json | 55 +++ locales/es/settings.json | 36 +- locales/fr/common.json | 55 +++ locales/fr/settings.json | 36 +- locales/he/common.json | 55 +++ locales/it/common.json | 55 +++ locales/it/settings.json | 36 +- locales/ja/common.json | 55 +++ locales/pt/common.json | 55 +++ locales/zh/common.json | 55 +++ public/locales/ar/bundle.json | 55 +++ public/locales/de/bundle.json | 55 +++ public/locales/el/bundle.json | 55 +++ public/locales/en/bundle.json | 55 +++ public/locales/es/bundle.json | 89 ++++- public/locales/fr/bundle.json | 89 ++++- public/locales/he/bundle.json | 55 +++ public/locales/it/bundle.json | 89 ++++- public/locales/ja/bundle.json | 55 +++ public/locales/pt/bundle.json | 55 +++ public/locales/zh/bundle.json | 55 +++ services/proForge/applyReviewEdits.ts | 133 +++++++ services/proForge/pipelineAgents/baseAgent.ts | 22 +- .../pipelineAgents/styleAdaptedWriterAgent.ts | 119 ------- .../pipelineAgents/supervisorAgent.ts | 132 ++++++- .../proForge/pipelineTools/toolRegistry.ts | 303 ---------------- services/proForge/proForgeHistoryStore.ts | 65 ++++ services/proForge/proForgeMemoryBank.ts | 95 +++-- services/proForge/proForgeOrchestrator.ts | 88 ++++- .../unit/lora/styleAdaptedWriterAgent.test.ts | 173 --------- tests/unit/proForge/applyReviewEdits.test.ts | 127 +++++++ .../components/PipelineReviewPanel.test.tsx | 27 +- .../proForge/pipelineAgents/baseAgent.test.ts | 18 + .../pipelineAgents/supervisorAgent.test.ts | 139 +++++++- .../proForge/proForgeHistoryStore.test.ts | 70 ++++ .../unit/proForge/proForgeMemoryBank.test.ts | 56 ++- .../proForge/proForgeOrchestrator.test.ts | 176 +++++++++ tests/unit/proForge/toolRegistry.test.ts | 333 ------------------ 50 files changed, 2752 insertions(+), 1384 deletions(-) create mode 100644 app/storeRef.ts create mode 100644 services/proForge/applyReviewEdits.ts delete mode 100644 services/proForge/pipelineAgents/styleAdaptedWriterAgent.ts delete mode 100644 services/proForge/pipelineTools/toolRegistry.ts create mode 100644 services/proForge/proForgeHistoryStore.ts delete mode 100644 tests/unit/lora/styleAdaptedWriterAgent.test.ts create mode 100644 tests/unit/proForge/applyReviewEdits.test.ts create mode 100644 tests/unit/proForge/proForgeHistoryStore.test.ts delete mode 100644 tests/unit/proForge/toolRegistry.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 659c131ff..23a82dad3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,8 +91,9 @@ services/ → External adapters; key sub-dirs: duckdb/ (duckdbClient, duckdbSchema, duckdbAnalytics, duckdbMigration, ragVectorMigration) help/ (helpCatalog, helpSearch, helpDocRetrieval) keyboard/ (shortcut normalization, conflict detection) - proForge/ (proForgeOrchestrator, proForgeMemoryBank, pipelineAgents/ — baseAgent, - supervisorAgent + 8 stage agents; pipelineOutput/, pipelinePrompts/, pipelineTools/) + proForge/ (proForgeOrchestrator, proForgeMemoryBank, proForgeHistoryStore, + applyReviewEdits, pipelineAgents/ — baseAgent, supervisorAgent + 8 + stage agents; pipelineOutput/) storage/ (idbCore, idbProjectStore, idbSnapshotStore, idbKeyStore, idbCodexStore, idbAssetStore, storageEncryptionService — AES-256-GCM at-rest via B-1) voice/ (voiceCommandService, voiceTypes, stt/tts/vad/wakeWord/intent engines, @@ -300,11 +301,11 @@ All `.md` guides listed in **[`README.md`](README.md#-documentation-hub) § Docu ### ProForge Pipeline -8-stage agentic editing pipeline. Flag: `enableProForge`. Full docs: `docs/PROFORGE-PIPELINE.md`. Stages: `intake` → `structural` → `lineProse` → `copyEdit` → `proof` → `production` → `publishing` → `analytics`. Pauses at `awaitingReview` — manuscript never auto-modified without user approval. +8-stage agentic editing pipeline. Flag: `enableProForge`. Full docs: `docs/PROFORGE-PIPELINE.md`. Stages: `intake` → `structural` → `lineProse` → `copyEdit` → `proof` → `production` → `publishing` → `analytics`. Pauses at `awaitingReview` — manuscript never auto-modified without user approval. On review submit, accepted `ReviewItem`s for **editing stages** are applied back into the manuscript via `applyReviewEdits.ts` (offset-safe, back-to-front, stale-match skip) before the post-stage snapshot; redux-undo makes them reversible. -**Redux slice** (`features/proForge/proForgeSlice.ts`, root key `proForge`, NOT undo-wrapped). **Types** in `features/proForge/types.ts`. **Orchestrator:** `services/proForge/proForgeOrchestrator.ts` — call via `hooks/useProForgeOrchestrator.ts`, never instantiate in components. Agents (all 8 extend `BaseAgent`) lazy-loaded per stage; call `this.buildAiOpts({ maxTokens: N })` — `AIRequestOptions` requires `model` + `provider`. `BaseAgent` provides `requireProject()`, `getMemoryBank()`, `selfReflect()`, `elapsed()`. +**Redux slice** (`features/proForge/proForgeSlice.ts`, root key `proForge`, NOT undo-wrapped, **ephemeral**). Completed/aborted runs persist per project to IDB `proforge-run-history` (`proForgeHistoryStore.ts`, cap 20) and rehydrate via `useProForgeOrchestrator`. **Types** in `features/proForge/types.ts`. **Orchestrator:** `services/proForge/proForgeOrchestrator.ts` — call via `hooks/useProForgeOrchestrator.ts` (reads live state via `appStoreRef` from `app/storeRef.ts`), never instantiate in components. Agents (all 8 extend `BaseAgent`) lazy-loaded per stage; call `this.buildAiOpts({ maxTokens: N })` — `AIRequestOptions` requires `model` + `provider`. `BaseAgent` provides `requireProject()`, `getMemoryBank()`, `selfReflect()`, `elapsed()`, `setRetryFeedback()` (supervisor reasons injected into retry prompts). -**SupervisorAgent**: heuristic gate, no AI calls. `evaluate(stage, result)` → `{verdict: 'pass'|'retry'|'fail'}`. Hard gate: intake `qualityScore < 30` → fail. All `createFallback*` use 0 scores + `isFallback: true` — never fake mid-range values. **Memory Bank:** IDB `proforge-memory-bank`. **View:** `ProForgeViewContext` + `useProForgeViewContext()`. +**SupervisorAgent**: heuristic gate, no AI calls, gates **all 8 stages**. `evaluate(stage, result)` → `SupervisionDecision { pass, retryRecommended, qualityScore, reasons }`. Detects `isFallback: true` (intake/structural/proof). Hard gate: intake `qualityScore < 30` → fail. All `createFallback*` use 0 scores + `isFallback: true` — never fake mid-range values. **Memory Bank:** IDB `proforge-memory-bank`; `search(query, limit, mode)` honours `ragMode` (`lexical` | `semantic` | `hybrid` via MiniLM embeddings, keyword fallback). **View:** `ProForgeViewContext` + `useProForgeViewContext()`. ### Scene-level services diff --git a/app/store.ts b/app/store.ts index a17cb4bc8..9a72f9ba1 100644 --- a/app/store.ts +++ b/app/store.ts @@ -204,7 +204,7 @@ const _tempStore = configureStore({ reducer: rootReducer }); export type RootState = ReturnType; export type AppDispatch = typeof _tempStore.dispatch; -// Global store reference for non-React consumers (e.g., voice service) -export const appStoreRef = { - current: null as { getState(): RootState; dispatch: AppDispatch } | null, -}; +// Global store reference for non-React consumers (e.g., voice service, ProForge orchestrator). +// QNBS-v3: Defined in app/storeRef.ts so consumers can import the ref without pulling the whole +// reducer graph; re-exported here for backward compatibility. +export { appStoreRef } from './storeRef'; diff --git a/app/storeRef.ts b/app/storeRef.ts new file mode 100644 index 000000000..3f39cc1ba --- /dev/null +++ b/app/storeRef.ts @@ -0,0 +1,12 @@ +/** + * Global store reference for non-React consumers (e.g., voice service, ProForge orchestrator). + * QNBS-v3: Lives in its own module so importing the ref does NOT pull the entire store/reducer + * graph into the consumer — keeps hooks/services lightweight and unit-testable in isolation. + * Type-only imports from './store' are erased at runtime, so there is no runtime dependency cycle. + */ + +import type { AppDispatch, RootState } from './store'; + +export const appStoreRef = { + current: null as { getState(): RootState; dispatch: AppDispatch } | null, +}; diff --git a/components/proForge/PipelineReviewPanel.tsx b/components/proForge/PipelineReviewPanel.tsx index 238d90295..0318a53c7 100644 --- a/components/proForge/PipelineReviewPanel.tsx +++ b/components/proForge/PipelineReviewPanel.tsx @@ -6,9 +6,11 @@ import type React from 'react'; import { useCallback, useMemo, useState } from 'react'; +import { useAnnounce } from '../../contexts/LiveRegionContext'; import { useProForgeViewContext } from '../../contexts/ProForgeViewContext'; import { proForgeActions } from '../../features/proForge/proForgeSlice'; import type { ReviewItem, ReviewItemStatus } from '../../features/proForge/types'; +import { useTranslation } from '../../hooks/useTranslation'; const SEVERITY_ICONS: Record = { critical: '🔴', @@ -16,42 +18,30 @@ const SEVERITY_ICONS: Record = { info: '🔵', }; -const TYPE_LABELS: Record = { - structuralEdit: 'Structural', - proseEdit: 'Prose', - grammarEdit: 'Grammar', - styleEdit: 'Style', - repetitionHit: 'Repetition', - consistencyIssue: 'Consistency', - plotHole: 'Plot Hole', - legalWarning: 'Legal', - technicalIssue: 'Technical', - pacingIssue: 'Pacing', - arcIssue: 'Arc', -}; - const SEVERITY_GROUPS = [ { key: 'critical' as const, - label: 'Critical Actions', + labelKey: 'proforge.review.group.critical', headerClass: 'text-[var(--sc-error)] border-[var(--sc-error-muted)]', badgeClass: 'bg-[var(--sc-error-muted)] text-[var(--sc-error)]', }, { key: 'warning' as const, - label: 'Warnings', + labelKey: 'proforge.review.group.warning', headerClass: 'text-[var(--sc-warning,#d97706)] border-[var(--sc-warning-muted,#fef3c7)]', badgeClass: 'bg-[var(--sc-warning-muted,#fef3c7)] text-[var(--sc-warning,#d97706)]', }, { key: 'info' as const, - label: 'Suggestions', + labelKey: 'proforge.review.group.info', headerClass: 'text-[var(--sc-accent)] border-[var(--sc-accent-muted,#eff6ff)]', badgeClass: 'bg-[var(--sc-accent-muted,#eff6ff)] text-[var(--sc-accent)]', }, ] as const; export const PipelineReviewPanel: React.FC = () => { + const { t } = useTranslation(); + const announce = useAnnounce(); const { currentRun, activeStageResult, @@ -65,6 +55,8 @@ export const PipelineReviewPanel: React.FC = () => { const [filter, setFilter] = useState<'all' | 'pending' | 'accepted' | 'rejected'>('all'); const stage = activeStageResult?.stage; + // QNBS-v3: Localised stage label; falls back to the raw stage id if no key exists. + const stageLabel = stage ? t(`proforge.stageName.${stage}`) : ''; const pendingCount = currentStageReviewItems.filter((i) => i.status === 'pending').length; const acceptedCount = currentStageReviewItems.filter((i) => i.status === 'accepted').length; @@ -138,8 +130,10 @@ export const PipelineReviewPanel: React.FC = () => { const items = activeStageResult?.reviewItems ?? []; const decisions = items.map((item) => ({ itemId: item.id, status: item.status })); void submitReview(stage, decisions); + // QNBS-v3: WCAG live-region announce so screen-reader users hear the stage was submitted. + announce(t('proforge.review.announceSubmitted', { stage: stageLabel }), 'polite'); setActiveView('dashboard'); - }, [stage, activeStageResult, submitReview, setActiveView]); + }, [stage, stageLabel, activeStageResult, submitReview, setActiveView, announce, t]); const handleSkip = useCallback(() => { if (!stage) return; @@ -150,7 +144,7 @@ export const PipelineReviewPanel: React.FC = () => { if (!currentRun || !stage) { return (
-

No review items available.

+

{t('proforge.review.noItems')}

); } @@ -160,9 +154,15 @@ export const PipelineReviewPanel: React.FC = () => { {/* Header */}
-

Review: {stage}

+

+ {t('proforge.review.heading', { stage: stageLabel })} +

- {pendingCount} pending · {acceptedCount} accepted · {rejectedCount} rejected + {t('proforge.review.counts', { + pending: pendingCount, + accepted: acceptedCount, + rejected: rejectedCount, + })}

@@ -171,9 +171,9 @@ export const PipelineReviewPanel: React.FC = () => { type="button" onClick={handleQuickAcceptHighConfidence} className="px-2.5 py-1 text-xs rounded-sc-md bg-[var(--sc-accent-muted,#eff6ff)] text-[var(--sc-accent)] hover:opacity-80 transition-opacity" - title="Accept all high-confidence non-critical suggestions" + title={t('proforge.review.quickAcceptTitle')} > - Quick Accept ({quickAcceptCount}) + {t('proforge.review.quickAccept', { count: quickAcceptCount })} )}
@@ -198,15 +198,20 @@ export const PipelineReviewPanel: React.FC = () => {
- 🔴 {criticalPending.length} Critical{' '} - {criticalPending.length === 1 ? 'Issue' : 'Issues'} Need Attention + + {t( + criticalPending.length === 1 + ? 'proforge.review.criticalHeadingOne' + : 'proforge.review.criticalHeadingOther', + { count: criticalPending.length }, + )}
{topCritical.map((item) => ( @@ -216,7 +221,7 @@ export const PipelineReviewPanel: React.FC = () => { ))} {criticalPending.length > 3 && (
- +{criticalPending.length - 3} more critical issues below + {t('proforge.review.moreCritical', { count: criticalPending.length - 3 })}
)}
@@ -224,24 +229,27 @@ export const PipelineReviewPanel: React.FC = () => { {/* Filter Tabs — secondary position */}
- {(['all', 'pending', 'accepted', 'rejected'] as const).map((f) => ( - - ))} + : currentStageReviewItems.filter((i) => i.status === f).length; + return ( + + ); + })}
{/* Review Items */} @@ -254,7 +262,7 @@ export const PipelineReviewPanel: React.FC = () => { return (
- {group.label} + {t(group.labelKey)} {items.length} @@ -269,7 +277,7 @@ export const PipelineReviewPanel: React.FC = () => { }) ) : filteredItems.length === 0 ? (

- No items match this filter. + {t('proforge.review.noItemsForFilter')}

) : ( filteredItems.map((item) => ( @@ -285,7 +293,7 @@ export const PipelineReviewPanel: React.FC = () => { onClick={handleSkip} className="px-3 py-1.5 text-xs rounded-sc-md text-[var(--sc-text-secondary)] hover:text-[var(--sc-text-primary)] transition-colors" > - Skip Stage + {t('proforge.review.skipStage')}
@@ -310,12 +320,13 @@ function ReviewItemCard({ item: ReviewItem; onStatusChange: (id: string, status: ReviewItemStatus) => void; }) { + const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); - const statusButtons: { status: ReviewItemStatus; label: string; color: string }[] = [ - { status: 'accepted', label: 'Accept', color: 'var(--sc-success)' }, - { status: 'rejected', label: 'Reject', color: 'var(--sc-error)' }, - { status: 'ignored', label: 'Ignore', color: 'var(--sc-text-tertiary)' }, + const statusButtons: { status: ReviewItemStatus; labelKey: string; color: string }[] = [ + { status: 'accepted', labelKey: 'proforge.review.accept', color: 'var(--sc-success)' }, + { status: 'rejected', labelKey: 'proforge.review.reject', color: 'var(--sc-error)' }, + { status: 'ignored', labelKey: 'proforge.review.ignore', color: 'var(--sc-text-tertiary)' }, ]; return ( @@ -329,11 +340,15 @@ function ReviewItemCard({ }`} >
- {SEVERITY_ICONS[item.severity]} + {/* QNBS-v3: Emoji is decorative; severity is conveyed to AT via the sr-only text below. */} + + {t(`proforge.review.severity.${item.severity}`)}
- {TYPE_LABELS[item.type] ?? item.type} + {t(`proforge.review.type.${item.type}`)} {item.sectionTitle && ( @@ -341,7 +356,7 @@ function ReviewItemCard({ )} - {Math.round(item.confidence * 100)}% confidence + {t('proforge.review.confidence', { percent: Math.round(item.confidence * 100) })}

{item.description}

@@ -349,10 +364,10 @@ function ReviewItemCard({ {item.original && item.proposed && expanded && (
- Original: {item.original} + {t('proforge.review.original')} {item.original}
- Proposed: {item.proposed} + {t('proforge.review.proposed')} {item.proposed}
)} @@ -361,9 +376,10 @@ function ReviewItemCard({ )} @@ -374,6 +390,7 @@ function ReviewItemCard({ type="button" key={btn.status} onClick={() => onStatusChange(item.id, btn.status)} + aria-pressed={item.status === btn.status} className={`px-2 py-0.5 text-xs rounded-sc-sm border transition-all ${ item.status === btn.status ? 'text-white' @@ -385,7 +402,7 @@ function ReviewItemCard({ : {} } > - {btn.label} + {t(btn.labelKey)} ))}
diff --git a/docs/PROFORGE-PIPELINE.md b/docs/PROFORGE-PIPELINE.md index 224c0f683..66e1fd164 100644 --- a/docs/PROFORGE-PIPELINE.md +++ b/docs/PROFORGE-PIPELINE.md @@ -88,7 +88,9 @@ features/proForge/ services/proForge/ proForgeOrchestrator.ts # Central orchestrator + executeStageWithSupervision loop - proForgeMemoryBank.ts # IndexedDB-backed project memory + proForgeMemoryBank.ts # IndexedDB-backed project memory (keyword / semantic / hybrid recall) + proForgeHistoryStore.ts # IndexedDB run-history persistence (survives reload) + applyReviewEdits.ts # Applies accepted ReviewItems back into manuscript text pipelineAgents/ baseAgent.ts # Abstract base class (~200 LOC saved across 8 agents) supervisorAgent.ts # Heuristic quality gate — no AI calls @@ -100,8 +102,6 @@ services/proForge/ productionAgent.ts # Stage 6 publishingAgent.ts # Stage 7 analyticsAgent.ts # Stage 8 - pipelineTools/ - toolRegistry.ts # Tool-calling registry + built-in tools pipelineOutput/ structuredOutput.ts # Zod schemas for all AI outputs @@ -159,17 +159,24 @@ export class SupervisorAgent { `SupervisionDecision` shape: ```typescript interface SupervisionDecision { - verdict: 'pass' | 'retry' | 'fail'; - reason: string; - retryHint?: string; + pass: boolean; + retryRecommended: boolean; + qualityScore: number; // 0–100 + reasons: string[]; } ``` -**Decision rules per stage:** -- `intake`: Detects uniform 50/100 fallback sentinel (all scores = exactly 50) → retry. `qualityScore < 30` → fail (hard gate). -- `structural`: If edit count > word count / 10 → retry (over-aggressive edits). -- `proof`: If grammar issue count > word count / 20 → retry (implausible density). -- All stages: `isFallback: true` on output → retry (up to `maxRetries` times). +**Decision rules per stage** (all 8 stages are gated): +- `intake`: `isFallback: true` → fail (`qualityScore` 0). `qualityScore < 30` → hard-fail the pipeline. +- `structural`: `isFallback: true` → retry. Zero edits + zero review items on a >1000-word manuscript → retry. +- `lineProse`: Zero prose edits + zero review items on a >1000-word manuscript → retry. +- `copyEdit`: Zero grammar/style/repetition/format findings on a >1500-word manuscript → retry. +- `proof`: `isFallback: true` → retry. `overallPass` with zero grammar issues on a >500-word manuscript → retry. +- `production`: Zero export artifacts → retry. +- `publishing`: Missing book-title metadata or back-cover blurb → retry. +- `analytics`: Never blocks (terminal, informational); flags a missing metrics block in `reasons`. + +A non-passing decision below the retry budget is attached to the `StageResult` as `supervisorDecision` so the reviewer sees why the stage was flagged. Only `intake` with `qualityScore < 30` hard-fails. --- @@ -180,17 +187,23 @@ interface SupervisionDecision { ``` for each selected stage: attempt = 0 + retryFeedback = '' loop: + agent = new AgentClass(context) + if retryFeedback: agent.setRetryFeedback(retryFeedback) # corrective guidance result = await agent.execute(signal) decision = supervisorAgent.evaluate(stage, result) - if decision.verdict === 'pass' OR attempt >= maxRetries: - dispatch stageCompleted / stageAwaitingReview + if decision.pass OR attempt >= maxRetries: + dispatch stageCompleted (with supervisorDecision if !pass) break + retryFeedback = decision.reasons + reflectionNotes # fed into next prompt attempt++ - (retry with updated context) ``` `maxRetries` (default `1`) is set in `PipelineConfig`. Set to `0` to disable supervision retries. +On retry, the supervisor's `reasons` (plus any `reflectionNotes`) are injected as a corrective +preamble into the agent's next prompt via `BaseAgent.setRetryFeedback`, so a retry is materially +different rather than an identical re-roll. --- @@ -248,29 +261,27 @@ Every edit, issue, or suggestion produced by an agent becomes a `ReviewItem`: - Per-item rationale and confidence score - Quick Accept for high-confidence non-critical items (≥ 0.85) +### Applying Accepted Edits +When a stage's review is submitted, accepted `ReviewItem`s for **editing stages** +(`intake`, `structural`, `lineProse`, `copyEdit`, `proof`) are written back into the manuscript +before the post-stage snapshot is taken (`services/proForge/applyReviewEdits.ts`): +- Edits are applied **back-to-front by offset** so earlier edits never invalidate later offsets. +- If an offset range no longer matches the recorded `original`, the applier falls back to locating + the `original` text; if it can't be found, the edit is **skipped** (counted, never force-applied). +- Because the `project` slice is `redux-undo` wrapped, applied edits are natively undoable. +- Non-editing stages (`production`, `publishing`, `analytics`) record decisions only. + ### Snapshots - **Pre-pipeline snapshot:** Auto-created at start - **Pre-stage snapshot:** Created before each stage -- **Post-stage snapshot:** Created after review acceptance +- **Post-stage snapshot:** Created after accepted edits are applied (captures the edited text) - Rollback restores any pre-stage snapshot ---- - -## Tool Calling - -Agents use the `toolRegistry` (`services/proForge/pipelineTools/toolRegistry.ts`) to interact with the manuscript: - -| Tool | Purpose | Stages | -|------|---------|--------| -| `readSection` | Read full section content | All | -| `readAllSections` | List all sections (metadata) | All | -| `readProjectMeta` | Read title, logline, characters, worlds | All | -| `searchLore` | Search memory bank for lore | All | -| `analyzePacing` | Compute tension scores per section | Intake, Structural | -| `countWords` | Word count statistics | All | -| `generateReport` | Save structured report to memory bank | All | -| `getMemoryContext` | Retrieve relevant memory context | All | -| `proposeEdit` | Queue an edit for Human-in-the-Loop review | Structural, Prose, CopyEdit | +### Run History Persistence +Completed and aborted runs are persisted per project to IndexedDB +(`proforge-run-history`, capped at 20 most-recent) by `proForgeHistoryStore.ts`, and rehydrated on +load by `useProForgeOrchestrator`. The `proForge` Redux slice itself stays ephemeral; this lets the +`AnalyticsAgent`'s cross-run comparisons survive a reload. --- @@ -280,8 +291,10 @@ The `ProForgeMemoryBank` (`services/proForge/proForgeMemoryBank.ts`) provides pe - **Storage:** IndexedDB (`proforge-memory-bank`) - **Categories:** `lore` | `character` | `style` | `feedback` | `edit` | `meta` -- **Retrieval:** Keyword search + stage-aware filtering -- **Context Building:** Automatically assembles memory blocks for prompt injection +- **Stage-context recall:** `buildContextString` returns prior-stage + lore/character entries (chronological). +- **Targeted search:** `search(query, limit, mode)` honours the run's `ragMode` — `lexical` (keyword), + or `semantic` / `hybrid` via the local MiniLM embedding service (`embedText` + cosine similarity), + with automatic keyword fallback if embeddings are unavailable (offline / model not loaded). --- @@ -319,11 +332,12 @@ interface QualityGateReport { isFallback?: boolean; ... } // (on DiagnosticReport and StructuralEditPlan) reflectionNotes?: string; -// Supervisor decision — attached to StageResult after supervision +// Supervisor decision — attached to StageResult when a stage is flagged interface SupervisionDecision { - verdict: 'pass' | 'retry' | 'fail'; - reason: string; - retryHint?: string; + pass: boolean; + retryRecommended: boolean; + qualityScore: number; // 0–100 + reasons: string[]; } // On StageResult: supervisorDecision?: SupervisionDecision; diff --git a/features/proForge/types.ts b/features/proForge/types.ts index 96d26117f..6d02187ee 100644 --- a/features/proForge/types.ts +++ b/features/proForge/types.ts @@ -95,8 +95,6 @@ export interface PipelineConfig { language: string; /** Max supervisor-triggered retries per stage (0 = no retry, 1 = one retry) */ maxRetries?: 0 | 1; - /** Active LoRA adapter ID — enables the optional StyleAdaptedWriterAgent stage. */ - loraAdapterId?: string; } export const DEFAULT_PIPELINE_CONFIG: PipelineConfig = { diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index e737b34fd..4b385dde4 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,12 +1,12 @@ -# Graph Report - StoryCraft-Studio (2026-06-05) +# Graph Report - StoryCraft-Studio (2026-06-10) ## Corpus Check -- 948 files · ~585,302 words +- 998 files · ~1,160,450 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 2945 nodes · 3896 edges · 58 communities detected -- Extraction: 68% EXTRACTED · 32% INFERRED · 0% AMBIGUOUS · INFERRED: 1228 edges (avg confidence: 0.8) +- 4475 nodes · 8039 edges · 54 communities detected +- Extraction: 77% EXTRACTED · 23% INFERRED · 0% AMBIGUOUS · INFERRED: 1870 edges (avg confidence: 0.8) - Token cost: 0 input · 0 output ## Community Hubs (Navigation) @@ -37,186 +37,182 @@ - [[_COMMUNITY_Community 24|Community 24]] - [[_COMMUNITY_Community 25|Community 25]] - [[_COMMUNITY_Community 26|Community 26]] -- [[_COMMUNITY_Community 29|Community 29]] -- [[_COMMUNITY_Community 30|Community 30]] +- [[_COMMUNITY_Community 27|Community 27]] +- [[_COMMUNITY_Community 28|Community 28]] - [[_COMMUNITY_Community 31|Community 31]] - [[_COMMUNITY_Community 32|Community 32]] - [[_COMMUNITY_Community 34|Community 34]] - [[_COMMUNITY_Community 36|Community 36]] - [[_COMMUNITY_Community 37|Community 37]] -- [[_COMMUNITY_Community 38|Community 38]] +- [[_COMMUNITY_Community 41|Community 41]] - [[_COMMUNITY_Community 42|Community 42]] -- [[_COMMUNITY_Community 43|Community 43]] +- [[_COMMUNITY_Community 44|Community 44]] - [[_COMMUNITY_Community 45|Community 45]] - [[_COMMUNITY_Community 46|Community 46]] -- [[_COMMUNITY_Community 47|Community 47]] -- [[_COMMUNITY_Community 50|Community 50]] -- [[_COMMUNITY_Community 58|Community 58]] -- [[_COMMUNITY_Community 59|Community 59]] -- [[_COMMUNITY_Community 66|Community 66]] -- [[_COMMUNITY_Community 71|Community 71]] -- [[_COMMUNITY_Community 72|Community 72]] -- [[_COMMUNITY_Community 80|Community 80]] -- [[_COMMUNITY_Community 85|Community 85]] -- [[_COMMUNITY_Community 88|Community 88]] -- [[_COMMUNITY_Community 90|Community 90]] -- [[_COMMUNITY_Community 101|Community 101]] -- [[_COMMUNITY_Community 133|Community 133]] -- [[_COMMUNITY_Community 146|Community 146]] -- [[_COMMUNITY_Community 151|Community 151]] -- [[_COMMUNITY_Community 182|Community 182]] -- [[_COMMUNITY_Community 224|Community 224]] -- [[_COMMUNITY_Community 257|Community 257]] -- [[_COMMUNITY_Community 262|Community 262]] +- [[_COMMUNITY_Community 48|Community 48]] +- [[_COMMUNITY_Community 55|Community 55]] +- [[_COMMUNITY_Community 57|Community 57]] +- [[_COMMUNITY_Community 61|Community 61]] +- [[_COMMUNITY_Community 68|Community 68]] +- [[_COMMUNITY_Community 73|Community 73]] +- [[_COMMUNITY_Community 75|Community 75]] +- [[_COMMUNITY_Community 84|Community 84]] +- [[_COMMUNITY_Community 115|Community 115]] +- [[_COMMUNITY_Community 129|Community 129]] +- [[_COMMUNITY_Community 134|Community 134]] +- [[_COMMUNITY_Community 164|Community 164]] +- [[_COMMUNITY_Community 204|Community 204]] +- [[_COMMUNITY_Community 237|Community 237]] +- [[_COMMUNITY_Community 242|Community 242]] ## God Nodes (most connected - your core abstractions) -1. `fn()` - 47 edges -2. `StorageManager` - 36 edges -3. `CloudSyncBackend` - 36 edges -4. `retryFs()` - 31 edges -5. `t()` - 29 edges -6. `useTranslation()` - 28 edges -7. `VoiceCommandService` - 28 edges -8. `useAppDispatch()` - 25 edges -9. `CollaborationService` - 24 edges -10. `collectLibraryBackupPayload()` - 22 edges +1. `mt()` - 103 edges +2. `Bv` - 74 edges +3. `fn()` - 47 edges +4. `Ze()` - 43 edges +5. `wx()` - 41 edges +6. `xA` - 40 edges +7. `t()` - 40 edges +8. `CloudSyncBackend` - 39 edges +9. `StorageManager` - 36 edges +10. `tA()` - 34 edges ## Surprising Connections (you probably didn't know these) -- `App()` --calls--> `useApp()` [INFERRED] - App.tsx → hooks/useApp.ts +- `checkForUpdate()` --calls--> `update()` [INFERRED] + register-sw.ts → e2e-deep-report/trace/sw.bundle.js - `handleWebllmDownload()` --calls--> `generateLocalText()` [INFERRED] components/settings/AiSections.tsx → services/localAiFacade.ts - `ViewLoader()` --calls--> `useTranslation()` [INFERRED] App.tsx → hooks/useTranslation.ts +- `App()` --calls--> `useApp()` [INFERRED] + App.tsx → hooks/useApp.ts - `App()` --calls--> `useGlobalKeyboardShortcuts()` [INFERRED] App.tsx → hooks/useGlobalKeyboardShortcuts.ts -- `App()` --calls--> `usePushToTalk()` [INFERRED] - App.tsx → hooks/usePushToTalk.ts ## Communities ### Community 0 - "Community 0" -Cohesion: 0.02 -Nodes (54): AiInferenceCacheService, hashKey(), assertCloudAiAllowed(), assertCloudAiAllowedSync(), assertLoraLocalOnly(), binderDepth(), CloudSyncBackend, CloudSyncClient (+46 more) +Cohesion: 0.01 +Nodes (301): _0, _2(), A0, a2(), aA(), ac(), ad(), Ah() (+293 more) ### Community 1 - "Community 1" -Cohesion: 0.03 -Nodes (48): decryptCloudPayload(), deriveCloudSyncKey(), encryptCloudPayload(), loadStoryCodex(), IdbAssetStore, IdbCodexStore, compressData(), IdbConnectionManager (+40 more) +Cohesion: 0.01 +Nodes (171): af(), ef(), ff(), Ja(), lf(), mt(), nf(), of() (+163 more) ### Community 2 - "Community 2" -Cohesion: 0.03 -Nodes (45): createCancellationToken(), createAttentionPipeline(), createComputePipeline(), createKvCachePipeline(), createMlpPipeline(), createSimilarityBuffers(), createSimilarityPipeline(), encodeSimilarityUniforms() (+37 more) +Cohesion: 0.01 +Nodes (117): recordLatency(), AiInferenceCacheService, hashKey(), assertCloudAiAllowed(), assertCloudAiAllowedSync(), assertLoraLocalOnly(), _cleanupPendingRequest(), _clearPendingRequestsForTest() (+109 more) ### Community 3 - "Community 3" -Cohesion: 0.02 -Nodes (54): makeContext(), makeContext(), renderSheet(), makeDeps(), renderPanel(), makeStoreState(), createFakeAdapter(), createFakeDevice() (+46 more) +Cohesion: 0.01 +Nodes (82): handleCopyForNotion(), handleDocxImport(), handleExport(), handlePasteImport(), handleBuildLocalRag(), handleWebllmDownload(), isCustomOllamaModel(), handleAddFolder() (+74 more) ### Community 4 - "Community 4" Cohesion: 0.02 -Nodes (49): AnalyticsBootstrap(), App(), ViewLoader(), Header(), useAppDispatch(), useAppSelectorShallow(), IdbUnlockModal(), useAnnounce() (+41 more) +Nodes (63): pipeline(), pipeline(), getFocusable(), onKeyDown(), onPointerUp(), handleKeyDown(), applyPreset(), async() (+55 more) ### Community 5 - "Community 5" Cohesion: 0.02 -Nodes (27): buildKeyModuleMap(), loadBundleKeys(), loadModuleData(), getLocalUser(), getRandomColor(), sanitizeRoomInput(), stripControlChars(), loadFeatureFlagsState() (+19 more) +Nodes (35): hasMigrationMarker(), legacyDatabaseListed(), migrateLegacyStorycraftDbIfNeeded(), openLegacyDatabase(), promisifyRequest(), readAllFromStore(), setMigrationMarker(), stateDbHasProjectOrSettings() (+27 more) ### Community 6 - "Community 6" -Cohesion: 0.04 -Nodes (40): generateJson(), attachCause(), cleanPrompt(), sanitizePromptBlock(), stripControlChars(), stripJsonFences(), AnalyticsAgent, buildAiOpts() (+32 more) +Cohesion: 0.02 +Nodes (73): CloudSyncBackend, decryptCloudPayload(), deriveCloudSyncKey(), encryptCloudPayload(), decryptDuckDbData(), encryptDuckDbData(), initDuckDbEncryption(), translate() (+65 more) ### Community 7 - "Community 7" -Cohesion: 0.03 -Nodes (48): handleCopyForNotion(), handleDocxImport(), handleExport(), handlePasteImport(), handleRemoveKey(), handleSaveKey(), handleTestConnection(), handleAddFolder() (+40 more) +Cohesion: 0.02 +Nodes (53): assertNoSeriousViolations(), AudioNavigator, navigateToCollaborationSettings(), ab(), B_(), br(), bs(), Bv (+45 more) ### Community 8 - "Community 8" -Cohesion: 0.04 -Nodes (16): EcoModeService, FeedbackService, handleEcoToggle(), LoraEnvReport, LoraTrainPayload, train_lora(), ConsentRequiredError, createSttEngine() (+8 more) +Cohesion: 0.02 +Nodes (50): item(), glossaryTranslate(), loadCheckpoint(), loadGlossary(), main(), parseArgs(), saveCheckpoint(), sleep() (+42 more) ### Community 9 - "Community 9" -Cohesion: 0.03 -Nodes (29): CircuitBreaker, translate(), minimalProject(), getNotifications(), handleEvaluate(), ScoreGauge(), comparePromptOutputs(), computeStyleConsistencyScore() (+21 more) +Cohesion: 0.02 +Nodes (63): clampRetryAfter(), computeRetryDelayMs(), delay(), parseRetryAfterMs(), retryAfterStringToMs(), withTransientRetry(), makeContext(), makeContext() (+55 more) ### Community 10 - "Community 10" -Cohesion: 0.04 -Nodes (47): handleBuildLocalRag(), handleWebllmDownload(), isCustomOllamaModel(), duckdbCodexWrite(), duckdbCrossProjectWrite(), duckdbDualWrite(), duckdbRagWrite(), esc() (+39 more) +Cohesion: 0.02 +Nodes (53): AnalyticsBootstrap(), App(), ViewLoader(), Header(), useAppDispatch(), useAppSelectorShallow(), IdbUnlockModal(), useAnnounce() (+45 more) ### Community 11 - "Community 11" Cohesion: 0.04 -Nodes (34): deleteIdb(), formatStorageError(), initializeStorage(), resetAllDatabases(), routeTask(), addDebouncedListener(), initAdaptiveAiOnStartup(), initWorkerBusOnStartup() (+26 more) +Nodes (54): generateJson(), attachCause(), cleanPrompt(), sanitizePromptBlock(), stripControlChars(), stripJsonFences(), AnalyticsAgent, handleRemoveKey() (+46 more) ### Community 12 - "Community 12" -Cohesion: 0.04 -Nodes (35): generateMessageId(), getWorker(), send(), getDuckDb(), initDuckDb(), isOPFSSupported(), buildTimeoutSignal(), createStoryCraftFetch() (+27 more) +Cohesion: 0.03 +Nodes (47): AdaptiveAiEngine, _clearLatencyHistory(), estimateLatency(), getTaskConfig(), selectModelForBackend(), start(), getLastBenchmarkResults(), loadResults() (+39 more) ### Community 13 - "Community 13" -Cohesion: 0.04 -Nodes (21): pipeline(), pipeline(), start(), decrypt(), decryptJson(), encrypt(), encryptJson(), GpuResourceManager (+13 more) +Cohesion: 0.03 +Nodes (35): CollabEncryptionRequiredError, CollaborationService, resolveWebRtcSignalingUrls(), MockDoc, MockWebrtcProvider, createAttentionPipeline(), createComputePipeline(), createKvCachePipeline() (+27 more) ### Community 14 - "Community 14" -Cohesion: 0.09 -Nodes (16): FsAssetStore, FsCodexStore, countProjectWords(), decompressData(), decryptText(), deriveFileSystemCryptoKey(), encryptText(), FsCore (+8 more) +Cohesion: 0.03 +Nodes (45): check(), green(), grep(), hasRuntimeConsumption(), read(), red(), collect(), id (+37 more) ### Community 15 - "Community 15" Cohesion: 0.05 -Nodes (38): applyPreset(), async(), close(), countWords(), enrichProjectIndex(), extractCharacterNames(), getDb(), indexProject() (+30 more) +Nodes (42): NT, getDuckDb(), handleExec(), handleQuery(), handleShutdown(), initDuckDb(), isOPFSSupported(), duckdbCodexWrite() (+34 more) ### Community 16 - "Community 16" -Cohesion: 0.06 -Nodes (28): _clearLatencyHistory(), _clearPendingRequestsForTest(), clearCommunityTemplateCache(), clearServiceWorkerCaches(), deleteAllIndexedDBDatabases(), wipeAllAppData(), clearEmbeddingCache(), detectOnnxExecutionProviders() (+20 more) +Cohesion: 0.08 +Nodes (20): FsAssetStore, FsCodexStore, deleteIdb(), formatStorageError(), initializeStorage(), resetAllDatabases(), countProjectWords(), decompressData() (+12 more) ### Community 17 - "Community 17" -Cohesion: 0.07 -Nodes (37): _cleanupPendingRequest(), _deduplicateRequest(), generateText(), generateTextSingleProvider(), _pendingKey(), streamAiHelpResponse(), streamAnthropic(), streamGrok() (+29 more) +Cohesion: 0.04 +Nodes (27): DeadLetterQueue, openDlqDb(), storeClear(), storeGetAll(), k2, download_artifact(), get_failed_logs(), get_latest_failed_run() (+19 more) ### Community 18 - "Community 18" -Cohesion: 0.07 -Nodes (28): AdaptiveAiEngine, estimateLatency(), recordLatency(), selectModelForBackend(), getLastBenchmarkResults(), loadResults(), runAllBenchmarks(), runInferenceBenchmark() (+20 more) +Cohesion: 0.04 +Nodes (38): countWords(), enrichProjectIndex(), extractCharacterNames(), getDb(), indexProject(), listIndexedProjects(), removeProjectIndex(), semanticSearchProjects() (+30 more) ### Community 19 - "Community 19" -Cohesion: 0.07 -Nodes (25): check(), green(), grep(), hasRuntimeConsumption(), read(), red(), formatArgs(), formatLogsForReport() (+17 more) +Cohesion: 0.06 +Nodes (26): mockT(), aE, iE, lE(), rE, sE, analyzeSentiment(), classifyWritingTopic() (+18 more) ### Community 20 - "Community 20" -Cohesion: 0.11 -Nodes (1): StorageManager +Cohesion: 0.09 +Nodes (38): analyze_stryker_failure(), analyze_vitest_failure(), analyze_with_llm(), format_for_vscode(), get_openrouter_client(), main(), Send preprocessed errors to LLM for analysis., Format errors for VS Code problem matcher. (+30 more) ### Community 21 - "Community 21" -Cohesion: 0.09 -Nodes (5): CollabEncryptionRequiredError, CollaborationService, resolveWebRtcSignalingUrls(), MockDoc, MockWebrtcProvider +Cohesion: 0.08 +Nodes (27): generateTextSingleProvider(), _pendingKey(), streamAiHelpResponse(), streamAnthropic(), streamGrok(), streamOpenAI(), streamProvider(), testAIConnection() (+19 more) ### Community 22 - "Community 22" -Cohesion: 0.14 -Nodes (21): handleToggle(), handleDelete(), handleFileChange(), activateAdapter(), clearDatasetEntries(), deactivateAdapter(), deleteAdapter(), exportAdapter() (+13 more) +Cohesion: 0.1 +Nodes (1): StorageManager ### Community 23 - "Community 23" -Cohesion: 0.16 -Nodes (14): buildExcerpt(), extractCharacters(), extractManuscriptSections(), searchAcrossProjectIndex(), searchAcrossProjects(), normalizeSearch(), scoreAgainstQuery(), subsequenceScore() (+6 more) +Cohesion: 0.21 +Nodes (4): LS, Th(), xn(), aa ### Community 24 - "Community 24" -Cohesion: 0.14 -Nodes (12): navigateToCollaborationSettings(), clickNavItem(), ensureBlankProject(), flushWriterDebounce(), seedGeminiApiKey(), selectFirstEnabledWriterSection(), waitForMainChrome(), waitForSpaReady() (+4 more) +Cohesion: 0.16 +Nodes (11): cE(), fr(), Go(), jS(), ri(), v_(), wb(), Xd (+3 more) ### Community 25 - "Community 25" -Cohesion: 0.18 -Nodes (14): analyze_text(), count_sentences(), count_syllables(), counts_words_chars_and_spaces(), empty_text_is_all_zero(), flesch_score_is_finite_for_real_prose(), run_text_analyze(), RustTaskRequest (+6 more) +Cohesion: 0.14 +Nodes (21): handleToggle(), handleDelete(), handleFileChange(), activateAdapter(), clearDatasetEntries(), deactivateAdapter(), deleteAdapter(), exportAdapter() (+13 more) ### Community 26 - "Community 26" +Cohesion: 0.14 +Nodes (14): buildExcerpt(), extractCharacters(), extractManuscriptSections(), searchAcrossProjectIndex(), searchAcrossProjects(), normalizeSearch(), scoreAgainstQuery(), subsequenceScore() (+6 more) + +### Community 27 - "Community 27" +Cohesion: 0.35 +Nodes (2): cc, Gb() + +### Community 28 - "Community 28" Cohesion: 0.19 Nodes (6): buildNormManuscriptExport(), paginateNormLines(), stripLightMarkdown(), wrapParagraphToLines(), wrapPlainTextToNormLines(), UsageAnalyticsService -### Community 29 - "Community 29" +### Community 31 - "Community 31" Cohesion: 0.22 Nodes (4): accessibilityPresetDefaults(), normalizeAccessibilitySettings(), applyPreset(), baseSettings() -### Community 30 - "Community 30" -Cohesion: 0.29 -Nodes (1): PriorityTaskQueue - -### Community 31 - "Community 31" -Cohesion: 0.33 -Nodes (1): AudioNavigator - ### Community 32 - "Community 32" -Cohesion: 0.46 +Cohesion: 0.42 Nodes (6): emit(), main(), merge(), ProgressCallback, Emits JSON progress events on each training log step., train() ### Community 34 - "Community 34" @@ -228,171 +224,145 @@ Cohesion: 0.53 Nodes (4): buildWebNNExecutionProviders(), detectWebNN(), isDirectMLAvailable(), isDirectMLHeuristic() ### Community 37 - "Community 37" -Cohesion: 0.4 -Nodes (2): getFocusable(), onKeyDown() - -### Community 38 - "Community 38" Cohesion: 0.7 Nodes (4): check_cuda_and_vram(), check_package(), check_python_version(), main() -### Community 42 - "Community 42" +### Community 41 - "Community 41" Cohesion: 0.4 Nodes (2): useDashboardContext(), DashboardHeader() -### Community 43 - "Community 43" +### Community 42 - "Community 42" Cohesion: 0.4 Nodes (4): Room, SignalingConn, WebrtcConn, WebrtcProvider -### Community 45 - "Community 45" +### Community 44 - "Community 44" Cohesion: 0.6 Nodes (4): applyFormula(), computeReadabilitySnapshot(), estimateSyllables(), getSyllablePattern() -### Community 46 - "Community 46" +### Community 45 - "Community 45" Cohesion: 0.67 Nodes (2): sanitizeSpeechTranscript(), stripControlChars() -### Community 47 - "Community 47" +### Community 46 - "Community 46" Cohesion: 0.5 Nodes (1): SpeechSynthesisUtteranceMock -### Community 50 - "Community 50" +### Community 48 - "Community 48" Cohesion: 0.67 Nodes (2): makeConfig(), startPipelinePayload() -### Community 58 - "Community 58" +### Community 55 - "Community 55" Cohesion: 0.67 Nodes (2): defaultProject(), setProjectData() -### Community 59 - "Community 59" +### Community 57 - "Community 57" Cohesion: 0.5 Nodes (3): AsyncDuckDB, AsyncDuckDBConnection, ConsoleLogger -### Community 66 - "Community 66" -Cohesion: 0.67 -Nodes (2): getFocusable(), handleTabKey() - -### Community 71 - "Community 71" +### Community 61 - "Community 61" Cohesion: 0.67 Nodes (2): getQuestionsForArchetype(), getTemplateForArchetype() -### Community 72 - "Community 72" -Cohesion: 0.67 -Nodes (2): buildLcsTable(), diffTokensToOps() - -### Community 80 - "Community 80" +### Community 68 - "Community 68" Cohesion: 0.67 Nodes (1): makeSection() -### Community 85 - "Community 85" +### Community 73 - "Community 73" Cohesion: 0.67 Nodes (1): MockGoogleGenAI -### Community 88 - "Community 88" +### Community 75 - "Community 75" Cohesion: 0.67 Nodes (1): makeDeps() -### Community 90 - "Community 90" -Cohesion: 1.0 -Nodes (2): fireSwipe(), makePointerEvent() - -### Community 101 - "Community 101" +### Community 84 - "Community 84" Cohesion: 0.67 Nodes (1): TaskError -### Community 133 - "Community 133" +### Community 115 - "Community 115" Cohesion: 1.0 Nodes (1): MockIntersectionObserver -### Community 146 - "Community 146" +### Community 129 - "Community 129" Cohesion: 1.0 Nodes (1): MockWorker -### Community 151 - "Community 151" +### Community 134 - "Community 134" Cohesion: 1.0 Nodes (1): MockBroadcastChannel -### Community 182 - "Community 182" +### Community 164 - "Community 164" Cohesion: 1.0 Nodes (1): MockIntersectionObserver -### Community 224 - "Community 224" +### Community 204 - "Community 204" Cohesion: 1.0 Nodes (1): MockWorker -### Community 257 - "Community 257" +### Community 237 - "Community 237" Cohesion: 1.0 Nodes (1): FileSystemService -### Community 262 - "Community 262" +### Community 242 - "Community 242" Cohesion: 1.0 Nodes (1): IndexedDBService ## Knowledge Gaps -- **26 isolated node(s):** `Emits JSON progress events on each training log step.`, `MockIntersectionObserver`, `MockWorker`, `MockGoogleGenAI`, `MockBroadcastChannel` (+21 more) +- **47 isolated node(s):** `Emits JSON progress events on each training log step.`, `Remove ANSI escape codes from text.`, `Remove timestamp strings from text.`, `Replace long base64 strings with placeholder.`, `Remove NPM/pnpm warning lines.` (+42 more) These have ≤1 connection - possible missing edges or undocumented components. -- **Thin community `Community 20`** (36 nodes): `storageService.ts`, `StorageManager`, `.clearApiKey()`, `.clearGeminiApiKey()`, `.constructor()`, `.deleteAllBinderAssetsForProject()`, `.deleteBinderAsset()`, `.deleteImage()`, `.deleteProject()`, `.deleteRagVectors()`, `.deleteSnapshot()`, `.deleteStoryCodex()`, `.getApiKey()`, `.getBackend()`, `.getBinderAsset()`, `.getGeminiApiKey()`, `.getImage()`, `.getRagVectors()`, `.getSnapshotData()`, `.getStoryCodex()`, `.hasSavedData()`, `.initializeBackend()`, `.listBinderAssetIds()`, `.listProjects()`, `.listSnapshots()`, `.loadProject()`, `.loadSettings()`, `.saveApiKey()`, `.saveBinderAsset()`, `.saveGeminiApiKey()`, `.saveImage()`, `.saveProject()`, `.saveRagVectors()`, `.saveSettings()`, `.saveSnapshot()`, `.saveStoryCodex()` - Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 30`** (10 nodes): `taskQueue.ts`, `PriorityTaskQueue`, `.constructor()`, `.dequeue()`, `.effectivePriority()`, `.enqueue()`, `.peek()`, `.promoteStarvedTasks()`, `.stats()`, `.totalDepth()` - Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 31`** (9 nodes): `AudioNavigator`, `.announce()`, `.focusElement()`, `.focusFirstIn()`, `.getFocusedLabel()`, `.nextLandmark()`, `.previousLandmark()`, `.scanLandmarks()`, `audioNavigator.ts` - Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 37`** (6 nodes): `getFocusable()`, `onKeyDown()`, `onPointerDown()`, `onPointerMove()`, `onPointerUp()`, `BottomSheet.tsx` - Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 42`** (5 nodes): `DashboardHeader.tsx`, `DashboardContext.ts`, `useDashboardContext()`, `Chip()`, `DashboardHeader()` - Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 46`** (4 nodes): `useSpeechRecognition.ts`, `sanitizeSpeechTranscript()`, `stripControlChars()`, `useSpeechRecognition()` +- **Thin community `Community 22`** (37 nodes): `.initialize()`, `storageService.ts`, `StorageManager`, `.clearApiKey()`, `.clearGeminiApiKey()`, `.constructor()`, `.deleteAllBinderAssetsForProject()`, `.deleteBinderAsset()`, `.deleteImage()`, `.deleteProject()`, `.deleteRagVectors()`, `.deleteSnapshot()`, `.deleteStoryCodex()`, `.getApiKey()`, `.getBackend()`, `.getBinderAsset()`, `.getGeminiApiKey()`, `.getImage()`, `.getRagVectors()`, `.getSnapshotData()`, `.getStoryCodex()`, `.hasSavedData()`, `.initializeBackend()`, `.listBinderAssetIds()`, `.listProjects()`, `.listSnapshots()`, `.loadProject()`, `.loadSettings()`, `.saveApiKey()`, `.saveBinderAsset()`, `.saveGeminiApiKey()`, `.saveImage()`, `.saveProject()`, `.saveRagVectors()`, `.saveSettings()`, `.saveSnapshot()`, `.saveStoryCodex()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 47`** (4 nodes): `makeStorageMock()`, `SpeechSynthesisUtteranceMock`, `.constructor()`, `setup.ts` +- **Thin community `Community 27`** (17 nodes): `cc`, `._applyAttribute()`, `._assert()`, `.constructor()`, `._eof()`, `._isWhitespace()`, `._next()`, `.parse()`, `._peek()`, `._readAttributes()`, `._readIdentifier()`, `._readRegex()`, `._readString()`, `._readStringOrRegex()`, `._skipWhitespace()`, `._throwError()`, `Gb()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 50`** (4 nodes): `makeConfig()`, `makeReviewItem()`, `startPipelinePayload()`, `proForgeSlice.test.ts` +- **Thin community `Community 41`** (5 nodes): `DashboardHeader.tsx`, `DashboardContext.ts`, `useDashboardContext()`, `Chip()`, `DashboardHeader()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 58`** (4 nodes): `useDashboard.test.ts`, `defaultProject()`, `defaultSection()`, `setProjectData()` +- **Thin community `Community 45`** (4 nodes): `useSpeechRecognition.ts`, `sanitizeSpeechTranscript()`, `stripControlChars()`, `useSpeechRecognition()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 66`** (4 nodes): `Drawer.tsx`, `getFocusable()`, `handleEsc()`, `handleTabKey()` +- **Thin community `Community 46`** (4 nodes): `makeStorageMock()`, `SpeechSynthesisUtteranceMock`, `.constructor()`, `setup.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 71`** (4 nodes): `getAllTemplates()`, `getQuestionsForArchetype()`, `getTemplateForArchetype()`, `characterInterviewTemplates.ts` +- **Thin community `Community 48`** (4 nodes): `makeConfig()`, `makeReviewItem()`, `startPipelinePayload()`, `proForgeSlice.test.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 72`** (4 nodes): `wordDiff.ts`, `buildLcsTable()`, `diffTokensToOps()`, `tokenizeWordsAndSpaces()` +- **Thin community `Community 55`** (4 nodes): `useDashboard.test.ts`, `defaultProject()`, `defaultSection()`, `setProjectData()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 80`** (3 nodes): `makeSection()`, `plotBoardService.test.ts`, `plotBoardService.test.ts` +- **Thin community `Community 61`** (4 nodes): `getAllTemplates()`, `getQuestionsForArchetype()`, `getTemplateForArchetype()`, `characterInterviewTemplates.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 85`** (3 nodes): `makeStream()`, `MockGoogleGenAI`, `geminiService.test.ts` +- **Thin community `Community 68`** (3 nodes): `makeSection()`, `plotBoardService.test.ts`, `plotBoardService.test.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 88`** (3 nodes): `makeDeps()`, `aiSuggestions.test.ts`, `aiSuggestions.test.ts` +- **Thin community `Community 73`** (3 nodes): `makeStream()`, `MockGoogleGenAI`, `geminiService.test.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 90`** (3 nodes): `useSwipeGesture.test.ts`, `fireSwipe()`, `makePointerEvent()` +- **Thin community `Community 75`** (3 nodes): `makeDeps()`, `aiSuggestions.test.ts`, `aiSuggestions.test.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 101`** (3 nodes): `types.ts`, `TaskError`, `.constructor()` +- **Thin community `Community 84`** (3 nodes): `types.ts`, `TaskError`, `.constructor()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 133`** (2 nodes): `MockIntersectionObserver`, `BookPreviewView.test.tsx` +- **Thin community `Community 115`** (2 nodes): `MockIntersectionObserver`, `BookPreviewView.test.tsx` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 146`** (2 nodes): `MockWorker`, `duckdbClient.test.ts` +- **Thin community `Community 129`** (2 nodes): `MockWorker`, `duckdbClient.test.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 151`** (2 nodes): `MockBroadcastChannel`, `tabLeaderElection.test.ts` +- **Thin community `Community 134`** (2 nodes): `MockBroadcastChannel`, `tabLeaderElection.test.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 182`** (2 nodes): `useBookPreviewView.test.ts`, `MockIntersectionObserver` +- **Thin community `Community 164`** (2 nodes): `useBookPreviewView.test.ts`, `MockIntersectionObserver` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 224`** (2 nodes): `workerPool.test.ts`, `MockWorker` +- **Thin community `Community 204`** (2 nodes): `workerPool.test.ts`, `MockWorker` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 257`** (2 nodes): `FileSystemService`, `index.ts` +- **Thin community `Community 237`** (2 nodes): `FileSystemService`, `index.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 262`** (2 nodes): `IndexedDBService`, `index.ts` +- **Thin community `Community 242`** (2 nodes): `IndexedDBService`, `index.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. ## Suggested Questions _Questions this graph is uniquely positioned to answer:_ -- **Why does `fn()` connect `Community 3` to `Community 0`, `Community 1`, `Community 6`, `Community 14`, `Community 17`?** - _High betweenness centrality (0.064) - this node is a cross-community bridge._ -- **Why does `t()` connect `Community 7` to `Community 10`, `Community 2`, `Community 4`, `Community 22`?** - _High betweenness centrality (0.048) - this node is a cross-community bridge._ -- **Why does `useTranslation()` connect `Community 4` to `Community 7`?** - _High betweenness centrality (0.028) - this node is a cross-community bridge._ +- **Why does `t()` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 5`, `Community 7`, `Community 10`, `Community 11`, `Community 14`, `Community 25`?** + _High betweenness centrality (0.078) - this node is a cross-community bridge._ +- **Why does `mt()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 11`, `Community 14`, `Community 19`, `Community 23`, `Community 24`?** + _High betweenness centrality (0.068) - this node is a cross-community bridge._ +- **Why does `wx()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 7`, `Community 13`, `Community 16`?** + _High betweenness centrality (0.057) - this node is a cross-community bridge._ +- **Are the 86 inferred relationships involving `mt()` (e.g. with `pE()` and `xE()`) actually correct?** + _`mt()` has 86 INFERRED edges - model-reasoned connections that need verification._ - **Are the 46 inferred relationships involving `fn()` (e.g. with `makeMediaQuery()` and `MockSpeechRecognition()`) actually correct?** _`fn()` has 46 INFERRED edges - model-reasoned connections that need verification._ -- **Are the 30 inferred relationships involving `retryFs()` (e.g. with `.saveProject()` and `.loadProject()`) actually correct?** - _`retryFs()` has 30 INFERRED edges - model-reasoned connections that need verification._ -- **Are the 28 inferred relationships involving `t()` (e.g. with `useHelpView()` and `handleSaveKey()`) actually correct?** - _`t()` has 28 INFERRED edges - model-reasoned connections that need verification._ -- **What connects `Emits JSON progress events on each training log step.`, `MockIntersectionObserver`, `MockWorker` to the rest of the system?** - _26 weakly-connected nodes found - possible documentation gaps or missing edges._ \ No newline at end of file +- **Are the 16 inferred relationships involving `wx()` (e.g. with `for()` and `.addEventListener()`) actually correct?** + _`wx()` has 16 INFERRED edges - model-reasoned connections that need verification._ +- **What connects `Emits JSON progress events on each training log step.`, `Remove ANSI escape codes from text.`, `Remove timestamp strings from text.` to the rest of the system?** + _47 weakly-connected nodes found - possible documentation gaps or missing edges._ \ No newline at end of file diff --git a/hooks/useProForgeOrchestrator.ts b/hooks/useProForgeOrchestrator.ts index de4698e6b..1869b8c34 100644 --- a/hooks/useProForgeOrchestrator.ts +++ b/hooks/useProForgeOrchestrator.ts @@ -3,8 +3,9 @@ * QNBS-v3: Bridges Redux state, orchestrator, and UI components. */ -import { useCallback, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useAppDispatch, useAppSelector } from '../app/hooks'; +import { appStoreRef } from '../app/storeRef'; import { proForgeActions } from '../features/proForge/proForgeSlice'; import type { PipelineConfig, PipelineStage, ReviewItemStatus } from '../features/proForge/types'; import { createProForgeOrchestrator } from '../services/proForge/proForgeOrchestrator'; @@ -50,17 +51,47 @@ export function useProForgeOrchestrator() { [settings, featureFlags, language], ); + // QNBS-v3: Track which project the cached orchestrator was built for, so we rebuild + // it (and drop its stale AbortController/context) when the user switches projects. + const orchestratorProjectIdRef = useRef(null); + + // QNBS-v3: Hydrate persisted run history when a project loads — analytics comparisons across + // runs were lost on reload because the proForge slice is ephemeral. Best-effort. + const projectId = project?.id; + useEffect(() => { + if (!projectId) return; + let cancelled = false; + void (async () => { + try { + const { loadRunHistory } = await import('../services/proForge/proForgeHistoryStore'); + const runs = await loadRunHistory(projectId); + if (!cancelled && runs.length > 0) { + dispatch(proForgeActions.loadRunHistory(runs)); + } + } catch { + // Non-blocking: history hydration failures must not break the view. + } + })(); + return () => { + cancelled = true; + }; + }, [projectId, dispatch]); + const getOrchestrator = useCallback(() => { if (!project) return null; + const projectId = project.id || 'default'; + + if (orchestratorRef.current && orchestratorProjectIdRef.current !== projectId) { + orchestratorRef.current.dispose(); + orchestratorRef.current = null; + } + if (!orchestratorRef.current) { orchestratorRef.current = createProForgeOrchestrator({ dispatch, - getState: () => - ({ - ...orchestratorRef.current!.context.getState(), - proForge: proForgeState, - }) as import('../app/store').RootState, - projectId: project.id || 'default', + // QNBS-v3: Read the LIVE store (was self-referential → infinite recursion + stale state). + getState: () => appStoreRef.current!.getState(), + projectId, manuscript: project.manuscript.map((s) => ({ id: s.id, title: s.title, @@ -72,11 +103,16 @@ export function useProForgeOrchestrator() { worlds: Object.values(project.worlds?.entities ?? {}) .filter(Boolean) .map((w) => ({ id: (w as { id: string }).id, name: (w as { name: string }).name })), - config: currentRun?.config ?? defaultConfig, + // QNBS-v3: Live config getter — agents read this.context.config at execute time, + // so a custom config passed to startPipeline must win over the build-time default. + get config() { + return appStoreRef.current!.getState().proForge.currentRun?.config ?? defaultConfig; + }, }); + orchestratorProjectIdRef.current = projectId; } return orchestratorRef.current; - }, [dispatch, project, proForgeState, defaultConfig, currentRun?.config]); + }, [dispatch, project, defaultConfig]); const startPipeline = useCallback( async (label: string, config: PipelineConfig) => { diff --git a/locales/ar/common.json b/locales/ar/common.json index b1c33aef2..57f5eeb8c 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "جارٍ النظر في شكل قصتك…", "proforge.pipeline.noneActive": "لا يوجد خط أنابيب نشط", "proforge.pipeline.title": "خط أنابيب المؤلّف الأمثل", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "كلمات مكتوبة في الجلسات", "progress.allTime.title": "كل الأوقات", "progress.bestDay.title": "أفضل يوم", diff --git a/locales/de/common.json b/locales/de/common.json index e71f3f19f..0b8e1f18d 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Betrachtet die Struktur deiner Geschichte…", "proforge.pipeline.noneActive": "Keine aktive Pipeline", "proforge.pipeline.title": "Ultimative Autoren-Pipeline", + "proforge.stageName.idle": "Leerlauf", + "proforge.stageName.intake": "Aufnahme & Diagnose", + "proforge.stageName.structural": "Strukturlektorat", + "proforge.stageName.lineProse": "Sprach- & Prosalektorat", + "proforge.stageName.copyEdit": "Schlusslektorat", + "proforge.stageName.proof": "Korrektorat", + "proforge.stageName.production": "Produktion", + "proforge.stageName.publishing": "Veröffentlichung", + "proforge.stageName.analytics": "Auswertung", + "proforge.stageName.archived": "Archiviert", + "proforge.review.heading": "Prüfung: {{stage}}", + "proforge.review.counts": "{{pending}} offen · {{accepted}} übernommen · {{rejected}} abgelehnt", + "proforge.review.quickAccept": "Schnell übernehmen ({{count}})", + "proforge.review.quickAcceptTitle": "Alle unkritischen Vorschläge mit hoher Konfidenz übernehmen", + "proforge.review.acceptAll": "Alle übernehmen", + "proforge.review.rejectAll": "Alle ablehnen", + "proforge.review.criticalHeadingOne": "{{count}} kritisches Problem erfordert Aufmerksamkeit", + "proforge.review.criticalHeadingOther": "{{count}} kritische Probleme erfordern Aufmerksamkeit", + "proforge.review.acceptAllCritical": "Alle kritischen übernehmen", + "proforge.review.moreCritical": "+{{count}} weitere kritische Probleme unten", + "proforge.review.filter.all": "Alle", + "proforge.review.filter.pending": "Offen", + "proforge.review.filter.accepted": "Übernommen", + "proforge.review.filter.rejected": "Abgelehnt", + "proforge.review.noItems": "Keine Prüfpunkte verfügbar.", + "proforge.review.noItemsForFilter": "Keine Einträge für diesen Filter.", + "proforge.review.skipStage": "Phase überspringen", + "proforge.review.submitContinue": "Übernehmen & fortfahren", + "proforge.review.pendingCount": "{{count}} offen", + "proforge.review.confidence": "{{percent}} % Konfidenz", + "proforge.review.original": "Original:", + "proforge.review.proposed": "Vorschlag:", + "proforge.review.showDetails": "Details anzeigen", + "proforge.review.showLess": "Weniger anzeigen", + "proforge.review.accept": "Übernehmen", + "proforge.review.reject": "Ablehnen", + "proforge.review.ignore": "Ignorieren", + "proforge.review.group.critical": "Kritische Maßnahmen", + "proforge.review.group.warning": "Warnungen", + "proforge.review.group.info": "Vorschläge", + "proforge.review.severity.critical": "Kritisch", + "proforge.review.severity.warning": "Warnung", + "proforge.review.severity.info": "Info", + "proforge.review.announceSubmitted": "Prüfung {{stage}} übermittelt", + "proforge.review.type.structuralEdit": "Struktur", + "proforge.review.type.proseEdit": "Prosa", + "proforge.review.type.grammarEdit": "Grammatik", + "proforge.review.type.styleEdit": "Stil", + "proforge.review.type.repetitionHit": "Wiederholung", + "proforge.review.type.consistencyIssue": "Konsistenz", + "proforge.review.type.plotHole": "Logiklücke", + "proforge.review.type.legalWarning": "Rechtliches", + "proforge.review.type.technicalIssue": "Technik", + "proforge.review.type.pacingIssue": "Tempo", + "proforge.review.type.arcIssue": "Spannungsbogen", "progress.allTime.label": "in Sitzungen geschriebene Wörter", "progress.allTime.title": "Gesamt", "progress.bestDay.title": "Bester Tag", diff --git a/locales/el/common.json b/locales/el/common.json index c6f7bb448..8b1590ea6 100644 --- a/locales/el/common.json +++ b/locales/el/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Κοιτάζοντας τη μορφή της ιστορίας σας…", "proforge.pipeline.noneActive": "Δεν υπάρχει ενεργός αγωγός", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "λέξεις γραμμένες σε συνεδρίες", "progress.allTime.title": "Όλων των Εποχών", "progress.bestDay.title": "Καλύτερη μέρα", diff --git a/locales/en/common.json b/locales/en/common.json index f2fada71e..6810e724e 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -491,6 +491,61 @@ "proforge.emptyState.title": "Your manuscript, refined.", "proforge.emptyState.description": "ProForge reads every chapter, then walks you through editing stage by stage. Nothing gets lost — every change is reversible.", "proforge.enabledHint": "ProForge Pipeline enabled. Open the Writer view and click the ProForge button in the tools panel to start.", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.structural": "Structural Edit", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.production": "Production", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.acceptAll": "Accept All", + "proforge.review.rejectAll": "Reject All", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.original": "Original:", + "proforge.review.proposed": "Proposed:", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.accept": "Accept", + "proforge.review.reject": "Reject", + "proforge.review.ignore": "Ignore", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.warning": "Warnings", + "proforge.review.group.info": "Suggestions", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.warning": "Warning", + "proforge.review.severity.info": "Info", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.technicalIssue": "Technical", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.arcIssue": "Arc", "sceneboard.emptyState.title": "Your plot board is a blank canvas", "sceneboard.emptyState.description": "Add your first scene or import your outline to start planning your story.", "characters.emptyState.title": "Your cast list is empty", diff --git a/locales/es/common.json b/locales/es/common.json index 0bf2ad516..87b3ec96d 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Observando la forma de tu historia…", "proforge.pipeline.noneActive": "Sin pipeline activa", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "palabras escritas en sesiones", "progress.allTime.title": "Total histórico", "progress.bestDay.title": "Mejor día", diff --git a/locales/es/settings.json b/locales/es/settings.json index 02c1cdd8c..9e6391f1f 100644 --- a/locales/es/settings.json +++ b/locales/es/settings.json @@ -546,11 +546,11 @@ "settings.privacy.encryptionUnlockButton": "Desbloquear", "settings.privacy.encryptionWarning": "Si olvidas tu contraseña, tus datos no podrán recuperarse.", "settings.privacy.encryptionWrongPassphrase": "Contraseña incorrecta — descifrado fallido", + "settings.privacy.euDataResidency": "Residencia de datos en la UE", + "settings.privacy.euDataResidencyHint": "Mantener todos los datos en servidores de la Unión Europea cuando sea posible.", "settings.privacy.localStorageOnly": "Solo almacenamiento local", "settings.privacy.migrationComplete": "Todos los datos cifrados correctamente", "settings.privacy.migrationProgress": "Cifrando datos existentes…", - "settings.privacy.euDataResidency": "Residencia de datos en la UE", - "settings.privacy.euDataResidencyHint": "Mantener todos los datos en servidores de la Unión Europea cuando sea posible.", "settings.privacy.shareUsageData": "Compartir datos de uso", "settings.privacy.title": "Privacidad y seguridad", "settings.projectAi.activeIndicator": "IA de proyecto activa", @@ -627,6 +627,9 @@ "settings.voice.downloadTtsModel": "Download TTS (Kokoro)", "settings.voice.enableHint": "Activate voice commands, dictation, and audio feedback.", "settings.voice.enableLabel": "Enable Voice Support", + "settings.voice.engine.auto": "Automático (recomendado)", + "settings.voice.engine.wasm": "Modelo WASM local (sin conexión)", + "settings.voice.engine.webSpeech": "Web Speech API (nube)", "settings.voice.feedbackLevel": "Audio Feedback Level", "settings.voice.intro": "Control StoryCraft Studio with your voice. All voice processing runs locally for maximum privacy.", "settings.voice.level.minimal": "Minimal — errors only", @@ -641,12 +644,24 @@ "settings.voice.privacy.consentDecline": "Rechazar", "settings.voice.privacy.consentTitle": "Consentimiento Web Speech", "settings.voice.privacy.revokeConsent": "Revocar consentimiento", + "settings.voice.privacy.revokeConsentHint": "Revoca tu consentimiento de Web Speech. La voz usará WASM local o se desactivará.", "settings.voice.privacy.statusExternal": "Externo", "settings.voice.privacy.statusLocal": "Local", "settings.voice.privacyNotice": "Voice data is processed locally on your device. No audio is sent to the cloud unless you explicitly enable cloud fallback below.", + "settings.voice.pttHint": "Atajo Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Mantener para grabar.", + "settings.voice.speechRate": "Velocidad de habla", + "settings.voice.speechRateHint": "Qué tan rápido habla la voz de síntesis. 1.0 = velocidad normal.", + "settings.voice.speechVolume": "Volumen de voz", + "settings.voice.speechVolumeHint": "Volumen de la voz de síntesis. No afecta la entrada del micrófono.", + "settings.voice.sttEngine": "Motor de reconocimiento de voz (STT)", + "settings.voice.sttEngineHint": "Elige el motor que convierte tu voz en texto. 'Auto' selecciona la mejor opción disponible.", "settings.voice.title": "Voice Full Support", + "settings.voice.ttsEngine": "Motor de síntesis de voz (TTS)", + "settings.voice.ttsEngineHint": "Elige la voz que lee las respuestas en voz alta. 'Auto' selecciona la mejor opción disponible.", "settings.voice.ttsMuted": "Mute All Speech Output", "settings.voice.ttsMutedHint": "Disable text-to-speech while keeping visual indicators.", + "settings.voice.wakeWordPhrase": "Frase de activación", + "settings.voice.wakeWordPhraseHint": "La frase que dices para activar los comandos de voz sin manos.", "settings.voice.wasmModels": "Modelos de voz WASM", "settings.voice.wasmModelsNotReady": "Modelos Whisper STT (~42 MB) y Kokoro TTS (~15 MB) no descargados", "settings.voice.wasmModelsReady": "Descargados y listos para procesamiento de voz local", @@ -655,20 +670,5 @@ "voice.modelDownload.error": "Descarga fallida: {{error}}", "voice.modelDownload.progress": "{{percent}}% completado", "voice.modelDownload.retry": "Reintentar", - "voice.modelDownload.title": "Descarga de modelo de voz", - "settings.voice.sttEngine": "Motor de reconocimiento de voz (STT)", - "settings.voice.sttEngineHint": "Elige el motor que convierte tu voz en texto. 'Auto' selecciona la mejor opción disponible.", - "settings.voice.ttsEngine": "Motor de síntesis de voz (TTS)", - "settings.voice.ttsEngineHint": "Elige la voz que lee las respuestas en voz alta. 'Auto' selecciona la mejor opción disponible.", - "settings.voice.engine.auto": "Automático (recomendado)", - "settings.voice.engine.webSpeech": "Web Speech API (nube)", - "settings.voice.engine.wasm": "Modelo WASM local (sin conexión)", - "settings.voice.speechRate": "Velocidad de habla", - "settings.voice.speechRateHint": "Qué tan rápido habla la voz de síntesis. 1.0 = velocidad normal.", - "settings.voice.speechVolume": "Volumen de voz", - "settings.voice.speechVolumeHint": "Volumen de la voz de síntesis. No afecta la entrada del micrófono.", - "settings.voice.wakeWordPhrase": "Frase de activación", - "settings.voice.wakeWordPhraseHint": "La frase que dices para activar los comandos de voz sin manos.", - "settings.voice.pttHint": "Atajo Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Mantener para grabar.", - "settings.voice.privacy.revokeConsentHint": "Revoca tu consentimiento de Web Speech. La voz usará WASM local o se desactivará." + "voice.modelDownload.title": "Descarga de modelo de voz" } diff --git a/locales/fr/common.json b/locales/fr/common.json index 77b5182c2..a34bb87b4 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Examen de la structure de votre histoire…", "proforge.pipeline.noneActive": "Aucune pipeline active", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "mots écrits en sessions", "progress.allTime.title": "Total cumulé", "progress.bestDay.title": "Meilleur jour", diff --git a/locales/fr/settings.json b/locales/fr/settings.json index 20fa914d8..2103135ff 100644 --- a/locales/fr/settings.json +++ b/locales/fr/settings.json @@ -546,11 +546,11 @@ "settings.privacy.encryptionUnlockButton": "Déverrouiller", "settings.privacy.encryptionWarning": "Si vous oubliez votre phrase secrète, vos données ne pourront pas être récupérées.", "settings.privacy.encryptionWrongPassphrase": "Phrase secrète incorrecte — déchiffrement échoué", + "settings.privacy.euDataResidency": "Résidence des données UE", + "settings.privacy.euDataResidencyHint": "Conserver toutes les données sur des serveurs de l'Union européenne si possible.", "settings.privacy.localStorageOnly": "Stockage local uniquement", "settings.privacy.migrationComplete": "Toutes les données ont été chiffrées avec succès", "settings.privacy.migrationProgress": "Chiffrement des données existantes…", - "settings.privacy.euDataResidency": "Résidence des données UE", - "settings.privacy.euDataResidencyHint": "Conserver toutes les données sur des serveurs de l'Union européenne si possible.", "settings.privacy.shareUsageData": "Partager les données d'utilisation", "settings.privacy.title": "Confidentialité et sécurité", "settings.projectAi.activeIndicator": "IA projet active", @@ -627,6 +627,9 @@ "settings.voice.downloadTtsModel": "Download TTS (Kokoro)", "settings.voice.enableHint": "Activate voice commands, dictation, and audio feedback.", "settings.voice.enableLabel": "Enable Voice Support", + "settings.voice.engine.auto": "Automatique (recommandé)", + "settings.voice.engine.wasm": "Modèle WASM local (hors ligne)", + "settings.voice.engine.webSpeech": "Web Speech API (cloud)", "settings.voice.feedbackLevel": "Audio Feedback Level", "settings.voice.intro": "Control StoryCraft Studio with your voice. All voice processing runs locally for maximum privacy.", "settings.voice.level.minimal": "Minimal — errors only", @@ -641,12 +644,24 @@ "settings.voice.privacy.consentDecline": "Refuser", "settings.voice.privacy.consentTitle": "Consentement Web Speech", "settings.voice.privacy.revokeConsent": "Révoquer le consentement", + "settings.voice.privacy.revokeConsentHint": "Révoque votre consentement Web Speech. La voix reviendra au WASM local ou sera désactivée.", "settings.voice.privacy.statusExternal": "Externe", "settings.voice.privacy.statusLocal": "Local", "settings.voice.privacyNotice": "Voice data is processed locally on your device. No audio is sent to the cloud unless you explicitly enable cloud fallback below.", + "settings.voice.pttHint": "Raccourci Push-to-Talk : Ctrl+Maj+V (Windows/Linux) ou ⌘+Maj+V (Mac). Maintenir pour enregistrer.", + "settings.voice.speechRate": "Vitesse d'élocution", + "settings.voice.speechRateHint": "Vitesse à laquelle la voix de synthèse vocale parle. 1,0 = vitesse normale.", + "settings.voice.speechVolume": "Volume de la parole", + "settings.voice.speechVolumeHint": "Volume de la voix de synthèse vocale. N'affecte pas l'entrée du microphone.", + "settings.voice.sttEngine": "Moteur de reconnaissance vocale (STT)", + "settings.voice.sttEngineHint": "Choisissez le moteur qui convertit votre parole en texte. 'Auto' choisit la meilleure option disponible.", "settings.voice.title": "Voice Full Support", + "settings.voice.ttsEngine": "Moteur de synthèse vocale (TTS)", + "settings.voice.ttsEngineHint": "Choisissez la voix qui lit les réponses à voix haute. 'Auto' choisit la meilleure option disponible.", "settings.voice.ttsMuted": "Mute All Speech Output", "settings.voice.ttsMutedHint": "Disable text-to-speech while keeping visual indicators.", + "settings.voice.wakeWordPhrase": "Phrase d'activation", + "settings.voice.wakeWordPhraseHint": "La phrase que vous dites pour activer les commandes vocales en mains libres.", "settings.voice.wasmModels": "Modèles vocaux WASM", "settings.voice.wasmModelsNotReady": "Modèles Whisper STT (~42 Mo) et Kokoro TTS (~15 Mo) non téléchargés", "settings.voice.wasmModelsReady": "Téléchargés et prêts pour le traitement vocal local", @@ -655,20 +670,5 @@ "voice.modelDownload.error": "Échec du téléchargement : {{error}}", "voice.modelDownload.progress": "{{percent}}% terminé", "voice.modelDownload.retry": "Réessayer", - "voice.modelDownload.title": "Téléchargement du modèle vocal", - "settings.voice.sttEngine": "Moteur de reconnaissance vocale (STT)", - "settings.voice.sttEngineHint": "Choisissez le moteur qui convertit votre parole en texte. 'Auto' choisit la meilleure option disponible.", - "settings.voice.ttsEngine": "Moteur de synthèse vocale (TTS)", - "settings.voice.ttsEngineHint": "Choisissez la voix qui lit les réponses à voix haute. 'Auto' choisit la meilleure option disponible.", - "settings.voice.engine.auto": "Automatique (recommandé)", - "settings.voice.engine.webSpeech": "Web Speech API (cloud)", - "settings.voice.engine.wasm": "Modèle WASM local (hors ligne)", - "settings.voice.speechRate": "Vitesse d'élocution", - "settings.voice.speechRateHint": "Vitesse à laquelle la voix de synthèse vocale parle. 1,0 = vitesse normale.", - "settings.voice.speechVolume": "Volume de la parole", - "settings.voice.speechVolumeHint": "Volume de la voix de synthèse vocale. N'affecte pas l'entrée du microphone.", - "settings.voice.wakeWordPhrase": "Phrase d'activation", - "settings.voice.wakeWordPhraseHint": "La phrase que vous dites pour activer les commandes vocales en mains libres.", - "settings.voice.pttHint": "Raccourci Push-to-Talk : Ctrl+Maj+V (Windows/Linux) ou ⌘+Maj+V (Mac). Maintenir pour enregistrer.", - "settings.voice.privacy.revokeConsentHint": "Révoque votre consentement Web Speech. La voix reviendra au WASM local ou sera désactivée." + "voice.modelDownload.title": "Téléchargement du modèle vocal" } diff --git a/locales/he/common.json b/locales/he/common.json index 35ed21f3a..2e0ed54f2 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "מתבונן במבנה הסיפור שלכם…", "proforge.pipeline.noneActive": "אין צינור פעיל", "proforge.pipeline.title": "צינור הסופר האולטימטיבי", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "מילים שנכתבו בהפעלות", "progress.allTime.title": "כל הזמן", "progress.bestDay.title": "היום הטוב ביותר", diff --git a/locales/it/common.json b/locales/it/common.json index 3059369eb..b8ce9d005 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Osservando la forma della tua storia…", "proforge.pipeline.noneActive": "Nessuna pipeline attiva", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "parole scritte nelle sessioni", "progress.allTime.title": "Totale storico", "progress.bestDay.title": "Giorno migliore", diff --git a/locales/it/settings.json b/locales/it/settings.json index 9ff182243..404442bab 100644 --- a/locales/it/settings.json +++ b/locales/it/settings.json @@ -546,11 +546,11 @@ "settings.privacy.encryptionUnlockButton": "Sblocca", "settings.privacy.encryptionWarning": "Se dimentichi la passphrase, i tuoi dati non potranno essere recuperati.", "settings.privacy.encryptionWrongPassphrase": "Passphrase errata — decifratura non riuscita", + "settings.privacy.euDataResidency": "Residenza dati UE", + "settings.privacy.euDataResidencyHint": "Mantenere tutti i dati su server dell'Unione Europea ove possibile.", "settings.privacy.localStorageOnly": "Solo archiviazione locale", "settings.privacy.migrationComplete": "Tutti i dati cifrati con successo", "settings.privacy.migrationProgress": "Cifratura dei dati esistenti in corso…", - "settings.privacy.euDataResidency": "Residenza dati UE", - "settings.privacy.euDataResidencyHint": "Mantenere tutti i dati su server dell'Unione Europea ove possibile.", "settings.privacy.shareUsageData": "Condividi dati di utilizzo", "settings.privacy.title": "Privacy e sicurezza", "settings.projectAi.activeIndicator": "IA progetto attiva", @@ -627,6 +627,9 @@ "settings.voice.downloadTtsModel": "Download TTS (Kokoro)", "settings.voice.enableHint": "Attiva comandi vocali, dettatura e feedback audio.", "settings.voice.enableLabel": "Abilita controllo vocale", + "settings.voice.engine.auto": "Automatico (consigliato)", + "settings.voice.engine.wasm": "Modello WASM locale (offline)", + "settings.voice.engine.webSpeech": "Web Speech API (cloud)", "settings.voice.feedbackLevel": "Livello di feedback audio", "settings.voice.intro": "Controlla StoryCraft Studio con la tua voce. Tutto l’elaborazione vocale avviene localmente per la massima privacy.", "settings.voice.level.minimal": "Minimo — solo errori", @@ -641,12 +644,24 @@ "settings.voice.privacy.consentDecline": "Rifiuta", "settings.voice.privacy.consentTitle": "Consenso Web Speech", "settings.voice.privacy.revokeConsent": "Revoca il consenso", + "settings.voice.privacy.revokeConsentHint": "Revoca il tuo consenso Web Speech. La voce passerà al WASM locale o verrà disattivata.", "settings.voice.privacy.statusExternal": "Esterno", "settings.voice.privacy.statusLocal": "Locale", "settings.voice.privacyNotice": "I dati vocali vengono elaborati localmente sul tuo dispositivo. Nessun audio viene inviato al cloud a meno che tu non abiliti esplicitamente il fallback cloud.", + "settings.voice.pttHint": "Scorciatoia Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Tenere premuto per registrare.", + "settings.voice.speechRate": "Velocità del parlato", + "settings.voice.speechRateHint": "Quanto velocemente parla la voce di sintesi. 1.0 = velocità normale.", + "settings.voice.speechVolume": "Volume del parlato", + "settings.voice.speechVolumeHint": "Volume della voce di sintesi. Non influisce sull'ingresso del microfono.", + "settings.voice.sttEngine": "Motore di riconoscimento vocale (STT)", + "settings.voice.sttEngineHint": "Scegli il motore che converte il tuo parlato in testo. 'Auto' sceglie la migliore opzione disponibile.", "settings.voice.title": "Controllo vocale", + "settings.voice.ttsEngine": "Motore di sintesi vocale (TTS)", + "settings.voice.ttsEngineHint": "Scegli la voce che legge le risposte ad alta voce. 'Auto' sceglie la migliore opzione disponibile.", "settings.voice.ttsMuted": "Silenzia tutta l’uscita vocale", "settings.voice.ttsMutedHint": "Disabilita il text-to-speech mantenendo gli indicatori visivi.", + "settings.voice.wakeWordPhrase": "Frase di attivazione", + "settings.voice.wakeWordPhraseHint": "La frase che dici per attivare i comandi vocali a mani libere.", "settings.voice.wasmModels": "Modelli vocali WASM", "settings.voice.wasmModelsNotReady": "Modelli Whisper STT (~42 MB) e Kokoro TTS (~15 MB) non scaricati", "settings.voice.wasmModelsReady": "Scaricati e pronti per il riconoscimento vocale locale", @@ -655,20 +670,5 @@ "voice.modelDownload.error": "Download fallito: {{error}}", "voice.modelDownload.progress": "{{percent}}% completato", "voice.modelDownload.retry": "Riprova", - "voice.modelDownload.title": "Download modello vocale", - "settings.voice.sttEngine": "Motore di riconoscimento vocale (STT)", - "settings.voice.sttEngineHint": "Scegli il motore che converte il tuo parlato in testo. 'Auto' sceglie la migliore opzione disponibile.", - "settings.voice.ttsEngine": "Motore di sintesi vocale (TTS)", - "settings.voice.ttsEngineHint": "Scegli la voce che legge le risposte ad alta voce. 'Auto' sceglie la migliore opzione disponibile.", - "settings.voice.engine.auto": "Automatico (consigliato)", - "settings.voice.engine.webSpeech": "Web Speech API (cloud)", - "settings.voice.engine.wasm": "Modello WASM locale (offline)", - "settings.voice.speechRate": "Velocità del parlato", - "settings.voice.speechRateHint": "Quanto velocemente parla la voce di sintesi. 1.0 = velocità normale.", - "settings.voice.speechVolume": "Volume del parlato", - "settings.voice.speechVolumeHint": "Volume della voce di sintesi. Non influisce sull'ingresso del microfono.", - "settings.voice.wakeWordPhrase": "Frase di attivazione", - "settings.voice.wakeWordPhraseHint": "La frase che dici per attivare i comandi vocali a mani libere.", - "settings.voice.pttHint": "Scorciatoia Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Tenere premuto per registrare.", - "settings.voice.privacy.revokeConsentHint": "Revoca il tuo consenso Web Speech. La voce passerà al WASM locale o verrà disattivata." + "voice.modelDownload.title": "Download modello vocale" } diff --git a/locales/ja/common.json b/locales/ja/common.json index e37de140e..92aed1cb3 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "あなたの物語の形を見てみると…", "proforge.pipeline.noneActive": "アクティブなパイプラインがありません", "proforge.pipeline.title": "究極の著者パイプライン", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "セッションで書いた言葉", "progress.allTime.title": "オールタイム", "progress.bestDay.title": "最高の一日", diff --git a/locales/pt/common.json b/locales/pt/common.json index 0a288b7df..4477019ad 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Olhando para a forma da sua história…", "proforge.pipeline.noneActive": "Nenhum pipeline ativo", "proforge.pipeline.title": "Pipeline final do autor", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "palavras escritas em sessões", "progress.allTime.title": "Todos os tempos", "progress.bestDay.title": "Melhor dia", diff --git a/locales/zh/common.json b/locales/zh/common.json index 615248862..159faf13f 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "看看你的故事的形状……", "proforge.pipeline.noneActive": "无活动管道", "proforge.pipeline.title": "最终作者管道", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "会话中写下的单词", "progress.allTime.title": "所有时间", "progress.bestDay.title": "最好的一天", diff --git a/public/locales/ar/bundle.json b/public/locales/ar/bundle.json index 54f1f94c5..f8459c1aa 100644 --- a/public/locales/ar/bundle.json +++ b/public/locales/ar/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "جارٍ النظر في شكل قصتك…", "proforge.pipeline.noneActive": "لا يوجد خط أنابيب نشط", "proforge.pipeline.title": "خط أنابيب المؤلّف الأمثل", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "كلمات مكتوبة في الجلسات", "progress.allTime.title": "كل الأوقات", "progress.bestDay.title": "أفضل يوم", diff --git a/public/locales/de/bundle.json b/public/locales/de/bundle.json index 94d5f1c18..92ca202f1 100644 --- a/public/locales/de/bundle.json +++ b/public/locales/de/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Betrachtet die Struktur deiner Geschichte…", "proforge.pipeline.noneActive": "Keine aktive Pipeline", "proforge.pipeline.title": "Ultimative Autoren-Pipeline", + "proforge.stageName.idle": "Leerlauf", + "proforge.stageName.intake": "Aufnahme & Diagnose", + "proforge.stageName.structural": "Strukturlektorat", + "proforge.stageName.lineProse": "Sprach- & Prosalektorat", + "proforge.stageName.copyEdit": "Schlusslektorat", + "proforge.stageName.proof": "Korrektorat", + "proforge.stageName.production": "Produktion", + "proforge.stageName.publishing": "Veröffentlichung", + "proforge.stageName.analytics": "Auswertung", + "proforge.stageName.archived": "Archiviert", + "proforge.review.heading": "Prüfung: {{stage}}", + "proforge.review.counts": "{{pending}} offen · {{accepted}} übernommen · {{rejected}} abgelehnt", + "proforge.review.quickAccept": "Schnell übernehmen ({{count}})", + "proforge.review.quickAcceptTitle": "Alle unkritischen Vorschläge mit hoher Konfidenz übernehmen", + "proforge.review.acceptAll": "Alle übernehmen", + "proforge.review.rejectAll": "Alle ablehnen", + "proforge.review.criticalHeadingOne": "{{count}} kritisches Problem erfordert Aufmerksamkeit", + "proforge.review.criticalHeadingOther": "{{count}} kritische Probleme erfordern Aufmerksamkeit", + "proforge.review.acceptAllCritical": "Alle kritischen übernehmen", + "proforge.review.moreCritical": "+{{count}} weitere kritische Probleme unten", + "proforge.review.filter.all": "Alle", + "proforge.review.filter.pending": "Offen", + "proforge.review.filter.accepted": "Übernommen", + "proforge.review.filter.rejected": "Abgelehnt", + "proforge.review.noItems": "Keine Prüfpunkte verfügbar.", + "proforge.review.noItemsForFilter": "Keine Einträge für diesen Filter.", + "proforge.review.skipStage": "Phase überspringen", + "proforge.review.submitContinue": "Übernehmen & fortfahren", + "proforge.review.pendingCount": "{{count}} offen", + "proforge.review.confidence": "{{percent}} % Konfidenz", + "proforge.review.original": "Original:", + "proforge.review.proposed": "Vorschlag:", + "proforge.review.showDetails": "Details anzeigen", + "proforge.review.showLess": "Weniger anzeigen", + "proforge.review.accept": "Übernehmen", + "proforge.review.reject": "Ablehnen", + "proforge.review.ignore": "Ignorieren", + "proforge.review.group.critical": "Kritische Maßnahmen", + "proforge.review.group.warning": "Warnungen", + "proforge.review.group.info": "Vorschläge", + "proforge.review.severity.critical": "Kritisch", + "proforge.review.severity.warning": "Warnung", + "proforge.review.severity.info": "Info", + "proforge.review.announceSubmitted": "Prüfung {{stage}} übermittelt", + "proforge.review.type.structuralEdit": "Struktur", + "proforge.review.type.proseEdit": "Prosa", + "proforge.review.type.grammarEdit": "Grammatik", + "proforge.review.type.styleEdit": "Stil", + "proforge.review.type.repetitionHit": "Wiederholung", + "proforge.review.type.consistencyIssue": "Konsistenz", + "proforge.review.type.plotHole": "Logiklücke", + "proforge.review.type.legalWarning": "Rechtliches", + "proforge.review.type.technicalIssue": "Technik", + "proforge.review.type.pacingIssue": "Tempo", + "proforge.review.type.arcIssue": "Spannungsbogen", "progress.allTime.label": "in Sitzungen geschriebene Wörter", "progress.allTime.title": "Gesamt", "progress.bestDay.title": "Bester Tag", diff --git a/public/locales/el/bundle.json b/public/locales/el/bundle.json index e68d5e6bb..cd6624588 100644 --- a/public/locales/el/bundle.json +++ b/public/locales/el/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Κοιτάζοντας τη μορφή της ιστορίας σας…", "proforge.pipeline.noneActive": "Δεν υπάρχει ενεργός αγωγός", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "λέξεις γραμμένες σε συνεδρίες", "progress.allTime.title": "Όλων των Εποχών", "progress.bestDay.title": "Καλύτερη μέρα", diff --git a/public/locales/en/bundle.json b/public/locales/en/bundle.json index 86443951a..b7ea93d5d 100644 --- a/public/locales/en/bundle.json +++ b/public/locales/en/bundle.json @@ -491,6 +491,61 @@ "proforge.emptyState.title": "Your manuscript, refined.", "proforge.emptyState.description": "ProForge reads every chapter, then walks you through editing stage by stage. Nothing gets lost — every change is reversible.", "proforge.enabledHint": "ProForge Pipeline enabled. Open the Writer view and click the ProForge button in the tools panel to start.", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.structural": "Structural Edit", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.production": "Production", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.acceptAll": "Accept All", + "proforge.review.rejectAll": "Reject All", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.original": "Original:", + "proforge.review.proposed": "Proposed:", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.accept": "Accept", + "proforge.review.reject": "Reject", + "proforge.review.ignore": "Ignore", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.warning": "Warnings", + "proforge.review.group.info": "Suggestions", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.warning": "Warning", + "proforge.review.severity.info": "Info", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.technicalIssue": "Technical", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.arcIssue": "Arc", "sceneboard.emptyState.title": "Your plot board is a blank canvas", "sceneboard.emptyState.description": "Add your first scene or import your outline to start planning your story.", "characters.emptyState.title": "Your cast list is empty", diff --git a/public/locales/es/bundle.json b/public/locales/es/bundle.json index 55810ea93..aa73bf86a 100644 --- a/public/locales/es/bundle.json +++ b/public/locales/es/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Observando la forma de tu historia…", "proforge.pipeline.noneActive": "Sin pipeline activa", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "palabras escritas en sesiones", "progress.allTime.title": "Total histórico", "progress.bestDay.title": "Mejor día", @@ -1865,11 +1920,11 @@ "settings.privacy.encryptionUnlockButton": "Desbloquear", "settings.privacy.encryptionWarning": "Si olvidas tu contraseña, tus datos no podrán recuperarse.", "settings.privacy.encryptionWrongPassphrase": "Contraseña incorrecta — descifrado fallido", + "settings.privacy.euDataResidency": "Residencia de datos en la UE", + "settings.privacy.euDataResidencyHint": "Mantener todos los datos en servidores de la Unión Europea cuando sea posible.", "settings.privacy.localStorageOnly": "Solo almacenamiento local", "settings.privacy.migrationComplete": "Todos los datos cifrados correctamente", "settings.privacy.migrationProgress": "Cifrando datos existentes…", - "settings.privacy.euDataResidency": "Residencia de datos en la UE", - "settings.privacy.euDataResidencyHint": "Mantener todos los datos en servidores de la Unión Europea cuando sea posible.", "settings.privacy.shareUsageData": "Compartir datos de uso", "settings.privacy.title": "Privacidad y seguridad", "settings.projectAi.activeIndicator": "IA de proyecto activa", @@ -1946,6 +2001,9 @@ "settings.voice.downloadTtsModel": "Download TTS (Kokoro)", "settings.voice.enableHint": "Activate voice commands, dictation, and audio feedback.", "settings.voice.enableLabel": "Enable Voice Support", + "settings.voice.engine.auto": "Automático (recomendado)", + "settings.voice.engine.wasm": "Modelo WASM local (sin conexión)", + "settings.voice.engine.webSpeech": "Web Speech API (nube)", "settings.voice.feedbackLevel": "Audio Feedback Level", "settings.voice.intro": "Control StoryCraft Studio with your voice. All voice processing runs locally for maximum privacy.", "settings.voice.level.minimal": "Minimal — errors only", @@ -1960,12 +2018,24 @@ "settings.voice.privacy.consentDecline": "Rechazar", "settings.voice.privacy.consentTitle": "Consentimiento Web Speech", "settings.voice.privacy.revokeConsent": "Revocar consentimiento", + "settings.voice.privacy.revokeConsentHint": "Revoca tu consentimiento de Web Speech. La voz usará WASM local o se desactivará.", "settings.voice.privacy.statusExternal": "Externo", "settings.voice.privacy.statusLocal": "Local", "settings.voice.privacyNotice": "Voice data is processed locally on your device. No audio is sent to the cloud unless you explicitly enable cloud fallback below.", + "settings.voice.pttHint": "Atajo Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Mantener para grabar.", + "settings.voice.speechRate": "Velocidad de habla", + "settings.voice.speechRateHint": "Qué tan rápido habla la voz de síntesis. 1.0 = velocidad normal.", + "settings.voice.speechVolume": "Volumen de voz", + "settings.voice.speechVolumeHint": "Volumen de la voz de síntesis. No afecta la entrada del micrófono.", + "settings.voice.sttEngine": "Motor de reconocimiento de voz (STT)", + "settings.voice.sttEngineHint": "Elige el motor que convierte tu voz en texto. 'Auto' selecciona la mejor opción disponible.", "settings.voice.title": "Voice Full Support", + "settings.voice.ttsEngine": "Motor de síntesis de voz (TTS)", + "settings.voice.ttsEngineHint": "Elige la voz que lee las respuestas en voz alta. 'Auto' selecciona la mejor opción disponible.", "settings.voice.ttsMuted": "Mute All Speech Output", "settings.voice.ttsMutedHint": "Disable text-to-speech while keeping visual indicators.", + "settings.voice.wakeWordPhrase": "Frase de activación", + "settings.voice.wakeWordPhraseHint": "La frase que dices para activar los comandos de voz sin manos.", "settings.voice.wasmModels": "Modelos de voz WASM", "settings.voice.wasmModelsNotReady": "Modelos Whisper STT (~42 MB) y Kokoro TTS (~15 MB) no descargados", "settings.voice.wasmModelsReady": "Descargados y listos para procesamiento de voz local", @@ -1975,21 +2045,6 @@ "voice.modelDownload.progress": "{{percent}}% completado", "voice.modelDownload.retry": "Reintentar", "voice.modelDownload.title": "Descarga de modelo de voz", - "settings.voice.sttEngine": "Motor de reconocimiento de voz (STT)", - "settings.voice.sttEngineHint": "Elige el motor que convierte tu voz en texto. 'Auto' selecciona la mejor opción disponible.", - "settings.voice.ttsEngine": "Motor de síntesis de voz (TTS)", - "settings.voice.ttsEngineHint": "Elige la voz que lee las respuestas en voz alta. 'Auto' selecciona la mejor opción disponible.", - "settings.voice.engine.auto": "Automático (recomendado)", - "settings.voice.engine.webSpeech": "Web Speech API (nube)", - "settings.voice.engine.wasm": "Modelo WASM local (sin conexión)", - "settings.voice.speechRate": "Velocidad de habla", - "settings.voice.speechRateHint": "Qué tan rápido habla la voz de síntesis. 1.0 = velocidad normal.", - "settings.voice.speechVolume": "Volumen de voz", - "settings.voice.speechVolumeHint": "Volumen de la voz de síntesis. No afecta la entrada del micrófono.", - "settings.voice.wakeWordPhrase": "Frase de activación", - "settings.voice.wakeWordPhraseHint": "La frase que dices para activar los comandos de voz sin manos.", - "settings.voice.pttHint": "Atajo Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Mantener para grabar.", - "settings.voice.privacy.revokeConsentHint": "Revoca tu consentimiento de Web Speech. La voz usará WASM local o se desactivará.", "help.advanced.adaptiveAi.content": "El motor de IA adaptativa detecta tu hardware en tiempo de ejecución —WebGPU, WebNN, DirectML, núcleos de CPU, nivel de VRAM y batería— y enruta la inferencia local al backend más rápido disponible. Actívalo en Ajustes → Funciones de acceso anticipado y consulta la lectura en vivo en Ajustes → IA. El modo Eco elige el modelo más pequeño viable para ahorrar batería y memoria, y una elección de pestaña líder garantiza que solo una pestaña del navegador use la GPU a la vez para evitar colisiones de VRAM.", "help.advanced.adaptiveAi.title": "IA adaptativa, GPU y modo Eco", "help.advanced.cloudSync.content": "La sincronización opcional cifrada de extremo a extremo mantiene tu biblioteca al día en todos tus dispositivos mediante un backend de Cloudflare R2. Activa «Sincronización en la nube» en Ajustes → Funciones de acceso anticipado y configúrala en Ajustes → Conexiones. Los datos del proyecto se cifran localmente con AES-256-GCM antes de subirse, y tus claves de API de IA nunca se envían a la nube. La sincronización es totalmente opcional; sin ella, StoryCraft sigue siendo una app totalmente «offline-first».", diff --git a/public/locales/fr/bundle.json b/public/locales/fr/bundle.json index c3cb47a1b..3f5257e7f 100644 --- a/public/locales/fr/bundle.json +++ b/public/locales/fr/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Examen de la structure de votre histoire…", "proforge.pipeline.noneActive": "Aucune pipeline active", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "mots écrits en sessions", "progress.allTime.title": "Total cumulé", "progress.bestDay.title": "Meilleur jour", @@ -1865,11 +1920,11 @@ "settings.privacy.encryptionUnlockButton": "Déverrouiller", "settings.privacy.encryptionWarning": "Si vous oubliez votre phrase secrète, vos données ne pourront pas être récupérées.", "settings.privacy.encryptionWrongPassphrase": "Phrase secrète incorrecte — déchiffrement échoué", + "settings.privacy.euDataResidency": "Résidence des données UE", + "settings.privacy.euDataResidencyHint": "Conserver toutes les données sur des serveurs de l'Union européenne si possible.", "settings.privacy.localStorageOnly": "Stockage local uniquement", "settings.privacy.migrationComplete": "Toutes les données ont été chiffrées avec succès", "settings.privacy.migrationProgress": "Chiffrement des données existantes…", - "settings.privacy.euDataResidency": "Résidence des données UE", - "settings.privacy.euDataResidencyHint": "Conserver toutes les données sur des serveurs de l'Union européenne si possible.", "settings.privacy.shareUsageData": "Partager les données d'utilisation", "settings.privacy.title": "Confidentialité et sécurité", "settings.projectAi.activeIndicator": "IA projet active", @@ -1946,6 +2001,9 @@ "settings.voice.downloadTtsModel": "Download TTS (Kokoro)", "settings.voice.enableHint": "Activate voice commands, dictation, and audio feedback.", "settings.voice.enableLabel": "Enable Voice Support", + "settings.voice.engine.auto": "Automatique (recommandé)", + "settings.voice.engine.wasm": "Modèle WASM local (hors ligne)", + "settings.voice.engine.webSpeech": "Web Speech API (cloud)", "settings.voice.feedbackLevel": "Audio Feedback Level", "settings.voice.intro": "Control StoryCraft Studio with your voice. All voice processing runs locally for maximum privacy.", "settings.voice.level.minimal": "Minimal — errors only", @@ -1960,12 +2018,24 @@ "settings.voice.privacy.consentDecline": "Refuser", "settings.voice.privacy.consentTitle": "Consentement Web Speech", "settings.voice.privacy.revokeConsent": "Révoquer le consentement", + "settings.voice.privacy.revokeConsentHint": "Révoque votre consentement Web Speech. La voix reviendra au WASM local ou sera désactivée.", "settings.voice.privacy.statusExternal": "Externe", "settings.voice.privacy.statusLocal": "Local", "settings.voice.privacyNotice": "Voice data is processed locally on your device. No audio is sent to the cloud unless you explicitly enable cloud fallback below.", + "settings.voice.pttHint": "Raccourci Push-to-Talk : Ctrl+Maj+V (Windows/Linux) ou ⌘+Maj+V (Mac). Maintenir pour enregistrer.", + "settings.voice.speechRate": "Vitesse d'élocution", + "settings.voice.speechRateHint": "Vitesse à laquelle la voix de synthèse vocale parle. 1,0 = vitesse normale.", + "settings.voice.speechVolume": "Volume de la parole", + "settings.voice.speechVolumeHint": "Volume de la voix de synthèse vocale. N'affecte pas l'entrée du microphone.", + "settings.voice.sttEngine": "Moteur de reconnaissance vocale (STT)", + "settings.voice.sttEngineHint": "Choisissez le moteur qui convertit votre parole en texte. 'Auto' choisit la meilleure option disponible.", "settings.voice.title": "Voice Full Support", + "settings.voice.ttsEngine": "Moteur de synthèse vocale (TTS)", + "settings.voice.ttsEngineHint": "Choisissez la voix qui lit les réponses à voix haute. 'Auto' choisit la meilleure option disponible.", "settings.voice.ttsMuted": "Mute All Speech Output", "settings.voice.ttsMutedHint": "Disable text-to-speech while keeping visual indicators.", + "settings.voice.wakeWordPhrase": "Phrase d'activation", + "settings.voice.wakeWordPhraseHint": "La phrase que vous dites pour activer les commandes vocales en mains libres.", "settings.voice.wasmModels": "Modèles vocaux WASM", "settings.voice.wasmModelsNotReady": "Modèles Whisper STT (~42 Mo) et Kokoro TTS (~15 Mo) non téléchargés", "settings.voice.wasmModelsReady": "Téléchargés et prêts pour le traitement vocal local", @@ -1975,21 +2045,6 @@ "voice.modelDownload.progress": "{{percent}}% terminé", "voice.modelDownload.retry": "Réessayer", "voice.modelDownload.title": "Téléchargement du modèle vocal", - "settings.voice.sttEngine": "Moteur de reconnaissance vocale (STT)", - "settings.voice.sttEngineHint": "Choisissez le moteur qui convertit votre parole en texte. 'Auto' choisit la meilleure option disponible.", - "settings.voice.ttsEngine": "Moteur de synthèse vocale (TTS)", - "settings.voice.ttsEngineHint": "Choisissez la voix qui lit les réponses à voix haute. 'Auto' choisit la meilleure option disponible.", - "settings.voice.engine.auto": "Automatique (recommandé)", - "settings.voice.engine.webSpeech": "Web Speech API (cloud)", - "settings.voice.engine.wasm": "Modèle WASM local (hors ligne)", - "settings.voice.speechRate": "Vitesse d'élocution", - "settings.voice.speechRateHint": "Vitesse à laquelle la voix de synthèse vocale parle. 1,0 = vitesse normale.", - "settings.voice.speechVolume": "Volume de la parole", - "settings.voice.speechVolumeHint": "Volume de la voix de synthèse vocale. N'affecte pas l'entrée du microphone.", - "settings.voice.wakeWordPhrase": "Phrase d'activation", - "settings.voice.wakeWordPhraseHint": "La phrase que vous dites pour activer les commandes vocales en mains libres.", - "settings.voice.pttHint": "Raccourci Push-to-Talk : Ctrl+Maj+V (Windows/Linux) ou ⌘+Maj+V (Mac). Maintenir pour enregistrer.", - "settings.voice.privacy.revokeConsentHint": "Révoque votre consentement Web Speech. La voix reviendra au WASM local ou sera désactivée.", "help.advanced.adaptiveAi.content": "Le moteur d’IA adaptatif détecte votre matériel à l’exécution — WebGPU, WebNN, DirectML, cœurs CPU, niveau de VRAM et batterie — et achemine l’inférence locale vers le backend le plus rapide disponible. Activez-le dans Paramètres → Fonctionnalités en accès anticipé et consultez l’affichage en direct dans Paramètres → IA. Le mode Éco choisit le plus petit modèle viable pour économiser batterie et mémoire, et une élection de l’onglet meneur garantit qu’un seul onglet du navigateur utilise le GPU à la fois pour éviter les collisions de VRAM.", "help.advanced.adaptiveAi.title": "IA adaptative, GPU et mode Éco", "help.advanced.cloudSync.content": "La synchronisation chiffrée de bout en bout, optionnelle, maintient votre bibliothèque à jour sur tous vos appareils via un backend Cloudflare R2. Activez « Synchronisation cloud » dans Paramètres → Fonctionnalités en accès anticipé et configurez-la dans Paramètres → Connexions. Les données du projet sont chiffrées localement en AES-256-GCM avant l’envoi, et vos clés d’API IA ne sont jamais transmises au cloud. La synchronisation est entièrement optionnelle ; sans elle, StoryCraft reste une application entièrement « offline-first ».", diff --git a/public/locales/he/bundle.json b/public/locales/he/bundle.json index 0b9efad7b..ef89a5509 100644 --- a/public/locales/he/bundle.json +++ b/public/locales/he/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "מתבונן במבנה הסיפור שלכם…", "proforge.pipeline.noneActive": "אין צינור פעיל", "proforge.pipeline.title": "צינור הסופר האולטימטיבי", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "מילים שנכתבו בהפעלות", "progress.allTime.title": "כל הזמן", "progress.bestDay.title": "היום הטוב ביותר", diff --git a/public/locales/it/bundle.json b/public/locales/it/bundle.json index 982073abe..3c9130e2d 100644 --- a/public/locales/it/bundle.json +++ b/public/locales/it/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Osservando la forma della tua storia…", "proforge.pipeline.noneActive": "Nessuna pipeline attiva", "proforge.pipeline.title": "Ultimate Author Pipeline", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "parole scritte nelle sessioni", "progress.allTime.title": "Totale storico", "progress.bestDay.title": "Giorno migliore", @@ -1865,11 +1920,11 @@ "settings.privacy.encryptionUnlockButton": "Sblocca", "settings.privacy.encryptionWarning": "Se dimentichi la passphrase, i tuoi dati non potranno essere recuperati.", "settings.privacy.encryptionWrongPassphrase": "Passphrase errata — decifratura non riuscita", + "settings.privacy.euDataResidency": "Residenza dati UE", + "settings.privacy.euDataResidencyHint": "Mantenere tutti i dati su server dell'Unione Europea ove possibile.", "settings.privacy.localStorageOnly": "Solo archiviazione locale", "settings.privacy.migrationComplete": "Tutti i dati cifrati con successo", "settings.privacy.migrationProgress": "Cifratura dei dati esistenti in corso…", - "settings.privacy.euDataResidency": "Residenza dati UE", - "settings.privacy.euDataResidencyHint": "Mantenere tutti i dati su server dell'Unione Europea ove possibile.", "settings.privacy.shareUsageData": "Condividi dati di utilizzo", "settings.privacy.title": "Privacy e sicurezza", "settings.projectAi.activeIndicator": "IA progetto attiva", @@ -1946,6 +2001,9 @@ "settings.voice.downloadTtsModel": "Download TTS (Kokoro)", "settings.voice.enableHint": "Attiva comandi vocali, dettatura e feedback audio.", "settings.voice.enableLabel": "Abilita controllo vocale", + "settings.voice.engine.auto": "Automatico (consigliato)", + "settings.voice.engine.wasm": "Modello WASM locale (offline)", + "settings.voice.engine.webSpeech": "Web Speech API (cloud)", "settings.voice.feedbackLevel": "Livello di feedback audio", "settings.voice.intro": "Controlla StoryCraft Studio con la tua voce. Tutto l’elaborazione vocale avviene localmente per la massima privacy.", "settings.voice.level.minimal": "Minimo — solo errori", @@ -1960,12 +2018,24 @@ "settings.voice.privacy.consentDecline": "Rifiuta", "settings.voice.privacy.consentTitle": "Consenso Web Speech", "settings.voice.privacy.revokeConsent": "Revoca il consenso", + "settings.voice.privacy.revokeConsentHint": "Revoca il tuo consenso Web Speech. La voce passerà al WASM locale o verrà disattivata.", "settings.voice.privacy.statusExternal": "Esterno", "settings.voice.privacy.statusLocal": "Locale", "settings.voice.privacyNotice": "I dati vocali vengono elaborati localmente sul tuo dispositivo. Nessun audio viene inviato al cloud a meno che tu non abiliti esplicitamente il fallback cloud.", + "settings.voice.pttHint": "Scorciatoia Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Tenere premuto per registrare.", + "settings.voice.speechRate": "Velocità del parlato", + "settings.voice.speechRateHint": "Quanto velocemente parla la voce di sintesi. 1.0 = velocità normale.", + "settings.voice.speechVolume": "Volume del parlato", + "settings.voice.speechVolumeHint": "Volume della voce di sintesi. Non influisce sull'ingresso del microfono.", + "settings.voice.sttEngine": "Motore di riconoscimento vocale (STT)", + "settings.voice.sttEngineHint": "Scegli il motore che converte il tuo parlato in testo. 'Auto' sceglie la migliore opzione disponibile.", "settings.voice.title": "Controllo vocale", + "settings.voice.ttsEngine": "Motore di sintesi vocale (TTS)", + "settings.voice.ttsEngineHint": "Scegli la voce che legge le risposte ad alta voce. 'Auto' sceglie la migliore opzione disponibile.", "settings.voice.ttsMuted": "Silenzia tutta l’uscita vocale", "settings.voice.ttsMutedHint": "Disabilita il text-to-speech mantenendo gli indicatori visivi.", + "settings.voice.wakeWordPhrase": "Frase di attivazione", + "settings.voice.wakeWordPhraseHint": "La frase che dici per attivare i comandi vocali a mani libere.", "settings.voice.wasmModels": "Modelli vocali WASM", "settings.voice.wasmModelsNotReady": "Modelli Whisper STT (~42 MB) e Kokoro TTS (~15 MB) non scaricati", "settings.voice.wasmModelsReady": "Scaricati e pronti per il riconoscimento vocale locale", @@ -1975,21 +2045,6 @@ "voice.modelDownload.progress": "{{percent}}% completato", "voice.modelDownload.retry": "Riprova", "voice.modelDownload.title": "Download modello vocale", - "settings.voice.sttEngine": "Motore di riconoscimento vocale (STT)", - "settings.voice.sttEngineHint": "Scegli il motore che converte il tuo parlato in testo. 'Auto' sceglie la migliore opzione disponibile.", - "settings.voice.ttsEngine": "Motore di sintesi vocale (TTS)", - "settings.voice.ttsEngineHint": "Scegli la voce che legge le risposte ad alta voce. 'Auto' sceglie la migliore opzione disponibile.", - "settings.voice.engine.auto": "Automatico (consigliato)", - "settings.voice.engine.webSpeech": "Web Speech API (cloud)", - "settings.voice.engine.wasm": "Modello WASM locale (offline)", - "settings.voice.speechRate": "Velocità del parlato", - "settings.voice.speechRateHint": "Quanto velocemente parla la voce di sintesi. 1.0 = velocità normale.", - "settings.voice.speechVolume": "Volume del parlato", - "settings.voice.speechVolumeHint": "Volume della voce di sintesi. Non influisce sull'ingresso del microfono.", - "settings.voice.wakeWordPhrase": "Frase di attivazione", - "settings.voice.wakeWordPhraseHint": "La frase che dici per attivare i comandi vocali a mani libere.", - "settings.voice.pttHint": "Scorciatoia Push-to-Talk: Ctrl+Shift+V (Windows/Linux) o ⌘+Shift+V (Mac). Tenere premuto per registrare.", - "settings.voice.privacy.revokeConsentHint": "Revoca il tuo consenso Web Speech. La voce passerà al WASM locale o verrà disattivata.", "help.advanced.adaptiveAi.content": "Il motore IA adattivo rileva l’hardware in fase di esecuzione — WebGPU, WebNN, DirectML, core CPU, livello VRAM e batteria — e instrada l’inferenza locale al backend più veloce disponibile. Attivalo in Impostazioni → Funzioni ad accesso anticipato e controlla la lettura in tempo reale in Impostazioni → IA. La modalità Eco sceglie il modello più piccolo utilizzabile per risparmiare batteria e memoria, e un’elezione della scheda capofila garantisce che solo una scheda del browser usi la GPU alla volta per evitare collisioni di VRAM.", "help.advanced.adaptiveAi.title": "IA adattiva, GPU e modalità Eco", "help.advanced.cloudSync.content": "La sincronizzazione opzionale crittografata end-to-end mantiene allineata la tua libreria su più dispositivi tramite un backend Cloudflare R2. Attiva «Sincronizzazione cloud» in Impostazioni → Funzioni ad accesso anticipato e configurala in Impostazioni → Connessioni. I dati del progetto vengono crittografati localmente con AES-256-GCM prima del caricamento e le tue chiavi API dell’IA non vengono mai inviate al cloud. La sincronizzazione è del tutto facoltativa; senza di essa StoryCraft resta un’app completamente offline-first.", diff --git a/public/locales/ja/bundle.json b/public/locales/ja/bundle.json index 0ae7b5ea3..74de4559d 100644 --- a/public/locales/ja/bundle.json +++ b/public/locales/ja/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "あなたの物語の形を見てみると…", "proforge.pipeline.noneActive": "アクティブなパイプラインがありません", "proforge.pipeline.title": "究極の著者パイプライン", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "セッションで書いた言葉", "progress.allTime.title": "オールタイム", "progress.bestDay.title": "最高の一日", diff --git a/public/locales/pt/bundle.json b/public/locales/pt/bundle.json index 1200bae9e..59adde272 100644 --- a/public/locales/pt/bundle.json +++ b/public/locales/pt/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "Olhando para a forma da sua história…", "proforge.pipeline.noneActive": "Nenhum pipeline ativo", "proforge.pipeline.title": "Pipeline final do autor", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "palavras escritas em sessões", "progress.allTime.title": "Todos os tempos", "progress.bestDay.title": "Melhor dia", diff --git a/public/locales/zh/bundle.json b/public/locales/zh/bundle.json index 15a219e1c..318a44f74 100644 --- a/public/locales/zh/bundle.json +++ b/public/locales/zh/bundle.json @@ -255,6 +255,61 @@ "proforge.loading.structural": "看看你的故事的形状……", "proforge.pipeline.noneActive": "无活动管道", "proforge.pipeline.title": "最终作者管道", + "proforge.review.accept": "Accept", + "proforge.review.acceptAll": "Accept All", + "proforge.review.acceptAllCritical": "Accept All Critical", + "proforge.review.announceSubmitted": "{{stage}} review submitted", + "proforge.review.confidence": "{{percent}}% confidence", + "proforge.review.counts": "{{pending}} pending · {{accepted}} accepted · {{rejected}} rejected", + "proforge.review.criticalHeadingOne": "{{count}} critical issue needs attention", + "proforge.review.criticalHeadingOther": "{{count}} critical issues need attention", + "proforge.review.filter.accepted": "Accepted", + "proforge.review.filter.all": "All", + "proforge.review.filter.pending": "Pending", + "proforge.review.filter.rejected": "Rejected", + "proforge.review.group.critical": "Critical Actions", + "proforge.review.group.info": "Suggestions", + "proforge.review.group.warning": "Warnings", + "proforge.review.heading": "Review: {{stage}}", + "proforge.review.ignore": "Ignore", + "proforge.review.moreCritical": "+{{count}} more critical issues below", + "proforge.review.noItems": "No review items available.", + "proforge.review.noItemsForFilter": "No items match this filter.", + "proforge.review.original": "Original:", + "proforge.review.pendingCount": "{{count}} Pending", + "proforge.review.proposed": "Proposed:", + "proforge.review.quickAccept": "Quick Accept ({{count}})", + "proforge.review.quickAcceptTitle": "Accept all high-confidence non-critical suggestions", + "proforge.review.reject": "Reject", + "proforge.review.rejectAll": "Reject All", + "proforge.review.severity.critical": "Critical", + "proforge.review.severity.info": "Info", + "proforge.review.severity.warning": "Warning", + "proforge.review.showDetails": "Show details", + "proforge.review.showLess": "Show less", + "proforge.review.skipStage": "Skip Stage", + "proforge.review.submitContinue": "Submit & Continue", + "proforge.review.type.arcIssue": "Arc", + "proforge.review.type.consistencyIssue": "Consistency", + "proforge.review.type.grammarEdit": "Grammar", + "proforge.review.type.legalWarning": "Legal", + "proforge.review.type.pacingIssue": "Pacing", + "proforge.review.type.plotHole": "Plot Hole", + "proforge.review.type.proseEdit": "Prose", + "proforge.review.type.repetitionHit": "Repetition", + "proforge.review.type.structuralEdit": "Structural", + "proforge.review.type.styleEdit": "Style", + "proforge.review.type.technicalIssue": "Technical", + "proforge.stageName.analytics": "Analytics", + "proforge.stageName.archived": "Archived", + "proforge.stageName.copyEdit": "Copy Edit", + "proforge.stageName.idle": "Idle", + "proforge.stageName.intake": "Intake & Diagnostic", + "proforge.stageName.lineProse": "Line & Prose", + "proforge.stageName.production": "Production", + "proforge.stageName.proof": "Proofreading", + "proforge.stageName.publishing": "Publishing", + "proforge.stageName.structural": "Structural Edit", "progress.allTime.label": "会话中写下的单词", "progress.allTime.title": "所有时间", "progress.bestDay.title": "最好的一天", diff --git a/services/proForge/applyReviewEdits.ts b/services/proForge/applyReviewEdits.ts new file mode 100644 index 000000000..545dbeaf6 --- /dev/null +++ b/services/proForge/applyReviewEdits.ts @@ -0,0 +1,133 @@ +/** + * ProForge — apply accepted review edits back into manuscript text. + * QNBS-v3: Closes the core gap where "accept" recorded a status but never changed the + * manuscript. Offset-safe (back-to-front), with a text-match fallback for stale offsets, + * so an AI that returned slightly-off ranges still anchors correctly or is skipped — never + * corrupts the section. Applied edits are natively undoable (project slice is redux-undo wrapped). + */ + +import type { ReviewItem } from '../../features/proForge/types'; + +export interface SectionContentUpdate { + id: string; + content: string; +} + +export interface ApplyEditsResult { + /** New content for the section after all applicable edits. */ + content: string; + /** Number of edits successfully applied. */ + applied: number; + /** Number of edits that could not be anchored (stale/overlapping) and were skipped. */ + skipped: number; +} + +interface PlannedEdit { + start: number; + end: number; + proposed: string; +} + +function isValidRange(range: { start: number; end: number }, len: number): boolean { + return ( + Number.isFinite(range.start) && + Number.isFinite(range.end) && + range.start >= 0 && + range.end >= range.start && + range.end <= len + ); +} + +/** + * Apply the accepted `ReviewItem`s that target a single section's content. + * Only items carrying a `proposed` replacement are considered text edits; advisory items + * (no `proposed`) are ignored and not counted as skipped. + */ +export function applyReviewEditsToSection(content: string, items: ReviewItem[]): ApplyEditsResult { + let skipped = 0; + const planned: PlannedEdit[] = []; + + for (const item of items) { + // Advisory-only item (pacing note, quality score, plot-hole hint) — nothing to apply. + if (item.proposed === undefined) continue; + const proposed = item.proposed; + + // Strategy 1: trust offsets only if they still resolve to the expected original text + // (or no original was provided to verify against). + if (item.range && isValidRange(item.range, content.length)) { + const slice = content.slice(item.range.start, item.range.end); + if (item.original === undefined || slice === item.original) { + planned.push({ start: item.range.start, end: item.range.end, proposed }); + continue; + } + } + + // Strategy 2: offsets missing or stale — locate the original text directly. + if (item.original) { + const idx = content.indexOf(item.original); + if (idx !== -1) { + planned.push({ start: idx, end: idx + item.original.length, proposed }); + continue; + } + } + + // Could not anchor the edit — skip rather than guess. + skipped++; + } + + // Apply back-to-front so earlier edits never invalidate the offsets of later (already-applied) ones. + planned.sort((a, b) => b.start - a.start); + let next = content; + let lowerBound = content.length + 1; // start of the most recently applied (right-most) edit + let applied = 0; + for (const edit of planned) { + // Drop edits that overlap one already applied to its right. + if (edit.end > lowerBound) { + skipped++; + continue; + } + next = next.slice(0, edit.start) + edit.proposed + next.slice(edit.end); + lowerBound = edit.start; + applied++; + } + + return { content: next, applied, skipped }; +} + +/** + * Plan content updates for a whole manuscript from a flat list of accepted review items. + * Returns only the sections whose content actually changed, plus aggregate counts. + */ +export function planAcceptedManuscriptEdits( + manuscript: ReadonlyArray<{ id: string; content?: string }>, + acceptedItems: ReadonlyArray, +): { updates: SectionContentUpdate[]; applied: number; skipped: number } { + const bySection = new Map(); + for (const item of acceptedItems) { + if (!item.sectionId || item.proposed === undefined) continue; + const list = bySection.get(item.sectionId); + if (list) list.push(item); + else bySection.set(item.sectionId, [item]); + } + + const updates: SectionContentUpdate[] = []; + let applied = 0; + let skipped = 0; + + for (const [sectionId, items] of bySection) { + const section = manuscript.find((s) => s.id === sectionId); + if (!section) { + // Section was deleted between analysis and acceptance. + skipped += items.length; + continue; + } + const result = applyReviewEditsToSection(section.content ?? '', items); + applied += result.applied; + skipped += result.skipped; + if (result.content !== (section.content ?? '')) { + updates.push({ id: sectionId, content: result.content }); + } + } + + return { updates, applied, skipped }; +} diff --git a/services/proForge/pipelineAgents/baseAgent.ts b/services/proForge/pipelineAgents/baseAgent.ts index 8334c0ff4..b30589d62 100644 --- a/services/proForge/pipelineAgents/baseAgent.ts +++ b/services/proForge/pipelineAgents/baseAgent.ts @@ -15,12 +15,20 @@ export abstract class BaseAgent { protected readonly context: OrchestratorContext; // QNBS-v3: Gateway injected from context or falls back to module singleton — keeps agents testable. protected readonly gateway: InferenceGateway; + // QNBS-v3: Supervisor feedback from the previous failed attempt; prepended to the next prompt + // so a retry is materially different instead of re-rolling the identical request. + private retryFeedback = ''; constructor(context: OrchestratorContext) { this.context = context; this.gateway = context.gateway ?? inferenceGateway; } + /** Orchestrator-only: seed corrective feedback for a retry attempt. */ + setRetryFeedback(feedback: string): void { + this.retryFeedback = feedback; + } + abstract execute( signal: AbortSignal, ): Promise>; @@ -44,13 +52,25 @@ export abstract class BaseAgent { protected async generate(prompt: string, maxTokens?: number): Promise { // QNBS-v3: exactOptionalPropertyTypes — only pass maxTokens when it's defined. const result = await this.gateway.generate({ - prompt, + prompt: this.withRetryPreamble(prompt), creativity: this.context.config.creativity, options: this.buildAiOpts(maxTokens !== undefined ? { maxTokens } : undefined), }); return result.text; } + // QNBS-v3: Prepend corrective guidance from the prior failed attempt so the model addresses + // the supervisor's concerns rather than repeating the same output. + private withRetryPreamble(prompt: string): string { + if (!this.retryFeedback) return prompt; + return `IMPORTANT — your previous attempt was rejected by the quality reviewer for these reasons: +${this.retryFeedback} + +Produce a corrected response that resolves the issues above. Do not repeat the prior output. + +${prompt}`; + } + // QNBS-v3: Builds AIRequestOptions from context.config — provider/model defaulting for pipeline agents. protected buildAiOpts(overrides?: { maxTokens?: number; diff --git a/services/proForge/pipelineAgents/styleAdaptedWriterAgent.ts b/services/proForge/pipelineAgents/styleAdaptedWriterAgent.ts deleted file mode 100644 index 2b2bcd33d..000000000 --- a/services/proForge/pipelineAgents/styleAdaptedWriterAgent.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * StyleAdaptedWriterAgent — Optional ProForge agent using an active LoRA adapter. - * QNBS-v3: Extends BaseAgent; inserts after 'lineProse' via PipelineStageExtension. - * Produces proseEdit review items generated by the user's personal style model. - * Returns a fallback (isFallback: true) when no adapter is active. - */ - -import { v4 as uuid } from 'uuid'; -import type { ReviewItem, StageResult } from '../../../features/proForge/types'; -import { generateLocalText } from '../../localAiFacade'; -import { logger } from '../../logger'; -import { getActiveAdapter } from '../../loraAdapterService'; -import { BaseAgent } from './baseAgent'; - -export class StyleAdaptedWriterAgent extends BaseAgent { - async execute( - signal: AbortSignal, - ): Promise> { - const startTime = performance.now(); - - // Resolve active adapter from IDB (not Redux — agents run outside React lifecycle) - const activeAdapter = await getActiveAdapter(); - - if (!activeAdapter) { - logger.info('StyleAdaptedWriterAgent: no active LoRA adapter — returning fallback'); - return { - reviewItems: [], - metrics: { - aiCalls: 0, - tokensConsumed: 0, - durationMs: this.elapsed(startTime), - itemsFound: 0, - itemsAccepted: 0, - itemsRejected: 0, - }, - agentOutput: { isFallback: true, reason: 'no_active_adapter' }, - }; - } - - if (signal.aborted) { - return { - reviewItems: [], - metrics: { - aiCalls: 0, - tokensConsumed: 0, - durationMs: 0, - itemsFound: 0, - itemsAccepted: 0, - itemsRejected: 0, - }, - agentOutput: { isFallback: true, reason: 'aborted' }, - }; - } - - const project = this.requireProject(); - const sections = project.manuscript ?? []; - const reviewItems: ReviewItem[] = []; - let aiCalls = 0; - let tokensConsumed = 0; - - for (const section of sections.slice(0, 3)) { - if (signal.aborted) break; - const text = section.content ?? ''; - if (text.length < 200) continue; - - const prompt = `You are a style-adapted writing assistant trained on this author's voice. Suggest one improvement to this passage that preserves the author's style while enhancing prose quality:\n\n${text.slice(0, 800)}`; - - try { - const response = await generateLocalText( - prompt, - activeAdapter.modelCompatibility, - undefined, - activeAdapter.id, - ); - const suggestion = response?.text ?? ''; - if (suggestion.length > 50) { - reviewItems.push({ - id: uuid(), - stage: 'lineProse' as const, - type: 'proseEdit', - severity: 'info', - status: 'pending', - sectionId: section.id, - description: `Style-adapted suggestion from ${activeAdapter.name}`, - original: text.slice(0, 200), - proposed: suggestion, - rationale: `Adapter: ${activeAdapter.name}`, - confidence: 0.7, - createdAt: new Date().toISOString(), - }); - aiCalls++; - tokensConsumed += suggestion.length; - } - } catch (err) { - logger.warn('StyleAdaptedWriterAgent: generation failed for section', { - sectionId: section.id, - err, - }); - } - } - - return { - reviewItems, - metrics: { - aiCalls, - tokensConsumed, - durationMs: this.elapsed(startTime), - itemsFound: reviewItems.length, - itemsAccepted: 0, - itemsRejected: 0, - }, - agentOutput: { - adapterId: activeAdapter.id, - adapterName: activeAdapter.name, - suggestionsGenerated: reviewItems.length, - }, - }; - } -} diff --git a/services/proForge/pipelineAgents/supervisorAgent.ts b/services/proForge/pipelineAgents/supervisorAgent.ts index 412284514..b2a5def5c 100644 --- a/services/proForge/pipelineAgents/supervisorAgent.ts +++ b/services/proForge/pipelineAgents/supervisorAgent.ts @@ -5,8 +5,12 @@ */ import type { + CopyEditPlan, DiagnosticReport, + PipelineAnalyticsReport, PipelineStage, + ProductionManifest, + PublishingPackage, QualityGateReport, StageResult, StructuralEditPlan, @@ -30,8 +34,18 @@ export class SupervisorAgent { return this.evaluateIntake(result); case 'structural': return this.evaluateStructural(result); + case 'lineProse': + return this.evaluateLineProse(result); + case 'copyEdit': + return this.evaluateCopyEdit(result); case 'proof': return this.evaluateProof(result); + case 'production': + return this.evaluateProduction(result); + case 'publishing': + return this.evaluatePublishing(result); + case 'analytics': + return this.evaluateAnalytics(result); default: return { pass: true, retryRecommended: false, qualityScore: 100, reasons: [] }; } @@ -43,12 +57,9 @@ export class SupervisorAgent { const output = result.agentOutput as DiagnosticReport | undefined; const reasons: string[] = []; - // QNBS-v3: qualityScore.overall===50 across all 6 dimensions is the fallback sentinel. - const isFallback = - output?.qualityScore !== undefined && - output.qualityScore.overall === 50 && - output.qualityScore.prose === 50 && - output.qualityScore.structure === 50; + // QNBS-v3: Agents mark synthetic reports with isFallback:true + zeroed scores + // (see createFallbackReport). The previous `=== 50` heuristic never matched. + const isFallback = output?.isFallback === true; if (isFallback) { reasons.push( @@ -77,6 +88,12 @@ export class SupervisorAgent { const output = result.agentOutput as StructuralEditPlan | undefined; const reasons: string[] = []; + // QNBS-v3: Honour the explicit fallback marker before heuristics. + if (output?.isFallback === true) { + reasons.push('Structural analysis returned a fallback plan — the AI call did not complete.'); + return { pass: false, retryRecommended: true, qualityScore: 0, reasons }; + } + const wordCount = this.estimateManuscriptWordCount(); const hasEdits = (output?.edits?.length ?? 0) > 0; const hasReviewItems = result.reviewItems.length > 0; @@ -98,6 +115,12 @@ export class SupervisorAgent { const output = result.agentOutput as QualityGateReport | undefined; const reasons: string[] = []; + // QNBS-v3: Honour the explicit fallback marker before heuristics. + if (output?.isFallback === true) { + reasons.push('Proof/quality gate returned a fallback report — the AI call did not complete.'); + return { pass: false, retryRecommended: true, qualityScore: 0, reasons }; + } + const wordCount = this.estimateManuscriptWordCount(); const seemsFallback = output?.overallPass === true && @@ -114,6 +137,103 @@ export class SupervisorAgent { return { pass: true, retryRecommended: false, qualityScore: 90, reasons }; } + private evaluateLineProse( + result: Pick, + ): SupervisionDecision { + const output = result.agentOutput as { edits?: unknown[] } | undefined; + const wordCount = this.estimateManuscriptWordCount(); + const hasEdits = (output?.edits?.length ?? 0) > 0; + + // QNBS-v3: A substantial manuscript with zero prose edits suggests the AI call didn't land. + if (!hasEdits && result.reviewItems.length === 0 && wordCount > 1000) { + return { + pass: false, + retryRecommended: true, + qualityScore: 45, + reasons: [ + `No prose edits for a ${wordCount}-word manuscript — verify the AI provider responded.`, + ], + }; + } + return { pass: true, retryRecommended: false, qualityScore: 85, reasons: [] }; + } + + private evaluateCopyEdit( + result: Pick, + ): SupervisionDecision { + const output = result.agentOutput as CopyEditPlan | undefined; + const total = + (output?.grammarEdits?.length ?? 0) + + (output?.styleEdits?.length ?? 0) + + (output?.repetitionHits?.length ?? 0) + + (output?.formatIssues?.length ?? 0); + const wordCount = this.estimateManuscriptWordCount(); + + // QNBS-v3: Zero grammar/style/repetition/format findings on a long manuscript is suspicious. + if (total === 0 && result.reviewItems.length === 0 && wordCount > 1500) { + return { + pass: false, + retryRecommended: true, + qualityScore: 50, + reasons: [ + 'Copy-edit found zero grammar/style/repetition issues on a long manuscript — verify the AI ran.', + ], + }; + } + return { pass: true, retryRecommended: false, qualityScore: 88, reasons: [] }; + } + + private evaluateProduction( + result: Pick, + ): SupervisionDecision { + const output = result.agentOutput as ProductionManifest | undefined; + const artifactCount = output?.artifacts?.length ?? 0; + + // QNBS-v3: Production must emit at least one artifact; none means the build failed. + if (artifactCount === 0) { + return { + pass: false, + retryRecommended: true, + qualityScore: 0, + reasons: ['Production produced no export artifacts.'], + }; + } + return { pass: true, retryRecommended: false, qualityScore: 95, reasons: [] }; + } + + private evaluatePublishing( + result: Pick, + ): SupervisionDecision { + const output = result.agentOutput as PublishingPackage | undefined; + const reasons: string[] = []; + const hasTitle = (output?.metadata?.title ?? '').trim().length > 0; + const hasBlurb = (output?.blurbs?.backCover ?? '').trim().length > 0; + + if (!hasTitle) reasons.push('Publishing package is missing book-title metadata.'); + if (!hasBlurb) reasons.push('Publishing package is missing a back-cover blurb.'); + if (reasons.length > 0) { + return { pass: false, retryRecommended: true, qualityScore: 40, reasons }; + } + return { pass: true, retryRecommended: false, qualityScore: 90, reasons: [] }; + } + + private evaluateAnalytics( + result: Pick, + ): SupervisionDecision { + const output = result.agentOutput as PipelineAnalyticsReport | undefined; + // QNBS-v3: Analytics is the terminal, informational stage — never block the pipeline on it, + // but flag a missing metrics block so the trace records it. + if (!output?.metrics) { + return { + pass: true, + retryRecommended: false, + qualityScore: 70, + reasons: ['Analytics report has no metrics block (non-blocking).'], + }; + } + return { pass: true, retryRecommended: false, qualityScore: 100, reasons: [] }; + } + private estimateManuscriptWordCount(): number { const project = this.context.getState().project.present?.data; if (!project) return 0; diff --git a/services/proForge/pipelineTools/toolRegistry.ts b/services/proForge/pipelineTools/toolRegistry.ts deleted file mode 100644 index fcff2fd9d..000000000 --- a/services/proForge/pipelineTools/toolRegistry.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * ProForge Tool Registry — Tool-calling infrastructure for agentic AI. - * QNBS-v3: Vercel AI SDK compatible tool schemas with sandboxed execution. - */ - -import { z } from 'zod'; -import { logger } from '../../logger'; - -// --------------------------------------------------------------------------- -// Tool Schema Types -// --------------------------------------------------------------------------- - -export interface ToolDefinition { - name: string; - description: string; - parameters: z.ZodType; - handler: (args: unknown, context: ToolContext) => Promise; - /** Which pipeline stages this tool is available in */ - stages: string[]; - /** Whether this tool modifies project data */ - isWrite: boolean; -} - -export interface ToolContext { - projectId: string; - dispatch: import('../../../app/store').AppDispatch; - getState: () => import('../../../app/store').RootState; - memoryBank: import('../proForgeMemoryBank').ProForgeMemoryBank; - signal: AbortSignal; -} - -// --------------------------------------------------------------------------- -// Registry -// --------------------------------------------------------------------------- - -class ToolRegistry { - private tools = new Map(); - - register(tool: ToolDefinition): void { - if (this.tools.has(tool.name)) { - logger.warn(`ToolRegistry: Overwriting existing tool "${tool.name}"`); - } - this.tools.set(tool.name, tool); - } - - unregister(name: string): boolean { - return this.tools.delete(name); - } - - get(name: string): ToolDefinition | undefined { - return this.tools.get(name); - } - - list(): ToolDefinition[] { - return Array.from(this.tools.values()); - } - - listForStage(stage: string): ToolDefinition[] { - return this.list().filter((t) => t.stages.includes(stage) || t.stages.includes('*')); - } - - async execute(name: string, args: unknown, context: ToolContext): Promise { - const tool = this.tools.get(name); - if (!tool) { - throw new Error(`Tool "${name}" not found in registry`); - } - // Validate args - const parsed = tool.parameters.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for tool "${name}": ${parsed.error.message}`); - } - return tool.handler(parsed.data, context); - } - - /** Convert to Vercel AI SDK tool format */ - toAiSdkTools( - stage: string, - ): Record }> { - const result: Record }> = {}; - for (const tool of this.listForStage(stage)) { - result[tool.name] = { - description: tool.description, - parameters: tool.parameters, - }; - } - return result; - } -} - -export const toolRegistry = new ToolRegistry(); - -// --------------------------------------------------------------------------- -// Built-in Tools -// --------------------------------------------------------------------------- - -// -- Manuscript Tools -- - -toolRegistry.register({ - name: 'readSection', - description: 'Read the full content of a manuscript section by ID.', - parameters: z.object({ - sectionId: z.string().describe('The section ID to read'), - }), - stages: ['*'], - isWrite: false, - handler: async (args, context) => { - const { sectionId } = args as { sectionId: string }; - const state = context.getState(); - const sections = state.project.present?.data?.manuscript ?? []; - const section = sections.find((s) => s.id === sectionId); - if (!section) { - return { error: `Section "${sectionId}" not found` }; - } - return { - id: section.id, - title: section.title, - content: section.content, - status: section.status, - }; - }, -}); - -toolRegistry.register({ - name: 'readAllSections', - description: - 'Read all manuscript sections (metadata only, no content). Use readSection for full content.', - parameters: z.object({ - includeContent: z - .boolean() - .optional() - .describe('If true, includes full content (warning: large payload)'), - }), - stages: ['*'], - isWrite: false, - handler: async (args, context) => { - const { includeContent } = (args as { includeContent?: boolean }) ?? {}; - const state = context.getState(); - const sections = state.project.present?.data?.manuscript ?? []; - return sections.map((s) => ({ - id: s.id, - title: s.title, - wordCount: s.content ? s.content.trim().split(/\s+/).length : 0, - status: s.status, - act: s.act, - ...(includeContent ? { content: s.content } : {}), - })); - }, -}); - -toolRegistry.register({ - name: 'readProjectMeta', - description: 'Read project metadata: title, logline, characters, worlds.', - parameters: z.object({}), - stages: ['*'], - isWrite: false, - handler: async (_args, context) => { - const state = context.getState(); - const data = state.project.present?.data; - if (!data) return { error: 'No project data' }; - return { - title: data.title, - logline: data.logline, - characters: Object.values(data.characters?.entities ?? {}), - worlds: Object.values(data.worlds?.entities ?? {}), - }; - }, -}); - -toolRegistry.register({ - name: 'searchLore', - description: 'Search the story lore (characters, worlds, codex) for a query.', - parameters: z.object({ - query: z.string().describe('Search query'), - limit: z.number().optional().describe('Max results'), - }), - stages: ['*'], - isWrite: false, - handler: async (args, context) => { - const { query, limit = 5 } = args as { query: string; limit?: number }; - const entries = await context.memoryBank.search(query, limit); - return entries.map((e) => ({ - category: e.category, - key: e.key, - content: e.content, - })); - }, -}); - -// -- Analysis Tools -- - -toolRegistry.register({ - name: 'analyzePacing', - description: 'Analyze manuscript pacing per section. Returns tension scores and recommendations.', - parameters: z.object({}), - stages: ['intake', 'structural'], - isWrite: false, - handler: async (_args, context) => { - const state = context.getState(); - const sections = state.project.present?.data?.manuscript ?? []; - // Simple heuristic: longer sections = slower pacing, shorter = faster - const wordCounts = sections.map((s) => s.content?.trim().split(/\s+/).length ?? 0); - const avg = wordCounts.reduce((a, b) => a + b, 0) / (wordCounts.length || 1); - const analysis = sections.map((s, i) => { - const wc = wordCounts[i] ?? 0; - const ratio = avg > 0 ? wc / avg : 1; - let tensionScore = 5; - if (ratio > 1.5) tensionScore = 3; // slow - if (ratio < 0.7) tensionScore = 7; // fast - return { - sectionId: s.id, - title: s.title, - wordCount: wc, - tensionScore, - recommendedAction: ratio > 1.5 ? 'compress' : ratio < 0.7 ? 'expand' : 'keep', - }; - }); - return { sections: analysis, overallPacing: 'uneven' }; - }, -}); - -toolRegistry.register({ - name: 'countWords', - description: 'Count total words in the manuscript.', - parameters: z.object({}), - stages: ['*'], - isWrite: false, - handler: async (_args, context) => { - const state = context.getState(); - const sections = state.project.present?.data?.manuscript ?? []; - const counts = sections.map((s) => s.content?.trim().split(/\s+/).length ?? 0); - const total = counts.reduce((a, b) => a + b, 0); - return { total, perSection: counts }; - }, -}); - -// -- Report Tools -- - -toolRegistry.register({ - name: 'generateReport', - description: 'Save a structured report to the memory bank for later retrieval.', - parameters: z.object({ - category: z.enum(['lore', 'character', 'style', 'feedback', 'edit', 'meta']), - key: z.string(), - content: z.string(), - }), - stages: ['*'], - isWrite: true, - handler: async (args, context) => { - const { category, key, content } = args as { - category: import('../../../features/proForge/types').MemoryBankEntry['category']; - key: string; - content: string; - }; - const run = context.getState().proForge.currentRun; - const stage = run?.activeStage ?? 'intake'; - await context.memoryBank.remember(category, key, content, stage); - return { success: true }; - }, -}); - -toolRegistry.register({ - name: 'getMemoryContext', - description: 'Retrieve relevant memory context for the current stage.', - parameters: z.object({ - query: z.string().optional(), - maxChars: z.number().optional(), - }), - stages: ['*'], - isWrite: false, - handler: async (args, context) => { - const { query, maxChars = 4000 } = args as { query?: string; maxChars?: number }; - const run = context.getState().proForge.currentRun; - const stage = run?.activeStage ?? 'intake'; - const contextStr = await context.memoryBank.buildContextString(stage, query, maxChars); - return { context: contextStr }; - }, -}); - -// -- Edit Proposal Tools (write operations gated) -- - -toolRegistry.register({ - name: 'proposeEdit', - description: - 'Propose an edit to a manuscript section. The edit is NOT applied automatically; it goes to the review queue.', - parameters: z.object({ - sectionId: z.string(), - startOffset: z.number(), - endOffset: z.number(), - original: z.string(), - proposed: z.string(), - rationale: z.string(), - category: z.string(), - confidence: z.number().min(0).max(1), - }), - stages: ['structural', 'lineProse', 'copyEdit'], - isWrite: true, - handler: async (args) => { - // Returns the proposal for the agent to collect into review items - return { proposal: args, status: 'queued_for_review' }; - }, -}); - -export { ToolRegistry }; diff --git a/services/proForge/proForgeHistoryStore.ts b/services/proForge/proForgeHistoryStore.ts new file mode 100644 index 000000000..abc13d057 --- /dev/null +++ b/services/proForge/proForgeHistoryStore.ts @@ -0,0 +1,65 @@ +/** + * ProForge Run-History Store — persists completed/aborted pipeline runs per project. + * QNBS-v3: The proForge slice is ephemeral (cleared on reload), so analytics comparisons across + * runs were always empty after a refresh. This IDB-backed store survives reloads. Capped per + * project to keep storage bounded. + */ + +import type { PipelineRun } from '../../features/proForge/types'; + +const HISTORY_DB = 'proforge-run-history'; +const HISTORY_VERSION = 1; +const STORE = 'history'; +/** Keep at most this many runs per project (most recent first). */ +export const MAX_RUN_HISTORY = 20; + +let dbPromise: Promise | null = null; + +function openHistoryDb(): Promise { + if (dbPromise) return dbPromise; + dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open(HISTORY_DB, HISTORY_VERSION); + request.onerror = () => reject(new Error('Failed to open ProForge history DB')); + request.onsuccess = () => resolve(request.result); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE)) { + db.createObjectStore(STORE, { keyPath: 'projectId' }); + } + }; + }); + return dbPromise; +} + +interface HistoryRecord { + projectId: string; + runs: PipelineRun[]; +} + +/** Persist the run history for a project (capped to MAX_RUN_HISTORY, most-recent-first). */ +export async function saveRunHistory(projectId: string, runs: PipelineRun[]): Promise { + const db = await openHistoryDb(); + const record: HistoryRecord = { projectId, runs: runs.slice(0, MAX_RUN_HISTORY) }; + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readwrite'); + const request = tx.objectStore(STORE).put(record); + request.onsuccess = () => resolve(); + request.onerror = () => reject(new Error('Failed to save ProForge run history')); + }); +} + +/** Load the persisted run history for a project (empty array if none). */ +export async function loadRunHistory(projectId: string): Promise { + const db = await openHistoryDb(); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readonly'); + const request = tx.objectStore(STORE).get(projectId); + request.onsuccess = () => resolve((request.result as HistoryRecord | undefined)?.runs ?? []); + request.onerror = () => reject(new Error('Failed to load ProForge run history')); + }); +} + +/** Reset the DB connection — test-only. */ +export function _resetHistoryDbForTest(): void { + dbPromise = null; +} diff --git a/services/proForge/proForgeMemoryBank.ts b/services/proForge/proForgeMemoryBank.ts index 0e1cfe988..d6c276644 100644 --- a/services/proForge/proForgeMemoryBank.ts +++ b/services/proForge/proForgeMemoryBank.ts @@ -1,6 +1,7 @@ /** * ProForge Memory Bank — Persistent project-specific memory for agentic context. - * QNBS-v3: IndexedDB-backed storage with hybrid RAG retrieval for agents. + * QNBS-v3: IndexedDB-backed storage. Retrieval honours the run's ragMode — keyword by default, + * or semantic/hybrid via the local MiniLM embedding service (best-effort, keyword fallback). */ import type { MemoryBankEntry, PipelineStage } from '../../features/proForge/types'; @@ -88,29 +89,72 @@ export async function getMemoryEntries( }); } +export type MemoryRagMode = 'lexical' | 'semantic' | 'hybrid'; + +function keywordScorer(query: string): (entry: MemoryBankEntry) => number { + const keywords = query + .toLowerCase() + .split(/\s+/) + .filter((k) => k.length > 2); + return (entry) => { + const text = `${entry.key} ${entry.content}`.toLowerCase(); + let score = 0; + for (const kw of keywords) if (text.includes(kw)) score += 1; + return score; + }; +} + +function rankByKeyword( + entries: MemoryBankEntry[], + query: string, + limit: number, +): MemoryBankEntry[] { + const score = keywordScorer(query); + return entries + .map((entry) => ({ entry, score: score(entry) })) + .filter((s) => s.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map((s) => s.entry); +} + export async function searchMemoryEntries( projectId: string, query: string, limit = 10, + mode: MemoryRagMode = 'lexical', ): Promise { const entries = await getMemoryEntries(projectId); - // Simple keyword scoring for now; can be upgraded to semantic search - const queryLower = query.toLowerCase(); - const scored = entries - .map((entry) => { - const text = `${entry.key} ${entry.content}`.toLowerCase(); - let score = 0; - const keywords = queryLower.split(/\s+/).filter((k) => k.length > 2); - for (const kw of keywords) { - if (text.includes(kw)) score += 1; - } - return { entry, score }; - }) - .filter((s) => s.score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, limit); + if (entries.length === 0) return []; - return scored.map((s) => s.entry); + if (mode === 'lexical') { + return rankByKeyword(entries, query, limit); + } + + // QNBS-v3: Semantic/hybrid recall — best-effort. The embedding service is dynamically imported + // so the memory bank stays light in node tests, and ANY failure (no model, offline) falls back + // to keyword ranking rather than breaking the pipeline. + try { + const { embedText, cosineSimilarity } = await import('../ai/localEmbeddingService'); + const scoreKeyword = keywordScorer(query); + const maxKw = Math.max(1, ...entries.map(scoreKeyword)); + const qVec = await embedText(query); + const scored = await Promise.all( + entries.map(async (entry) => { + const eVec = entry.embedding ?? (await embedText(`${entry.key} ${entry.content}`)); + const sim = cosineSimilarity(qVec, eVec); + // QNBS-v3: hybrid blends semantic (0.7) with normalised keyword overlap (0.3). + const score = mode === 'semantic' ? sim : 0.7 * sim + 0.3 * (scoreKeyword(entry) / maxKw); + return { entry, score }; + }), + ); + return scored + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map((s) => s.entry); + } catch { + return rankByKeyword(entries, query, limit); + } } export async function deleteMemoryEntry(id: string): Promise { @@ -159,8 +203,12 @@ export class ProForgeMemoryBank { return getMemoryEntries(this.projectId, category); } - async search(query: string, limit = 10): Promise { - return searchMemoryEntries(this.projectId, query, limit); + async search( + query: string, + limit = 10, + mode: MemoryRagMode = 'lexical', + ): Promise { + return searchMemoryEntries(this.projectId, query, limit, mode); } async recallForStage(stage: PipelineStage): Promise { @@ -192,10 +240,15 @@ export class ProForgeMemoryBank { /** * Build a context string for prompt injection from relevant memory entries. */ - async buildContextString(stage: PipelineStage, query?: string, maxChars = 4000): Promise { + async buildContextString( + stage: PipelineStage, + query?: string, + maxChars = 4000, + mode: MemoryRagMode = 'lexical', + ): Promise { let entries: MemoryBankEntry[]; if (query) { - entries = await this.search(query, 20); + entries = await this.search(query, 20, mode); } else { entries = await this.recallForStage(stage); } diff --git a/services/proForge/proForgeOrchestrator.ts b/services/proForge/proForgeOrchestrator.ts index 02a3cc3fe..2985e648c 100644 --- a/services/proForge/proForgeOrchestrator.ts +++ b/services/proForge/proForgeOrchestrator.ts @@ -20,8 +20,9 @@ import type { ReviewItemStatus, SupervisionDecision, } from '../../features/proForge/types'; -import { nextStage } from '../../features/proForge/types'; +import { isEditingStage, nextStage } from '../../features/proForge/types'; import { logger } from '../logger'; +import { planAcceptedManuscriptEdits } from './applyReviewEdits'; // --------------------------------------------------------------------------- // Agent imports (lazy to avoid circular deps at module level) @@ -76,6 +77,28 @@ export class ProForgeOrchestrator { this.context = context; } + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + /** QNBS-v3: HEAD snapshot of the current branch — deduplicates the 4× inline lookup. */ + private headSnapshotId(): string | undefined { + const vcState = this.context.getState().versionControl; + return vcState.branches.find((b) => b.id === vcState.currentBranchId)?.headSnapshotId; + } + + /** QNBS-v3: Best-effort persist of run history to IDB (survives reload). Never throws. */ + private async persistHistory(): Promise { + try { + const runHistory = this.context.getState().proForge.runHistory; + if (!runHistory?.length) return; + const { saveRunHistory } = await import('./proForgeHistoryStore'); + await saveRunHistory(this.context.projectId, runHistory); + } catch (err) { + logger.warn('ProForge: failed to persist run history', err); + } + } + // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- @@ -86,6 +109,9 @@ export class ProForgeOrchestrator { */ async startPipeline(label: string, config: PipelineConfig): Promise { const { dispatch, getState, projectId } = this.context; + // QNBS-v3: Reset the abort signal — the orchestrator instance is reused across runs + // (cached in a hook ref); a prior abort would otherwise leave every agent pre-aborted. + this.abortController = new AbortController(); const state = getState(); // Create pre-pipeline snapshot via version control @@ -106,9 +132,7 @@ export class ProForgeOrchestrator { ); // Retrieve the snapshot ID (it's the last one created on current branch) - const vcState = getState().versionControl; - const currentBranch = vcState.branches.find((b) => b.id === vcState.currentBranchId); - const preSnapshotId = currentBranch?.headSnapshotId ?? 'unknown'; + const preSnapshotId = this.headSnapshotId() ?? 'unknown'; dispatch( (await import('../../features/proForge/proForgeSlice')).startPipeline({ @@ -150,9 +174,7 @@ export class ProForgeOrchestrator { }), ); } - const vcState = getState().versionControl; - const currentBranch = vcState.branches.find((b) => b.id === vcState.currentBranchId); - const snapshotId = currentBranch?.headSnapshotId; + const snapshotId = this.headSnapshotId(); dispatch(stageStarted({ stage, ...(snapshotId !== undefined && { snapshotId }) })); @@ -166,11 +188,14 @@ export class ProForgeOrchestrator { maxRetries: number, ): Promise { const { dispatch } = this.context; + // QNBS-v3: Carries the prior attempt's rejection reasons into the next prompt. + let retryFeedback = ''; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const AgentClass = await loadAgent(stage); const agent = new AgentClass(this.context); + if (retryFeedback) agent.setRetryFeedback(retryFeedback); const result = await agent.execute(this.abortController.signal); if (this.abortController.signal.aborted) { @@ -187,6 +212,13 @@ export class ProForgeOrchestrator { `SupervisorAgent: Stage ${stage} flagged (retry ${attempt + 1}):`, decision.reasons, ); + // QNBS-v3: Feed the supervisor's reasons + any self-reflection note into the retry. + const reflectionNote = (result.agentOutput as { reflectionNotes?: string } | undefined) + ?.reflectionNotes; + retryFeedback = [...decision.reasons, reflectionNote] + .filter((r): r is string => Boolean(r)) + .map((r) => `- ${r}`) + .join('\n'); continue; } @@ -235,6 +267,7 @@ export class ProForgeOrchestrator { if (!next || next === 'archived') { // Pipeline complete dispatch(pipelineCompleted()); + await this.persistHistory(); return; } @@ -259,7 +292,41 @@ export class ProForgeOrchestrator { ): Promise { const { dispatch, getState } = this.context; - // Create post-stage snapshot after applying accepted edits + // QNBS-v3: Apply accepted edits to the manuscript BEFORE snapshotting, so the post-stage + // snapshot captures the edited text. Only editing stages mutate prose; production/publishing/ + // analytics are advisory. Stale/unanchorable edits are skipped, never force-applied. + if (isEditingStage(stage)) { + const stageResult = getState().proForge.currentRun?.stages.find((s) => s.stage === stage); + const project = getState().project.present?.data; + if (stageResult && project) { + const acceptedIds = new Set( + decisions.filter((d) => d.status === 'accepted').map((d) => d.itemId), + ); + const acceptedItems = stageResult.reviewItems.filter((ri) => acceptedIds.has(ri.id)); + const { updates, applied, skipped } = planAcceptedManuscriptEdits( + project.manuscript, + acceptedItems, + ); + if (updates.length > 0) { + const { projectActions } = await import('../../features/project/projectSlice'); + for (const update of updates) { + dispatch( + projectActions.updateManuscriptSection({ + id: update.id, + changes: { content: update.content }, + }), + ); + } + } + if (skipped > 0) { + logger.warn( + `ProForge submitReview: stage ${stage} applied ${applied} edit(s), skipped ${skipped} stale/unanchorable edit(s).`, + ); + } + } + } + + // Create post-stage snapshot (now reflecting any applied edits). const project = getState().project.present?.data; if (project) { const { versionControlActions } = await import( @@ -272,9 +339,7 @@ export class ProForgeOrchestrator { }), ); } - const vcState = getState().versionControl; - const currentBranch = vcState.branches.find((b) => b.id === vcState.currentBranchId); - const postSnapshotId = currentBranch?.headSnapshotId; + const postSnapshotId = this.headSnapshotId(); dispatch( submitStageReview({ @@ -336,6 +401,7 @@ export class ProForgeOrchestrator { } dispatch(pipelineAborted()); + await this.persistHistory(); } /** diff --git a/tests/unit/lora/styleAdaptedWriterAgent.test.ts b/tests/unit/lora/styleAdaptedWriterAgent.test.ts deleted file mode 100644 index 8bb300c4e..000000000 --- a/tests/unit/lora/styleAdaptedWriterAgent.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Tests for services/proForge/pipelineAgents/styleAdaptedWriterAgent.ts - * QNBS-v3: Fallback path (no active adapter) + suggestion path with mocked localAiFacade. - */ - -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -// --------------------------------------------------------------------------- -// Mocks -// --------------------------------------------------------------------------- - -vi.mock('../../../services/loraAdapterService', () => ({ - getActiveAdapter: vi.fn(), -})); - -vi.mock('../../../services/localAiFacade', () => ({ - generateLocalText: vi.fn(), -})); - -vi.mock('../../../services/proForge/proForgeMemoryBank', () => ({ - getMemoryBank: vi.fn(() => ({ - buildContextString: vi.fn().mockResolvedValue(''), - remember: vi.fn(), - recall: vi.fn().mockResolvedValue([]), - })), -})); - -vi.mock('../../../services/logger', () => ({ - logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, -})); - -vi.mock('../../../services/aiProviderService', () => ({ - aiProviderService: { generateText: vi.fn() }, -})); - -// --------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------- - -import { generateLocalText } from '../../../services/localAiFacade'; -import { getActiveAdapter } from '../../../services/loraAdapterService'; -import { StyleAdaptedWriterAgent } from '../../../services/proForge/pipelineAgents/styleAdaptedWriterAgent'; -import type { OrchestratorContext } from '../../../services/proForge/proForgeOrchestrator'; - -// --------------------------------------------------------------------------- -// Fixtures -// --------------------------------------------------------------------------- - -const ACTIVE_ADAPTER = { - id: 'lora-1', - name: 'HemingwayStyle', - description: '', - modelCompatibility: 'llama-3.2-7b', - scale: 1, - fileSizeBytes: 100, - createdAt: 1, - isActive: true, -}; - -/** Long enough content to pass the 200-char threshold in the agent (≥ 201 chars). */ -const LONG_CONTENT = - 'The old lighthouse keeper had not left the island in forty years. ' + - 'Each morning he climbed the spiral staircase and polished the great lens until it blazed. ' + - 'Tonight a storm was drawing close across the black water and the fog horns wailed.'; - -function makeContext( - sections = [{ id: 's1', title: 'Ch 1', content: LONG_CONTENT }], -): OrchestratorContext { - return { - projectId: 'proj-1', - // biome-ignore lint/suspicious/noExplicitAny: test mock - dispatch: vi.fn() as any, - getState: vi.fn().mockReturnValue({ - project: { - present: { - data: { - title: 'Test Novel', - logline: '', - manuscript: sections, - characters: { ids: [], entities: {} }, - worlds: { ids: [], entities: {} }, - outline: [], - }, - }, - }, - // biome-ignore lint/suspicious/noExplicitAny: partial test state - } as any), - manuscript: sections, - characters: [], - worlds: [], - config: { - genrePreset: 'literary-fiction', - selectedStages: ['lineProse'], - aiProvider: 'webllm', - ragMode: 'hybrid', - maxTokens: 2000, - creativity: 'Balanced', - useDuckDb: false, - autoAcceptThreshold: 0, - language: 'en', - }, - }; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('StyleAdaptedWriterAgent — no active adapter', () => { - beforeEach(() => { - vi.mocked(getActiveAdapter).mockResolvedValue(null); - }); - - it('returns isFallback: true and empty reviewItems', async () => { - const agent = new StyleAdaptedWriterAgent(makeContext()); - const result = await agent.execute(new AbortController().signal); - - expect(result.reviewItems).toHaveLength(0); - expect(result.agentOutput).toMatchObject({ isFallback: true, reason: 'no_active_adapter' }); - expect(result.metrics.aiCalls).toBe(0); - }); -}); - -describe('StyleAdaptedWriterAgent — with active adapter', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(getActiveAdapter).mockResolvedValue(ACTIVE_ADAPTER); - vi.mocked(generateLocalText).mockResolvedValue({ - layer: 'webllm' as const, - text: 'The keeper gripped the railing as the storm surged closer, each wave a drumbeat of inevitability.', - }); - }); - - it('produces at least one proseEdit reviewItem', async () => { - const agent = new StyleAdaptedWriterAgent(makeContext()); - const result = await agent.execute(new AbortController().signal); - - // Verify the mock was actually invoked via the dynamic import path - expect(vi.mocked(generateLocalText)).toHaveBeenCalled(); - expect(result.reviewItems.length).toBeGreaterThan(0); - expect(result.reviewItems[0]!.type).toBe('proseEdit'); - expect(result.reviewItems[0]!.sectionId).toBe('s1'); - expect(result.agentOutput).not.toHaveProperty('isFallback'); - }); - - it('reports adapter id and name in agentOutput', async () => { - const agent = new StyleAdaptedWriterAgent(makeContext()); - const result = await agent.execute(new AbortController().signal); - - expect(result.agentOutput).toMatchObject({ - adapterId: 'lora-1', - adapterName: 'HemingwayStyle', - }); - }); - - it('skips sections shorter than 200 characters', async () => { - const agent = new StyleAdaptedWriterAgent( - makeContext([{ id: 's1', title: 'Short', content: 'Too short.' }]), - ); - const result = await agent.execute(new AbortController().signal); - expect(result.reviewItems).toHaveLength(0); - expect(vi.mocked(generateLocalText)).not.toHaveBeenCalled(); - }); - - it('returns isFallback: true when signal is already aborted', async () => { - const ctrl = new AbortController(); - ctrl.abort(); - const agent = new StyleAdaptedWriterAgent(makeContext()); - const result = await agent.execute(ctrl.signal); - - expect(result.agentOutput).toMatchObject({ isFallback: true, reason: 'aborted' }); - }); -}); diff --git a/tests/unit/proForge/applyReviewEdits.test.ts b/tests/unit/proForge/applyReviewEdits.test.ts new file mode 100644 index 000000000..edadf685e --- /dev/null +++ b/tests/unit/proForge/applyReviewEdits.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from 'vitest'; +import type { ReviewItem } from '../../../features/proForge/types'; +import { + applyReviewEditsToSection, + planAcceptedManuscriptEdits, +} from '../../../services/proForge/applyReviewEdits'; + +// QNBS-v3: Minimal ReviewItem factory — only the fields the applier reads. +function item(partial: Partial): ReviewItem { + return { + id: partial.id ?? 'r1', + stage: 'lineProse', + type: 'proseEdit', + severity: 'info', + description: 'd', + confidence: 0.9, + status: 'accepted', + createdAt: '2026-01-01T00:00:00.000Z', + ...partial, + }; +} + +describe('applyReviewEditsToSection', () => { + it('applies a single range-based edit verified against original', () => { + const content = 'The quick brown fox.'; + const result = applyReviewEditsToSection(content, [ + item({ range: { start: 4, end: 9 }, original: 'quick', proposed: 'slow' }), + ]); + expect(result.content).toBe('The slow brown fox.'); + expect(result.applied).toBe(1); + expect(result.skipped).toBe(0); + }); + + it('applies multiple edits back-to-front so offsets stay valid', () => { + const content = 'aaa bbb ccc'; + const result = applyReviewEditsToSection(content, [ + item({ id: '1', range: { start: 0, end: 3 }, original: 'aaa', proposed: 'X' }), + item({ id: '2', range: { start: 8, end: 11 }, original: 'ccc', proposed: 'YYYYY' }), + ]); + expect(result.content).toBe('X bbb YYYYY'); + expect(result.applied).toBe(2); + }); + + it('falls back to text match when the offset is stale', () => { + const content = 'hello wonderful world'; + // Range points at the wrong place; original text still locatable. + const result = applyReviewEditsToSection(content, [ + item({ range: { start: 0, end: 5 }, original: 'wonderful', proposed: 'cruel' }), + ]); + expect(result.content).toBe('hello cruel world'); + expect(result.applied).toBe(1); + expect(result.skipped).toBe(0); + }); + + it('skips an edit whose original text cannot be found', () => { + const content = 'nothing to see here'; + const result = applyReviewEditsToSection(content, [ + item({ original: 'absent phrase', proposed: 'x' }), + ]); + expect(result.content).toBe(content); + expect(result.applied).toBe(0); + expect(result.skipped).toBe(1); + }); + + it('ignores advisory items with no proposed replacement (not counted as skipped)', () => { + const content = 'unchanged'; + const result = applyReviewEditsToSection(content, [ + item({ type: 'pacingIssue', description: 'advisory only' }), + ]); + expect(result.content).toBe('unchanged'); + expect(result.applied).toBe(0); + expect(result.skipped).toBe(0); + }); + + it('drops overlapping edits rather than corrupting the text', () => { + const content = 'abcdef'; + const result = applyReviewEditsToSection(content, [ + item({ id: '1', range: { start: 1, end: 4 }, proposed: 'XX' }), + item({ id: '2', range: { start: 2, end: 5 }, proposed: 'YY' }), + ]); + // Right-most (start=2) applies first; the start=1 edit overlaps it and is skipped. + expect(result.applied).toBe(1); + expect(result.skipped).toBe(1); + }); +}); + +describe('planAcceptedManuscriptEdits', () => { + const manuscript = [ + { id: 's1', content: 'The quick brown fox.' }, + { id: 's2', content: 'Jumped over the lazy dog.' }, + ]; + + it('returns only sections that changed, with aggregate counts', () => { + const { updates, applied, skipped } = planAcceptedManuscriptEdits(manuscript, [ + item({ + id: 'a', + sectionId: 's1', + range: { start: 4, end: 9 }, + original: 'quick', + proposed: 'sly', + }), + item({ id: 'b', sectionId: 's2', original: 'lazy', proposed: 'sleepy' }), + ]); + expect(updates).toHaveLength(2); + expect(updates.find((u) => u.id === 's1')?.content).toBe('The sly brown fox.'); + expect(updates.find((u) => u.id === 's2')?.content).toBe('Jumped over the sleepy dog.'); + expect(applied).toBe(2); + expect(skipped).toBe(0); + }); + + it('counts edits for a deleted section as skipped', () => { + const { updates, applied, skipped } = planAcceptedManuscriptEdits(manuscript, [ + item({ id: 'a', sectionId: 'ghost', original: 'x', proposed: 'y' }), + ]); + expect(updates).toHaveLength(0); + expect(applied).toBe(0); + expect(skipped).toBe(1); + }); + + it('ignores items without a sectionId or proposed text', () => { + const { updates } = planAcceptedManuscriptEdits(manuscript, [ + item({ id: 'a', proposed: 'orphan' }), + item({ id: 'b', sectionId: 's1', type: 'pacingIssue' }), + ]); + expect(updates).toHaveLength(0); + }); +}); diff --git a/tests/unit/proForge/components/PipelineReviewPanel.test.tsx b/tests/unit/proForge/components/PipelineReviewPanel.test.tsx index 7a230b05e..bc542700c 100644 --- a/tests/unit/proForge/components/PipelineReviewPanel.test.tsx +++ b/tests/unit/proForge/components/PipelineReviewPanel.test.tsx @@ -12,6 +12,8 @@ import type { ReviewItemType, StageStatus, } from '../../../../features/proForge/types'; +// QNBS-v3: Real English strings so assertions read naturally; t() resolves keys + {{vars}}. +import mockEnCommon from '../../../../locales/en/common.json'; // --------------------------------------------------------------------------- // Mocks @@ -144,6 +146,24 @@ vi.mock('../../../../features/proForge/proForgeSlice', () => ({ }, })); +vi.mock('../../../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string, vars?: Record) => { + const dict = mockEnCommon as Record; + let s = dict[key] ?? key; + if (vars) { + for (const [k, v] of Object.entries(vars)) s = s.replace(`{{${k}}}`, String(v)); + } + return s; + }, + language: 'en', + }), +})); + +vi.mock('../../../../contexts/LiveRegionContext', () => ({ + useAnnounce: () => vi.fn(), +})); + // --------------------------------------------------------------------------- // Imports after mocks // --------------------------------------------------------------------------- @@ -189,7 +209,8 @@ describe('PipelineReviewPanel', () => { describe('header', () => { it('shows stage name in header', () => { render(); - expect(screen.getByText(/Review: intake/i)).toBeInTheDocument(); + // Stage id 'intake' is localised to its display name in the header. + expect(screen.getByText(/Review: Intake/i)).toBeInTheDocument(); }); it('shows correct pending/accepted/rejected counts', () => { @@ -425,8 +446,8 @@ describe('PipelineReviewPanel', () => { currentStageReviewItems: [makeItem('item-1', 'pending', { severity: 'critical' })], }); render(); - // SEVERITY_ICONS.critical = '🔴' in ReviewItemCard; summary card shows longer string - expect(screen.getByText('🔴')).toBeInTheDocument(); + // 🔴 appears in the item card and (for critical items) the summary card; both are aria-hidden. + expect(screen.getAllByText('🔴').length).toBeGreaterThanOrEqual(1); }); it('shows sectionTitle when present', () => { diff --git a/tests/unit/proForge/pipelineAgents/baseAgent.test.ts b/tests/unit/proForge/pipelineAgents/baseAgent.test.ts index 7cf0de228..f5da84252 100644 --- a/tests/unit/proForge/pipelineAgents/baseAgent.test.ts +++ b/tests/unit/proForge/pipelineAgents/baseAgent.test.ts @@ -275,6 +275,24 @@ describe('BaseAgent', () => { expect.objectContaining({ options: expect.objectContaining({ maxTokens: 4000 }) }), ); }); + + it('does not alter the prompt when no retry feedback is set', async () => { + mockGenerate.mockResolvedValueOnce({ text: 'OK', usage: {} }); + await agent.publicGenerate('Original prompt.'); + expect(mockGenerate).toHaveBeenCalledWith( + expect.objectContaining({ prompt: 'Original prompt.' }), + ); + }); + + it('prepends supervisor feedback to the prompt after setRetryFeedback', async () => { + mockGenerate.mockResolvedValueOnce({ text: 'Corrected.', usage: {} }); + agent.setRetryFeedback('- No structural edits found.'); + await agent.publicGenerate('Original prompt.'); + const callArg = mockGenerate.mock.calls.at(-1)?.[0] as { prompt: string }; + expect(callArg.prompt).toContain('previous attempt was rejected'); + expect(callArg.prompt).toContain('No structural edits found.'); + expect(callArg.prompt).toContain('Original prompt.'); + }); }); describe('selfReflect()', () => { diff --git a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts index 3247e431e..4d9d0ea74 100644 --- a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts +++ b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts @@ -66,7 +66,7 @@ const REAL_QUALITY_SCORE = { marketability: 77, }; -// Fallback sentinel: all 3 checked dimensions are exactly 50. +// Uniform 50 scores — a plausible-but-mediocre real analysis, NOT a fallback by itself. const FALLBACK_QUALITY_SCORE = { overall: 50, prose: 50, @@ -77,6 +77,17 @@ const FALLBACK_QUALITY_SCORE = { marketability: 50, }; +// Zeroed scores accompany a genuine fallback report (isFallback: true). +const ZERO_QUALITY_SCORE = { + overall: 0, + prose: 0, + structure: 0, + consistency: 0, + pacing: 0, + dialogue: 0, + marketability: 0, +}; + // --------------------------------------------------------------------------- // Tests: evaluate() dispatch // --------------------------------------------------------------------------- @@ -88,21 +99,109 @@ describe('SupervisorAgent', () => { agent = new SupervisorAgent(makeContext()); }); - describe('evaluate() — unknown/default stages', () => { - it('passes with score 100 for stages without specific logic', () => { - const result = agent.evaluate('lineProse', { reviewItems: [], agentOutput: undefined }); + describe('evaluate() — default/terminal stages', () => { + it('passes with score 100 for idle/archived stages (no specific logic)', () => { + const idle = agent.evaluate('idle', { reviewItems: [], agentOutput: undefined }); + expect(idle.pass).toBe(true); + expect(idle.qualityScore).toBe(100); + expect(idle.reasons).toHaveLength(0); + expect(agent.evaluate('archived', { reviewItems: [], agentOutput: undefined }).pass).toBe( + true, + ); + }); + }); + + describe('evaluateLineProse', () => { + it('passes with score 85 when prose edits are present', () => { + const result = agent.evaluate('lineProse', { reviewItems: [], agentOutput: { edits: [{}] } }); expect(result.pass).toBe(true); - expect(result.qualityScore).toBe(100); - expect(result.reasons).toHaveLength(0); + expect(result.qualityScore).toBe(85); + }); + + it('passes for a short manuscript even with zero edits', () => { + const result = agent.evaluate('lineProse', { reviewItems: [], agentOutput: { edits: [] } }); + expect(result.pass).toBe(true); + }); + + it('fails for a long manuscript with zero prose edits and no review items', () => { + const longAgent = new SupervisorAgent(makeContext('word '.repeat(1100))); + const result = longAgent.evaluate('lineProse', { + reviewItems: [], + agentOutput: { edits: [] }, + }); + expect(result.pass).toBe(false); + expect(result.retryRecommended).toBe(true); + }); + }); + + describe('evaluateCopyEdit', () => { + it('passes when copy-edit findings exist', () => { + const result = agent.evaluate('copyEdit', { + reviewItems: [], + agentOutput: { grammarEdits: [{}], styleEdits: [], repetitionHits: [], formatIssues: [] }, + }); + expect(result.pass).toBe(true); + }); + + it('fails for a long manuscript with zero findings', () => { + const longAgent = new SupervisorAgent(makeContext('word '.repeat(1600))); + const result = longAgent.evaluate('copyEdit', { + reviewItems: [], + agentOutput: { grammarEdits: [], styleEdits: [], repetitionHits: [], formatIssues: [] }, + }); + expect(result.pass).toBe(false); + expect(result.retryRecommended).toBe(true); + }); + }); + + describe('evaluateProduction', () => { + it('passes with score 95 when at least one artifact exists', () => { + const result = agent.evaluate('production', { + reviewItems: [], + agentOutput: { artifacts: [{}] }, + }); + expect(result.pass).toBe(true); + expect(result.qualityScore).toBe(95); + }); + + it('fails when no artifacts were produced', () => { + const result = agent.evaluate('production', { + reviewItems: [], + agentOutput: { artifacts: [] }, + }); + expect(result.pass).toBe(false); + expect(result.reasons.some((r) => r.includes('artifact'))).toBe(true); + }); + }); + + describe('evaluatePublishing', () => { + it('passes when title + back-cover blurb are present', () => { + const result = agent.evaluate('publishing', { + reviewItems: [], + agentOutput: { metadata: { title: 'My Book' }, blurbs: { backCover: 'A gripping tale.' } }, + }); + expect(result.pass).toBe(true); + }); + + it('fails when title/blurb are missing', () => { + const result = agent.evaluate('publishing', { + reviewItems: [], + agentOutput: { metadata: { title: '' }, blurbs: { backCover: '' } }, + }); + expect(result.pass).toBe(false); + expect(result.reasons.length).toBeGreaterThanOrEqual(1); }); + }); - it('passes for copyEdit stage', () => { - const result = agent.evaluate('copyEdit', { reviewItems: [], agentOutput: undefined }); + describe('evaluateAnalytics', () => { + it('passes with score 100 when metrics exist', () => { + const result = agent.evaluate('analytics', { reviewItems: [], agentOutput: { metrics: {} } }); expect(result.pass).toBe(true); + expect(result.qualityScore).toBe(100); }); - it('passes for publishing stage', () => { - const result = agent.evaluate('publishing', { reviewItems: [], agentOutput: undefined }); + it('never blocks the pipeline even without metrics', () => { + const result = agent.evaluate('analytics', { reviewItems: [], agentOutput: undefined }); expect(result.pass).toBe(true); }); }); @@ -126,11 +225,14 @@ describe('SupervisorAgent', () => { expect(result.qualityScore).toBe(72); }); - it('fails and recommends retry when all 3 sentinel dimensions are 50', () => { + it('fails and recommends retry when the report is marked isFallback', () => { + // QNBS-v3: Real fallback reports carry isFallback:true with zeroed scores + // (see DiagnosticAgent.createFallbackReport). Detection keys off the flag, not score===50. const result = agent.evaluate('intake', { reviewItems: [], agentOutput: { - qualityScore: FALLBACK_QUALITY_SCORE, + isFallback: true, + qualityScore: ZERO_QUALITY_SCORE, consistencyIssues: [], structuralGaps: [], }, @@ -141,6 +243,19 @@ describe('SupervisorAgent', () => { expect(result.reasons.some((r) => r.includes('fallback'))).toBe(true); }); + it('does NOT treat uniform 50 scores as fallback without the isFallback flag', () => { + const result = agent.evaluate('intake', { + reviewItems: [], + agentOutput: { + qualityScore: FALLBACK_QUALITY_SCORE, + consistencyIssues: [{ id: 'ci-1' }], + structuralGaps: [], + }, + }); + // No isFallback flag → treated as a (low but real) analysis, passes the gate. + expect(result.pass).toBe(true); + }); + it('does NOT treat partial 50-scores as fallback (only all-3-at-50 triggers)', () => { const result = agent.evaluate('intake', { reviewItems: [], diff --git a/tests/unit/proForge/proForgeHistoryStore.test.ts b/tests/unit/proForge/proForgeHistoryStore.test.ts new file mode 100644 index 000000000..18433cb96 --- /dev/null +++ b/tests/unit/proForge/proForgeHistoryStore.test.ts @@ -0,0 +1,70 @@ +// @vitest-environment node +// QNBS-v3: node environment + fresh IDBFactory per test for full isolation (mirrors memory-bank test). + +import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import type { PipelineRun } from '../../../features/proForge/types'; +import { + _resetHistoryDbForTest, + loadRunHistory, + MAX_RUN_HISTORY, + saveRunHistory, +} from '../../../services/proForge/proForgeHistoryStore'; + +beforeEach(() => { + global.indexedDB = new IDBFactory(); + global.IDBKeyRange = IDBKeyRange; + _resetHistoryDbForTest(); +}); + +afterEach(() => { + _resetHistoryDbForTest(); +}); + +function run(id: string): PipelineRun { + return { + id, + projectId: 'p1', + label: id, + config: {} as PipelineRun['config'], + status: 'completed', + activeStage: 'archived', + stages: [], + startedAt: '2026-01-01T00:00:00.000Z', + prePipelineSnapshotId: 'snap-1', + traceLog: [], + }; +} + +describe('proForgeHistoryStore', () => { + it('returns an empty array when nothing is persisted', async () => { + expect(await loadRunHistory('p1')).toEqual([]); + }); + + it('round-trips run history for a project', async () => { + await saveRunHistory('p1', [run('a'), run('b')]); + const loaded = await loadRunHistory('p1'); + expect(loaded.map((r) => r.id)).toEqual(['a', 'b']); + }); + + it('scopes history by project id', async () => { + await saveRunHistory('p1', [run('a')]); + await saveRunHistory('p2', [run('z')]); + expect((await loadRunHistory('p2')).map((r) => r.id)).toEqual(['z']); + expect((await loadRunHistory('p1')).map((r) => r.id)).toEqual(['a']); + }); + + it('caps stored history to MAX_RUN_HISTORY (most-recent-first)', async () => { + const many = Array.from({ length: MAX_RUN_HISTORY + 5 }, (_, i) => run(`r${i}`)); + await saveRunHistory('p1', many); + const loaded = await loadRunHistory('p1'); + expect(loaded).toHaveLength(MAX_RUN_HISTORY); + expect(loaded[0]?.id).toBe('r0'); + }); + + it('overwrites prior history for the same project', async () => { + await saveRunHistory('p1', [run('old')]); + await saveRunHistory('p1', [run('new')]); + expect((await loadRunHistory('p1')).map((r) => r.id)).toEqual(['new']); + }); +}); diff --git a/tests/unit/proForge/proForgeMemoryBank.test.ts b/tests/unit/proForge/proForgeMemoryBank.test.ts index 944e45bac..7683d7f96 100644 --- a/tests/unit/proForge/proForgeMemoryBank.test.ts +++ b/tests/unit/proForge/proForgeMemoryBank.test.ts @@ -3,7 +3,25 @@ // Fresh IDBFactory + _resetDbForTest() ensures complete test isolation. import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// QNBS-v3: Deterministic 4-dim embeddings over a tiny vocabulary so semantic ranking is testable +// without loading the real MiniLM model. searchMemoryEntries dynamically imports this module. +vi.mock('../../../services/ai/localEmbeddingService', () => { + const VOCAB = ['dragon', 'sea', 'hero', 'magic']; + const embed = (text: string): number[] => { + const lower = text.toLowerCase(); + const v = VOCAB.map((w) => (lower.match(new RegExp(w, 'g')) ?? []).length); + const norm = Math.sqrt(v.reduce((a, b) => a + b * b, 0)) || 1; + return v.map((x) => x / norm); + }; + return { + embedText: vi.fn(async (t: string) => embed(t)), + cosineSimilarity: (a: number[], b: number[]) => a.reduce((s, x, i) => s + x * (b[i] ?? 0), 0), + embedBatch: vi.fn(), + }; +}); + import { _resetDbForTest, clearMemoryBankCache, @@ -217,6 +235,42 @@ describe('searchMemoryEntries', () => { }); }); +describe('searchMemoryEntries — semantic & hybrid modes', () => { + beforeEach(async () => { + await saveMemoryEntry({ + projectId: 'p2', + category: 'lore', + key: 'k1', + content: 'The dragon guards the mountain.', + sourceStage: 'intake', + }); + await saveMemoryEntry({ + projectId: 'p2', + category: 'lore', + key: 'k2', + content: 'The hero sails the sea.', + sourceStage: 'intake', + }); + }); + + it('ranks by embedding similarity in semantic mode', async () => { + const results = await searchMemoryEntries('p2', 'dragon', 10, 'semantic'); + expect(results[0]?.key).toBe('k1'); + }); + + it('blends keyword + semantic in hybrid mode', async () => { + const results = await searchMemoryEntries('p2', 'hero sea', 10, 'hybrid'); + expect(results[0]?.key).toBe('k2'); + }); + + it('falls back to keyword ranking when embedding fails', async () => { + const svc = await import('../../../services/ai/localEmbeddingService'); + vi.mocked(svc.embedText).mockRejectedValueOnce(new Error('no model')); + const results = await searchMemoryEntries('p2', 'dragon', 10, 'hybrid'); + expect(results.some((r) => r.key === 'k1')).toBe(true); + }); +}); + // --------------------------------------------------------------------------- // deleteMemoryEntry // --------------------------------------------------------------------------- diff --git a/tests/unit/proForge/proForgeOrchestrator.test.ts b/tests/unit/proForge/proForgeOrchestrator.test.ts index 8d0e92d82..dd5175731 100644 --- a/tests/unit/proForge/proForgeOrchestrator.test.ts +++ b/tests/unit/proForge/proForgeOrchestrator.test.ts @@ -33,48 +33,56 @@ vi.mock('../../../services/proForge/pipelineAgents/diagnosticAgent', () => ({ DiagnosticAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); vi.mock('../../../services/proForge/pipelineAgents/structuralAgent', () => ({ StructuralAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); vi.mock('../../../services/proForge/pipelineAgents/proseAgent', () => ({ ProseAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); vi.mock('../../../services/proForge/pipelineAgents/copyEditAgent', () => ({ CopyEditAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); vi.mock('../../../services/proForge/pipelineAgents/proofAgent', () => ({ ProofAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); vi.mock('../../../services/proForge/pipelineAgents/productionAgent', () => ({ ProductionAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); vi.mock('../../../services/proForge/pipelineAgents/publishingAgent', () => ({ PublishingAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); vi.mock('../../../services/proForge/pipelineAgents/analyticsAgent', () => ({ AnalyticsAgent: class { constructor(public ctx: unknown) {} execute = mockAgentExecute; + setRetryFeedback = vi.fn(); }, })); @@ -100,6 +108,11 @@ vi.mock('../../../features/proForge/proForgeSlice', () => ({ proForgeActions: {}, })); +// Mock run-history persistence dynamic import +vi.mock('../../../services/proForge/proForgeHistoryStore', () => ({ + saveRunHistory: vi.fn().mockResolvedValue(undefined), +})); + // --------------------------------------------------------------------------- // Import after mocks // --------------------------------------------------------------------------- @@ -266,6 +279,35 @@ describe('ProForgeOrchestrator', () => { 'No project data available', ); }); + + it('resets the abort signal so a re-run after dispose still completes the stage', async () => { + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'running', + stages: [], + config: { ...DEFAULT_CONFIG, selectedStages: ['intake'], maxRetries: 0 }, + label: 'Re-run', + }, + isRunning: true, + }); + const orch = new ProForgeOrchestrator(ctx); + // Simulate a prior abort — the constructor-created controller is now aborted. + orch.dispose(); + + await orch.startPipeline('Re-run', { + ...DEFAULT_CONFIG, + selectedStages: ['intake'], + maxRetries: 0, + }); + + const { stageCompleted, stageFailed } = await import( + '../../../features/proForge/proForgeSlice' + ); + // Without the reset, the stale aborted signal would trip the post-execute guard → stageFailed. + expect(vi.mocked(stageCompleted)).toHaveBeenCalled(); + expect(vi.mocked(stageFailed)).not.toHaveBeenCalled(); + }); }); describe('executeStage', () => { @@ -449,6 +491,140 @@ describe('ProForgeOrchestrator', () => { }); }); + describe('submitReview — applying accepted edits to the manuscript', () => { + const editItem = { + id: 'e1', + stage: 'lineProse', + type: 'proseEdit', + severity: 'info', + sectionId: 's1', + range: { start: 0, end: 5 }, + original: 'Hello', + proposed: 'Hi', + description: 'Tighten greeting', + confidence: 0.9, + status: 'accepted', + createdAt: '2026-01-01T00:00:00.000Z', + }; + + function findUpdate(ctx: OrchestratorContext) { + const calls = (ctx.dispatch as unknown as { mock: { calls: unknown[][] } }).mock.calls; + return calls + .map((c) => c[0] as { payload?: { id?: string; changes?: { content?: string } } }) + .find((a) => a?.payload?.changes?.content !== undefined); + } + + it('writes accepted edits into the section content (editing stage)', async () => { + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'awaitingReview', + activeStage: 'lineProse', + label: 'Edit Test', + config: DEFAULT_CONFIG, + stages: [{ stage: 'lineProse', status: 'awaitingReview', reviewItems: [editItem] }], + }, + }); + const orch = new ProForgeOrchestrator(ctx); + await orch.submitReview('lineProse', [{ itemId: 'e1', status: 'accepted' }], { + advance: false, + }); + + const update = findUpdate(ctx); + expect(update?.payload?.id).toBe('s1'); + expect(update?.payload?.changes?.content).toBe('Hi world.'); + }); + + it('does not modify the manuscript for non-editing stages', async () => { + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'awaitingReview', + activeStage: 'publishing', + label: 'Edit Test', + config: DEFAULT_CONFIG, + stages: [ + { + stage: 'publishing', + status: 'awaitingReview', + reviewItems: [{ ...editItem, stage: 'publishing' }], + }, + ], + }, + }); + const orch = new ProForgeOrchestrator(ctx); + await orch.submitReview('publishing', [{ itemId: 'e1', status: 'accepted' }], { + advance: false, + }); + + expect(findUpdate(ctx)).toBeUndefined(); + }); + + it('does not apply rejected edits', async () => { + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'awaitingReview', + activeStage: 'lineProse', + label: 'Edit Test', + config: DEFAULT_CONFIG, + stages: [{ stage: 'lineProse', status: 'awaitingReview', reviewItems: [editItem] }], + }, + }); + const orch = new ProForgeOrchestrator(ctx); + await orch.submitReview('lineProse', [{ itemId: 'e1', status: 'rejected' }], { + advance: false, + }); + + expect(findUpdate(ctx)).toBeUndefined(); + }); + }); + + describe('run-history persistence', () => { + it('persists run history to IDB on completion', async () => { + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'running', + activeStage: 'analytics', + stages: [], + config: DEFAULT_CONFIG, + label: 'Done', + }, + runHistory: [{ id: 'run-1', label: 'Done' }], + }); + const orch = new ProForgeOrchestrator(ctx); + await orch.advanceToNextStage('analytics'); + + const { saveRunHistory } = await import('../../../services/proForge/proForgeHistoryStore'); + expect(vi.mocked(saveRunHistory)).toHaveBeenCalledWith('p1', [ + { id: 'run-1', label: 'Done' }, + ]); + }); + + it('persists run history to IDB on abort', async () => { + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'running', + activeStage: 'structural', + stages: [], + config: DEFAULT_CONFIG, + label: 'Aborted', + prePipelineSnapshotId: 'snap-pre', + }, + runHistory: [{ id: 'run-1', label: 'Aborted' }], + }); + const orch = new ProForgeOrchestrator(ctx); + await orch.abortPipeline(); + + const { saveRunHistory } = await import('../../../services/proForge/proForgeHistoryStore'); + expect(vi.mocked(saveRunHistory)).toHaveBeenCalledWith('p1', [ + { id: 'run-1', label: 'Aborted' }, + ]); + }); + }); + describe('rollbackTo', () => { it('dispatches rollbackToStage', async () => { const ctx = makeContext({ diff --git a/tests/unit/proForge/toolRegistry.test.ts b/tests/unit/proForge/toolRegistry.test.ts deleted file mode 100644 index ddac2cc70..000000000 --- a/tests/unit/proForge/toolRegistry.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -/** - * Tests for ProForge ToolRegistry. - * QNBS-v3: Pure class behaviour — mocks logger and memoryBank. - */ - -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { z } from 'zod'; - -vi.mock('../../../services/logger', () => ({ - logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, -})); - -// Import AFTER mocks are set up -import { - type ToolContext, - type ToolDefinition, - ToolRegistry, - toolRegistry, -} from '../../../services/proForge/pipelineTools/toolRegistry'; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function makeTool(overrides: Partial = {}): ToolDefinition { - return { - name: 'testTool', - description: 'A test tool', - parameters: z.object({ input: z.string() }), - handler: vi.fn().mockResolvedValue({ result: 'ok' }), - stages: ['intake'], - isWrite: false, - ...overrides, - }; -} - -function makeContext(): ToolContext { - return { - projectId: 'proj-1', - // biome-ignore lint/suspicious/noExplicitAny: test mock - dispatch: vi.fn() as any, - getState: vi.fn().mockReturnValue({ - project: { - present: { - data: { manuscript: [], characters: { entities: {} }, worlds: { entities: {} } }, - }, - }, - proForge: { currentRun: null }, - }), - memoryBank: { - search: vi.fn().mockResolvedValue([]), - buildContextString: vi.fn().mockResolvedValue(''), - remember: vi.fn().mockResolvedValue({}), - recall: vi.fn().mockResolvedValue([]), - recallForStage: vi.fn().mockResolvedValue([]), - clear: vi.fn().mockResolvedValue(undefined), - // biome-ignore lint/suspicious/noExplicitAny: test mock - } as any, - signal: new AbortController().signal, - }; -} - -// --------------------------------------------------------------------------- -// ToolRegistry class tests -// --------------------------------------------------------------------------- - -describe('ToolRegistry', () => { - let registry: ToolRegistry; - - beforeEach(() => { - registry = new ToolRegistry(); - }); - - describe('register / get', () => { - it('registers a tool and retrieves it by name', () => { - const tool = makeTool({ name: 'myTool' }); - registry.register(tool); - expect(registry.get('myTool')).toBe(tool); - }); - - it('returns undefined for unknown tool', () => { - expect(registry.get('nonExistent')).toBeUndefined(); - }); - - it('overwrites an existing tool on re-register (warns)', async () => { - const { logger } = await import('../../../services/logger'); - const tool1 = makeTool({ name: 'dup' }); - const tool2 = makeTool({ name: 'dup', description: 'updated' }); - registry.register(tool1); - registry.register(tool2); - expect(registry.get('dup')?.description).toBe('updated'); - expect(vi.mocked(logger.warn)).toHaveBeenCalled(); - }); - }); - - describe('unregister', () => { - it('removes a registered tool and returns true', () => { - registry.register(makeTool({ name: 'toRemove' })); - expect(registry.unregister('toRemove')).toBe(true); - expect(registry.get('toRemove')).toBeUndefined(); - }); - - it('returns false when tool does not exist', () => { - expect(registry.unregister('ghost')).toBe(false); - }); - }); - - describe('list', () => { - it('returns empty array when no tools registered', () => { - expect(registry.list()).toHaveLength(0); - }); - - it('returns all registered tools', () => { - registry.register(makeTool({ name: 'a' })); - registry.register(makeTool({ name: 'b' })); - expect(registry.list()).toHaveLength(2); - }); - }); - - describe('listForStage', () => { - beforeEach(() => { - registry.register(makeTool({ name: 'intakeTool', stages: ['intake'] })); - registry.register(makeTool({ name: 'structuralTool', stages: ['structural'] })); - registry.register(makeTool({ name: 'globalTool', stages: ['*'] })); - }); - - it('returns tools for the specified stage plus global tools', () => { - const tools = registry.listForStage('intake'); - const names = tools.map((t) => t.name); - expect(names).toContain('intakeTool'); - expect(names).toContain('globalTool'); - expect(names).not.toContain('structuralTool'); - }); - - it('returns only global tools for an unmatched stage', () => { - const tools = registry.listForStage('proof'); - expect(tools.map((t) => t.name)).toEqual(['globalTool']); - }); - - it('returns nothing if no tools match', () => { - const emptyRegistry = new ToolRegistry(); - emptyRegistry.register(makeTool({ name: 'intakeOnly', stages: ['intake'] })); - expect(emptyRegistry.listForStage('analytics')).toHaveLength(0); - }); - }); - - describe('execute', () => { - it('calls the handler with validated args and returns the result', async () => { - const handler = vi.fn().mockResolvedValue({ done: true }); - registry.register(makeTool({ name: 'execTool', handler })); - const ctx = makeContext(); - const result = await registry.execute('execTool', { input: 'hello' }, ctx); - expect(result).toEqual({ done: true }); - expect(handler).toHaveBeenCalledWith({ input: 'hello' }, ctx); - }); - - it('throws when tool is not registered', async () => { - await expect(registry.execute('missing', {}, makeContext())).rejects.toThrow( - 'Tool "missing" not found', - ); - }); - - it('throws on invalid arguments (schema mismatch)', async () => { - registry.register(makeTool({ name: 'strictTool' })); - await expect(registry.execute('strictTool', { input: 123 }, makeContext())).rejects.toThrow( - 'Invalid arguments for tool', - ); - }); - }); - - describe('toAiSdkTools', () => { - it('returns tool descriptions and parameters for the stage', () => { - registry.register( - makeTool({ name: 'aiTool', stages: ['intake'], description: 'AI tool desc' }), - ); - const sdkTools = registry.toAiSdkTools('intake'); - expect(sdkTools['aiTool']).toBeDefined(); - expect(sdkTools['aiTool']!.description).toBe('AI tool desc'); - expect(sdkTools['aiTool']!.parameters).toBeDefined(); - }); - - it('excludes tools not available in the stage', () => { - registry.register(makeTool({ name: 'proofOnly', stages: ['proof'] })); - const sdkTools = registry.toAiSdkTools('intake'); - expect(sdkTools['proofOnly']).toBeUndefined(); - }); - }); -}); - -// --------------------------------------------------------------------------- -// Global toolRegistry singleton — built-in tools -// --------------------------------------------------------------------------- - -describe('toolRegistry singleton (built-in tools)', () => { - it('has readSection registered', () => { - expect(toolRegistry.get('readSection')).toBeDefined(); - }); - - it('has readAllSections registered', () => { - expect(toolRegistry.get('readAllSections')).toBeDefined(); - }); - - it('has readProjectMeta registered', () => { - expect(toolRegistry.get('readProjectMeta')).toBeDefined(); - }); - - it('has searchLore registered', () => { - expect(toolRegistry.get('searchLore')).toBeDefined(); - }); - - it('has analyzePacing registered (intake/structural only)', () => { - const tool = toolRegistry.get('analyzePacing'); - expect(tool).toBeDefined(); - expect(tool?.stages).toContain('intake'); - expect(tool?.stages).toContain('structural'); - }); - - it('has countWords registered', () => { - expect(toolRegistry.get('countWords')).toBeDefined(); - }); - - it('has generateReport registered', () => { - expect(toolRegistry.get('generateReport')).toBeDefined(); - }); - - it('has getMemoryContext registered', () => { - expect(toolRegistry.get('getMemoryContext')).toBeDefined(); - }); - - it('has proposeEdit registered (write tool)', () => { - const tool = toolRegistry.get('proposeEdit'); - expect(tool).toBeDefined(); - expect(tool?.isWrite).toBe(true); - }); - - it('readSection handler returns section data', async () => { - const ctx = makeContext(); - ctx.getState = vi.fn().mockReturnValue({ - project: { - present: { - data: { - manuscript: [ - { id: 's1', title: 'Ch 1', content: 'Hello world', status: 'draft', act: 1 }, - ], - characters: { entities: {} }, - worlds: { entities: {} }, - }, - }, - }, - proForge: { currentRun: null }, - }); - const result = (await toolRegistry.execute('readSection', { sectionId: 's1' }, ctx)) as Record< - string, - unknown - >; - expect(result['id']).toBe('s1'); - expect(result['title']).toBe('Ch 1'); - expect(result['content']).toBe('Hello world'); - }); - - it('readSection handler returns error for unknown section', async () => { - const ctx = makeContext(); - const result = (await toolRegistry.execute( - 'readSection', - { sectionId: 'ghost' }, - ctx, - )) as Record; - expect(result['error']).toContain('"ghost" not found'); - }); - - it('countWords handler returns total word count', async () => { - const ctx = makeContext(); - ctx.getState = vi.fn().mockReturnValue({ - project: { - present: { - data: { - manuscript: [ - { id: 's1', title: 'Ch 1', content: 'one two three' }, - { id: 's2', title: 'Ch 2', content: 'four five' }, - ], - }, - }, - }, - proForge: { currentRun: null }, - }); - const result = (await toolRegistry.execute('countWords', {}, ctx)) as Record; - expect(result['total']).toBe(5); - }); - - it('readAllSections returns metadata without content by default', async () => { - const ctx = makeContext(); - ctx.getState = vi.fn().mockReturnValue({ - project: { - present: { - data: { - manuscript: [ - { id: 's1', title: 'Ch 1', content: 'alpha beta', status: 'draft', act: 1 }, - ], - }, - }, - }, - proForge: { currentRun: null }, - }); - const result = (await toolRegistry.execute('readAllSections', {}, ctx)) as Array< - Record - >; - expect(Array.isArray(result)).toBe(true); - expect(result[0]!['content']).toBeUndefined(); - expect(result[0]!['wordCount']).toBe(2); - }); - - it('analyzePacing categorizes sections by word count relative to average', async () => { - const ctx = makeContext(); - // Section 1 has 3x the words of section 2 → should be flagged as slow - ctx.getState = vi.fn().mockReturnValue({ - project: { - present: { - data: { - manuscript: [ - { id: 's1', title: 'Long', content: 'word '.repeat(300) }, - { id: 's2', title: 'Short', content: 'a b c d e' }, - ], - }, - }, - }, - proForge: { currentRun: null }, - }); - const result = (await toolRegistry.execute('analyzePacing', {}, ctx)) as { - sections: Array<{ recommendedAction: string }>; - }; - expect(result.sections[0]!.recommendedAction).toBe('compress'); - }); -}); From 50aefb84e586e346f4a5d3f5dc4ce97686af8e75 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:00:23 +0200 Subject: [PATCH 2/3] fix(proforge): coerce stored embedding to Float32Array; align local typecheck with CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI Quality Gate caught a type error the local typecheck missed: proForgeMemoryBank.ts passed a `number[]` (persisted embedding) where `cosineSimilarity` expects `EmbeddingVector` (Float32Array). Coerce stored embeddings via `new Float32Array(...)`. Root cause of the local miss: `pnpm run typecheck` ran tsgo WITHOUT `--checkers 4`, while CI runs `--checkers 4`; the single-checker path did not surface the dynamic-import type error. Align the canonical `typecheck` script to the exact CI command so nothing slips past the gate: - `typecheck` → `tsgo --project tsconfig.tsgo.json --noEmit --checkers 4` (matches ci.yml) - add `typecheck:single` for the lighter single-checker variant (not gate-authoritative) - document the exact command in CLAUDE.md Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 2 +- package.json | 3 ++- services/proForge/proForgeMemoryBank.ts | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 23a82dad3..def38a27f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ pnpm run build # Production build to dist/ pnpm run smoke:prod # Headless mount check on dist/ (run AFTER build; catches prod-only crashes) pnpm run lint # Biome lint (--error-on-warnings — warnings fail like CI) pnpm run lint:fix # Biome auto-fix (lint + format) -pnpm run typecheck # TypeScript type check (tsgo --noEmit) +pnpm run typecheck # TypeScript type check — EXACT CI command (tsgo --project tsconfig.tsgo.json --noEmit --checkers 4). typecheck:single = lighter single-checker (may miss errors the gate catches; do not trust for the gate) pnpm run test # Vitest watch mode pnpm run test:run # Vitest single run (CI mode) pnpm run test:coverage # Vitest with V8 coverage (thresholds: lines 54%, branches 46%, functions 68%, statements 56%) diff --git a/package.json b/package.json index c04a6037c..52b1c83c8 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "build:pages": "vite build", "build:turbo": "turbo run build", "preview": "vite preview", - "typecheck": "tsgo --project tsconfig.tsgo.json --noEmit", + "typecheck": "tsgo --project tsconfig.tsgo.json --noEmit --checkers 4", + "typecheck:single": "tsgo --project tsconfig.tsgo.json --noEmit", "typecheck:parallel": "tsgo --project tsconfig.tsgo.json --noEmit --checkers 4", "typecheck:turbo": "turbo run typecheck", "lhci:assert": "lhci autorun --upload.target=temporary-public-storage", diff --git a/services/proForge/proForgeMemoryBank.ts b/services/proForge/proForgeMemoryBank.ts index d6c276644..76b9a4f64 100644 --- a/services/proForge/proForgeMemoryBank.ts +++ b/services/proForge/proForgeMemoryBank.ts @@ -141,7 +141,12 @@ export async function searchMemoryEntries( const qVec = await embedText(query); const scored = await Promise.all( entries.map(async (entry) => { - const eVec = entry.embedding ?? (await embedText(`${entry.key} ${entry.content}`)); + // QNBS-v3: stored embeddings are persisted as number[]; coerce to the Float32Array the + // similarity fn expects. Falls back to computing the embedding when none is stored. + const eVec = + entry.embedding != null + ? new Float32Array(entry.embedding) + : await embedText(`${entry.key} ${entry.content}`); const sim = cosineSimilarity(qVec, eVec); // QNBS-v3: hybrid blends semantic (0.7) with normalised keyword overlap (0.3). const score = mode === 'semantic' ? sim : 0.7 * sim + 0.3 * (scoreKeyword(entry) / maxKw); From adedb31a9e34034010a663970ee9b41ac71f59d6 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:11:26 +0200 Subject: [PATCH 3/3] fix(proforge): resolve all CodeAnt review comments + finish es/fr/it i18n Addresses every inline review comment on PR #106: - #2 proForgeHistoryStore: reset cached dbPromise on open error so a failed IDB open can be retried instead of poisoning the singleton. - #3 proForgeHistoryStore: resolve/reject on tx.oncomplete/onerror/onabort (not request.onsuccess) so a save only resolves once durably committed. - #4 applyReviewEdits: anchor stale-offset edits to the occurrence nearest the original range and skip already-claimed positions, so duplicate phrases map to distinct occurrences deterministically. - #5 baseAgent.gatherMemoryContext: feed a project-derived query (title/logline/genre) + ragMode into memory-bank retrieval instead of an undefined query; all 6 AI agents now route through it. - #6 PipelineReviewPanel: await submitReview before announce/navigate. - #7 useProForgeOrchestrator: always dispatch loadRunHistory (even []) so switching projects clears stale history. - #1 i18n: translate ~55 proforge.review.*/stageName.* keys to es/fr/it (were English fallback); rebuild bundles. Tests updated for the 4-arg gatherMemoryContext signature (diagnostic/structural/publishing) + duplicate-phrase edit cases. Local gates green: typecheck (--checkers 4), lint, full proForge suite. Co-Authored-By: Claude Opus 4.8 --- components/proForge/PipelineReviewPanel.tsx | 10 +- hooks/useProForgeOrchestrator.ts | 4 +- locales/es/common.json | 106 ++++++++--------- locales/fr/common.json | 98 ++++++++-------- locales/it/common.json | 108 +++++++++--------- public/locales/es/bundle.json | 106 ++++++++--------- public/locales/fr/bundle.json | 98 ++++++++-------- public/locales/it/bundle.json | 108 +++++++++--------- services/proForge/applyReviewEdits.ts | 40 ++++++- services/proForge/pipelineAgents/baseAgent.ts | 21 +++- .../proForge/pipelineAgents/copyEditAgent.ts | 2 +- .../pipelineAgents/diagnosticAgent.ts | 2 +- .../proForge/pipelineAgents/proofAgent.ts | 2 +- .../proForge/pipelineAgents/proseAgent.ts | 2 +- .../pipelineAgents/publishingAgent.ts | 2 +- .../pipelineAgents/structuralAgent.ts | 2 +- services/proForge/proForgeHistoryStore.ts | 17 ++- tests/unit/proForge/applyReviewEdits.test.ts | 21 ++++ .../pipelineAgents/diagnosticAgent.test.ts | 8 +- .../pipelineAgents/publishingAgent.test.ts | 8 +- .../pipelineAgents/structuralAgent.test.ts | 8 +- 21 files changed, 438 insertions(+), 335 deletions(-) diff --git a/components/proForge/PipelineReviewPanel.tsx b/components/proForge/PipelineReviewPanel.tsx index 0318a53c7..1e03b84d8 100644 --- a/components/proForge/PipelineReviewPanel.tsx +++ b/components/proForge/PipelineReviewPanel.tsx @@ -125,12 +125,14 @@ export const PipelineReviewPanel: React.FC = () => { (i) => i.confidence >= 0.85 && i.severity !== 'critical' && i.status === 'pending', ).length; - const handleSubmit = useCallback(() => { + const handleSubmit = useCallback(async () => { if (!stage) return; const items = activeStageResult?.reviewItems ?? []; const decisions = items.map((item) => ({ itemId: item.id, status: item.status })); - void submitReview(stage, decisions); - // QNBS-v3: WCAG live-region announce so screen-reader users hear the stage was submitted. + // QNBS-v3: Await completion before announcing/navigating — submitReview is async (applies + // edits + snapshots), so announcing success before it resolves could lie if it rejects. + await submitReview(stage, decisions); + // WCAG live-region announce so screen-reader users hear the stage was submitted. announce(t('proforge.review.announceSubmitted', { stage: stageLabel }), 'polite'); setActiveView('dashboard'); }, [stage, stageLabel, activeStageResult, submitReview, setActiveView, announce, t]); @@ -297,7 +299,7 @@ export const PipelineReviewPanel: React.FC = () => {