From 426da669d35a474df80ba0d5e31a0bf919eec915 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:18:47 +0200 Subject: [PATCH 1/4] feat(proforge): measured supervisor quality gates + review-required label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave B · deepens the ProForge supervisor (audit P0): scores were hard-coded constants and thresholds were fixed in code. - Measured scoring: replace the flat 80/85/88/90 pass-scores with a confidence score that scales with how much signal a stage produced relative to manuscript size (findings per ~1000 words), within a pass band. Fail/"suspect" scores now scale with manuscript size too. The supervisor still does NO AI calls — this is heuristic confidence, not editorial quality — but the score now actually varies with the work done instead of always reporting the same number. - Configurable thresholds: new QualityThresholds (largeManuscriptWords + intakeHardGate) on PipelineConfig, defaulted via DEFAULT_QUALITY_THRESHOLDS and threaded from run config → SupervisorAgent + the orchestrator's intake hard gate. - Dashboard: an explicit "Experimental — your review is required" line, so the human-in-the-loop expectation is stated, not just implied in help. i18n: proforge.pipeline.reviewRequired across all 19 locales + bundles. Tests: measured-score-varies-with-findings, configurable-threshold behaviour; existing exact-score assertions relaxed to pass-band ranges. Co-Authored-By: Claude Opus 4.8 --- components/proForge/ProForgeDashboard.tsx | 6 +- features/proForge/types.ts | 16 +++ graphify-out/GRAPH_REPORT.md | 132 +++++++++--------- locales/ar/common.json | 1 + locales/de/common.json | 1 + locales/el/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/eu/common.json | 1 + locales/fa/common.json | 1 + locales/fi/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/is/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/pt/common.json | 1 + locales/ru/common.json | 1 + locales/sv/common.json | 1 + locales/zh/common.json | 1 + public/locales/ar/bundle.json | 1 + public/locales/de/bundle.json | 1 + public/locales/el/bundle.json | 1 + public/locales/en/bundle.json | 1 + public/locales/es/bundle.json | 1 + public/locales/eu/bundle.json | 1 + public/locales/fa/bundle.json | 1 + public/locales/fi/bundle.json | 1 + public/locales/fr/bundle.json | 1 + public/locales/he/bundle.json | 1 + public/locales/hu/bundle.json | 1 + public/locales/is/bundle.json | 1 + public/locales/it/bundle.json | 1 + public/locales/ja/bundle.json | 1 + public/locales/ko/bundle.json | 1 + public/locales/pt/bundle.json | 1 + public/locales/ru/bundle.json | 1 + public/locales/sv/bundle.json | 1 + public/locales/zh/bundle.json | 1 + .../pipelineAgents/supervisorAgent.ts | 116 +++++++++++---- services/proForge/proForgeOrchestrator.ts | 16 ++- .../pipelineAgents/supervisorAgent.test.ts | 65 +++++++-- 44 files changed, 277 insertions(+), 112 deletions(-) diff --git a/components/proForge/ProForgeDashboard.tsx b/components/proForge/ProForgeDashboard.tsx index 964d4491..846b4e47 100644 --- a/components/proForge/ProForgeDashboard.tsx +++ b/components/proForge/ProForgeDashboard.tsx @@ -86,11 +86,15 @@ export const ProForgeDashboard: React.FC = () => { P
-
+

{t('proforge.pipeline.title')}

{/* QNBS-v3: ProForge is an on-by-default but experimental agentic pipeline — label it. */} {t('common.badge.experimental')}
+ {/* QNBS-v3: PR6 — make the human-in-the-loop expectation explicit, not just implied in help. */} +

+ {t('proforge.pipeline.reviewRequired')} +

{currentRun ? currentRun.label : t('proforge.pipeline.noneActive')}

diff --git a/features/proForge/types.ts b/features/proForge/types.ts index 042e92df..50be02e2 100644 --- a/features/proForge/types.ts +++ b/features/proForge/types.ts @@ -95,8 +95,23 @@ export interface PipelineConfig { language: string; /** Max supervisor-triggered retries per stage (0 = no retry, 1 = one retry) */ maxRetries?: 0 | 1; + /** Tunable supervisor quality-gate thresholds (defaults applied when omitted). */ + qualityThresholds?: QualityThresholds; } +/** Tunable thresholds for the heuristic supervisor quality gates. */ +export interface QualityThresholds { + /** Word count above which a stage producing zero edits/findings is treated as a fallback signal. */ + largeManuscriptWords: number; + /** Intake qualityScore below this hard-fails the run. */ + intakeHardGate: number; +} + +export const DEFAULT_QUALITY_THRESHOLDS: QualityThresholds = { + largeManuscriptWords: 1000, + intakeHardGate: 30, +}; + export const DEFAULT_PIPELINE_CONFIG: PipelineConfig = { genrePreset: 'general-fiction', selectedStages: [ @@ -117,6 +132,7 @@ export const DEFAULT_PIPELINE_CONFIG: PipelineConfig = { autoAcceptThreshold: 0, language: 'en', maxRetries: 1, + qualityThresholds: DEFAULT_QUALITY_THRESHOLDS, }; // --------------------------------------------------------------------------- diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index 56710e2c..33047553 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,12 +1,12 @@ # Graph Report - StoryCraft-Studio (2026-06-23) ## Corpus Check -- 1162 files · ~1,304,844 words +- 1162 files · ~1,305,358 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 5025 nodes · 8812 edges · 85 communities detected -- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 2157 edges (avg confidence: 0.8) +- 5027 nodes · 8823 edges · 85 communities detected +- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 2158 edges (avg confidence: 0.8) - Token cost: 0 input · 0 output ## Community Hubs (Navigation) @@ -56,7 +56,7 @@ - [[_COMMUNITY_Community 62|Community 62]] - [[_COMMUNITY_Community 63|Community 63]] - [[_COMMUNITY_Community 64|Community 64]] -- [[_COMMUNITY_Community 68|Community 68]] +- [[_COMMUNITY_Community 65|Community 65]] - [[_COMMUNITY_Community 70|Community 70]] - [[_COMMUNITY_Community 71|Community 71]] - [[_COMMUNITY_Community 77|Community 77]] @@ -111,136 +111,136 @@ ## Surprising Connections (you probably didn't know these) - `useTranslation()` --calls--> `IdbUnlockModal()` [INFERRED] hooks/useTranslation.ts → components/settings/IdbUnlockModal.tsx -- `getItem()` --calls--> `isLoggerEnabled()` [INFERRED] - features/featureFlags/featureFlagsStorage.ts → app/store.ts - `getItem()` --calls--> `readMode()` [INFERRED] features/featureFlags/featureFlagsStorage.ts → components/copilot/CopilotPanel.tsx - `setItem()` --calls--> `writeMode()` [INFERRED] features/featureFlags/featureFlagsStorage.ts → components/copilot/CopilotPanel.tsx - `setItem()` --calls--> `enableDebugLogging()` [INFERRED] features/featureFlags/featureFlagsStorage.ts → services/logger.ts +- `removeItem()` --calls--> `disableDebugLogging()` [INFERRED] + features/featureFlags/featureFlagsStorage.ts → services/logger.ts ## Communities ### Community 0 - "Community 0" Cohesion: 0.01 -Nodes (323): flushMicrotasks(), _0, _2(), A0, a2(), aA(), ac(), ad() (+315 more) +Nodes (312): md(), _0, _2(), A0, a2(), ab(), ac(), ad() (+304 more) ### Community 1 - "Community 1" Cohesion: 0.01 -Nodes (174): af(), ef(), ff(), Ja(), lf(), mt(), nf(), of() (+166 more) +Nodes (193): af(), ef(), ff(), Ja(), lf(), mt(), nf(), of() (+185 more) ### Community 2 - "Community 2" Cohesion: 0.01 -Nodes (135): recordLatency(), AiInferenceCacheService, hashKey(), _cleanupPendingRequest(), _clearPendingRequestsForTest(), _deduplicateRequest(), collectSubtreeIds(), buildConsistencyHints() (+127 more) +Nodes (92): pipeline(), pipeline(), isEcoMode(), flushMicrotasks(), decrypt(), decryptJson(), encrypt(), encryptJson() (+84 more) ### Community 3 - "Community 3" Cohesion: 0.01 -Nodes (92): accessibilityPresetDefaults(), normalizeAccessibilitySettings(), applyPreset(), loadAgent(), analyticsPersistenceAllowedNow(), isAnalyticsPersistenceAllowed(), handleRemoveKey(), handleSaveKey() (+84 more) +Nodes (96): handleCopyForNotion(), handleDocxImport(), handleExport(), handlePasteImport(), loadAgent(), handleBuildLocalRag(), handleWebllmDownload(), isCustomOllamaModel() (+88 more) ### Community 4 - "Community 4" -Cohesion: 0.01 -Nodes (78): handleCopyForNotion(), handleDocxImport(), handleExport(), handlePasteImport(), binderDepth(), handleAddFolder(), handleAddLink(), handleAddNote() (+70 more) +Cohesion: 0.02 +Nodes (96): recordLatency(), AiInferenceCacheService, hashKey(), _clearPendingRequestsForTest(), collectSubtreeIds(), buildConsistencyHints(), buildEntityId(), buildRelationshipEdges() (+88 more) ### Community 5 - "Community 5" Cohesion: 0.02 -Nodes (32): hasMigrationMarker(), legacyDatabaseListed(), migrateLegacyWorldscriptDbIfNeeded(), openLegacyDatabase(), promisifyRequest(), readAllFromStore(), setMigrationMarker(), stateDbHasProjectOrSettings() (+24 more) +Nodes (90): getActiveAiMode(), getOpenRouterFallbackProvider(), getOpenRouterModel(), isCloudOnlyMode(), isOffline(), notifyLocalModelsReady(), shouldRouteLocally(), shouldUseOpenRouter() (+82 more) ### Community 6 - "Community 6" -Cohesion: 0.02 -Nodes (59): pipeline(), pipeline(), isEcoMode(), applyPreset(), async(), close(), isSidebar(), onKey() (+51 more) +Cohesion: 0.03 +Nodes (21): a_(), aA(), bh, Dh(), eA(), el(), GE(), Gh() (+13 more) ### Community 7 - "Community 7" Cohesion: 0.02 -Nodes (68): FsAssetStore, glossaryTranslate(), loadCheckpoint(), loadGlossary(), main(), maskPlaceholders(), parseArgs(), restorePlaceholders() (+60 more) +Nodes (73): FsAssetStore, glossaryTranslate(), loadCheckpoint(), loadGlossary(), main(), maskPlaceholders(), parseArgs(), restorePlaceholders() (+65 more) ### Community 8 - "Community 8" -Cohesion: 0.03 -Nodes (80): getActiveAiMode(), getLocalFallbackModel(), getOpenRouterFallbackProvider(), getOpenRouterModel(), isCloudOnlyMode(), isOffline(), notifyLocalModelsReady(), shouldRouteLocally() (+72 more) +Cohesion: 0.01 +Nodes (78): categoryFromMessage(), categoryFromStatus(), classificationFor(), classifyAiError(), extractStatus(), getAiErrorMessage(), isOffline(), clampRetryAfter() (+70 more) ### Community 9 - "Community 9" Cohesion: 0.02 -Nodes (61): getFocusable(), onKeyDown(), onPointerUp(), handler(), decrypt(), decryptJson(), encrypt(), encryptJson() (+53 more) +Nodes (65): accessibilityPresetDefaults(), normalizeAccessibilitySettings(), applyPreset(), loadStoryCodex(), DeadLetterQueue, openDlqDb(), storeClear(), storeGetAll() (+57 more) ### Community 10 - "Community 10" -Cohesion: 0.01 -Nodes (67): makeContext(), makeContext(), renderSheet(), makeDeps(), renderPanel(), makeStoreState(), createFakeAdapter(), createFakeDevice() (+59 more) +Cohesion: 0.02 +Nodes (64): getFocusable(), onKeyDown(), onPointerUp(), applyPreset(), async(), close(), isSidebar(), onKey() (+56 more) ### Community 11 - "Community 11" -Cohesion: 0.03 -Nodes (77): handleBuildLocalRag(), handleWebllmDownload(), isCustomOllamaModel(), countWords(), enrichProjectIndex(), extractCharacterNames(), getDb(), indexProject() (+69 more) +Cohesion: 0.02 +Nodes (62): AnalyticsBootstrap(), App(), ViewLoader(), useCommandExecutor(), CopilotLauncher(), Header(), useAppDispatch(), useAppSelectorShallow() (+54 more) ### Community 12 - "Community 12" Cohesion: 0.02 -Nodes (63): AnalyticsBootstrap(), App(), ViewLoader(), BookPreviewView(), useCommandExecutor(), CopilotLauncher(), Header(), useAppDispatch() (+55 more) +Nodes (57): item(), BookPreviewView(), getLocalUser(), getRandomColor(), handleKeyDown(), sanitizeRoomInput(), stripControlChars(), deleteIdb() (+49 more) ### Community 13 - "Community 13" -Cohesion: 0.02 -Nodes (76): AiModeIndicator(), item(), getLocalUser(), getRandomColor(), handleKeyDown(), sanitizeRoomInput(), stripControlChars(), deleteIdb() (+68 more) +Cohesion: 0.03 +Nodes (67): countWords(), enrichProjectIndex(), extractCharacterNames(), getDb(), indexProject(), listIndexedProjects(), removeProjectIndex(), semanticSearchProjects() (+59 more) ### Community 14 - "Community 14" -Cohesion: 0.03 -Nodes (44): assertNoSeriousViolations(), navigateToCollaborationSettings(), md(), connectSrcTokens(), group1(), tauriCsp(), webCsp(), ab() (+36 more) +Cohesion: 0.05 +Nodes (48): getLocalFallbackModel(), generateJson(), attachCause(), cleanPrompt(), sanitizePromptBlock(), stripControlChars(), stripJsonFences(), AnalyticsAgent (+40 more) ### Community 15 - "Community 15" Cohesion: 0.03 -Nodes (57): AdaptiveAiEngine, _clearLatencyHistory(), estimateLatency(), getTaskConfig(), selectModelForBackend(), start(), clearBenchmarkResults(), getLastBenchmarkResults() (+49 more) +Nodes (45): AudioNavigator, buildEncodedPayload(), makeCommands(), makeProjectData(), Hb(), makeContext(), makeLargeContext(), makeSection() (+37 more) ### Community 16 - "Community 16" -Cohesion: 0.03 -Nodes (41): AudioNavigator, buildEncodedPayload(), makeCommands(), makeProjectData(), Hb(), makeContext(), makeLargeContext(), makeSection() (+33 more) +Cohesion: 0.04 +Nodes (34): CollabEncryptionRequiredError, CollaborationService, resolveWebRtcSignalingUrls(), MockDoc, MockWebrtcProvider, createAttentionPipeline(), createComputePipeline(), createKvCachePipeline() (+26 more) ### Community 17 - "Community 17" Cohesion: 0.04 -Nodes (37): applyTextEdit(), applyReviewEditsToSection(), containsDisallowedControlChar(), isValidRange(), nearestFreeOccurrence(), planAcceptedManuscriptEdits(), validateProposedText(), mockT() (+29 more) +Nodes (19): createCancellationToken(), InferenceProgressEmitter, sendMessage(), handleTellMore(), handleCancel(), handleRetry(), abortActivePreload(), detectOnnxExecutionProviders() (+11 more) ### Community 18 - "Community 18" -Cohesion: 0.05 -Nodes (22): CollabEncryptionRequiredError, CollaborationService, resolveWebRtcSignalingUrls(), MockDoc, MockWebrtcProvider, createAttentionPipeline(), createComputePipeline(), createKvCachePipeline() (+14 more) +Cohesion: 0.04 +Nodes (36): assertNoSeriousViolations(), navigateToCollaborationSettings(), connectSrcTokens(), group1(), tauriCsp(), webCsp(), clickNavItem(), ensureBlankProject() (+28 more) ### Community 19 - "Community 19" -Cohesion: 0.07 -Nodes (5): createCancellationToken(), PriorityTaskQueue, WorkerBus, shutdownWorkerBus(), WorkerPool +Cohesion: 0.11 +Nodes (10): bb(), cc, d_, f_, Gb(), h_, mc(), r0() (+2 more) ### Community 20 - "Community 20" -Cohesion: 0.07 -Nodes (29): check(), extractCatalogFlags(), extractHiddenFlags(), extractSectionFlags(), green(), grep(), hasRuntimeConsumption(), read() (+21 more) +Cohesion: 0.06 +Nodes (33): collect(), buildPaletteCommandModels(), collectAllDefinitions(), resolveTitle(), runCommandById(), id, install_app_menu(), run() (+25 more) ### Community 21 - "Community 21" -Cohesion: 0.07 -Nodes (13): createBrowserProForgeCapability(), buildPorts(), runCopilotDiagnostic(), buildNormManuscriptExport(), paginateNormLines(), stripLightMarkdown(), wrapParagraphToLines(), wrapPlainTextToNormLines() (+5 more) +Cohesion: 0.06 +Nodes (22): applyTextEdit(), applyReviewEditsToSection(), containsDisallowedControlChar(), isValidRange(), nearestFreeOccurrence(), planAcceptedManuscriptEdits(), validateProposedText(), mockT() (+14 more) ### Community 22 - "Community 22" -Cohesion: 0.08 -Nodes (27): collect(), install_app_menu(), run(), abort_lora_training(), check_lora_environment(), LoraEnvReport, LoraTrainPayload, merge_lora() (+19 more) +Cohesion: 0.07 +Nodes (30): AdaptiveAiEngine, _clearLatencyHistory(), estimateLatency(), getTaskConfig(), selectModelForBackend(), clearBenchmarkResults(), getLastBenchmarkResults(), loadResults() (+22 more) ### Community 23 - "Community 23" +Cohesion: 0.07 +Nodes (13): createBrowserProForgeCapability(), buildPorts(), runCopilotDiagnostic(), buildNormManuscriptExport(), paginateNormLines(), stripLightMarkdown(), wrapParagraphToLines(), wrapPlainTextToNormLines() (+5 more) + +### Community 24 - "Community 24" Cohesion: 0.1 Nodes (1): StorageManager -### Community 24 - "Community 24" +### Community 25 - "Community 25" Cohesion: 0.23 Nodes (3): LS, xn(), aa -### Community 25 - "Community 25" -Cohesion: 0.1 -Nodes (11): handleEvaluate(), ScoreGauge(), comparePromptOutputs(), computeStyleConsistencyScore(), cosineSimilarity(), getEmbeddingService(), meanSimilarity(), scoreLabel() (+3 more) - ### Community 26 - "Community 26" -Cohesion: 0.14 -Nodes (21): handleToggle(), handleDelete(), handleFileChange(), activateAdapter(), clearDatasetEntries(), deactivateAdapter(), deleteAdapter(), exportAdapter() (+13 more) +Cohesion: 0.16 +Nodes (23): AiModeIndicator(), isOpenRouterFreeModel(), buildHeaders(), buildMessages(), buildRequestBody(), computeBackoffMs(), delay(), _delayProvider() (+15 more) ### Community 27 - "Community 27" Cohesion: 0.14 -Nodes (15): normalize(), buildExcerpt(), extractCharacters(), extractManuscriptSections(), searchAcrossProjectIndex(), searchAcrossProjects(), normalizeSearch(), scoreAgainstQuery() (+7 more) +Nodes (21): handleToggle(), handleDelete(), handleFileChange(), activateAdapter(), clearDatasetEntries(), deactivateAdapter(), deleteAdapter(), exportAdapter() (+13 more) ### Community 28 - "Community 28" -Cohesion: 0.35 -Nodes (2): cc, Gb() +Cohesion: 0.14 +Nodes (15): normalize(), buildExcerpt(), extractCharacters(), extractManuscriptSections(), searchAcrossProjectIndex(), searchAcrossProjects(), normalizeSearch(), scoreAgainstQuery() (+7 more) ### Community 29 - "Community 29" -Cohesion: 0.2 -Nodes (14): categoryFromMessage(), categoryFromStatus(), classificationFor(), classifyAiError(), extractStatus(), getAiErrorMessage(), isOffline(), clampRetryAfter() (+6 more) +Cohesion: 0.17 +Nodes (8): check(), extractCatalogFlags(), extractHiddenFlags(), extractSectionFlags(), green(), grep(), hasRuntimeConsumption(), red() ### Community 32 - "Community 32" Cohesion: 0.42 @@ -304,11 +304,11 @@ Nodes (3): emptyChars(), emptyWorlds(), makeProject() ### Community 64 - "Community 64" Cohesion: 0.5 -Nodes (3): AsyncDuckDB, AsyncDuckDBConnection, ConsoleLogger +Nodes (2): ManuscriptDesktopLayout(), useManuscriptViewContext() -### Community 68 - "Community 68" +### Community 65 - "Community 65" Cohesion: 0.5 -Nodes (2): ManuscriptDesktopLayout(), useManuscriptViewContext() +Nodes (3): AsyncDuckDB, AsyncDuckDBConnection, ConsoleLogger ### Community 70 - "Community 70" Cohesion: 0.67 @@ -465,9 +465,7 @@ Nodes (1): Parse Stryker JSON report for surviving mutants. ## Knowledge Gaps - **53 isolated node(s):** `Emits JSON progress events on each training log step.`, `qb`, `v2`, `MockIntersectionObserver`, `MockWorker` (+48 more) These have ≤1 connection - possible missing edges or undocumented components. -- **Thin community `Community 23`** (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 28`** (17 nodes): `cc`, `._applyAttribute()`, `._assert()`, `.constructor()`, `._eof()`, `._isWhitespace()`, `._next()`, `.parse()`, `._peek()`, `._readAttributes()`, `._readIdentifier()`, `._readRegex()`, `._readString()`, `._readStringOrRegex()`, `._skipWhitespace()`, `._throwError()`, `Gb()` +- **Thin community `Community 24`** (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 45`** (5 nodes): `DashboardHeader.tsx`, `DashboardContext.ts`, `useDashboardContext()`, `Chip()`, `DashboardHeader()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. @@ -477,7 +475,7 @@ Nodes (1): Parse Stryker JSON report for surviving mutants. Too small to be a meaningful cluster - may be noise or needs more connections extracted. - **Thin community `Community 60`** (4 nodes): `make()`, `noop()`, `aiRetry.test.ts`, `aiRetry.test.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 68`** (4 nodes): `ManuscriptDesktopLayout.tsx`, `ManuscriptViewContext.ts`, `ManuscriptDesktopLayout()`, `useManuscriptViewContext()` +- **Thin community `Community 64`** (4 nodes): `ManuscriptDesktopLayout.tsx`, `ManuscriptViewContext.ts`, `ManuscriptDesktopLayout()`, `useManuscriptViewContext()` Too small to be a meaningful cluster - may be noise or needs more connections extracted. - **Thin community `Community 70`** (4 nodes): `getAllTemplates()`, `getQuestionsForArchetype()`, `getTemplateForArchetype()`, `characterInterviewTemplates.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. @@ -557,12 +555,12 @@ Nodes (1): Parse Stryker JSON report for surviving mutants. ## Suggested Questions _Questions this graph is uniquely positioned to answer:_ -- **Why does `mt()` connect `Community 1` to `Community 0`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 21`, `Community 24`?** - _High betweenness centrality (0.079) - this node is a cross-community bridge._ -- **Why does `t()` connect `Community 4` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 6`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 26`, `Community 29`?** - _High betweenness centrality (0.047) - this node is a cross-community bridge._ -- **Why does `wx()` connect `Community 0` to `Community 1`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 14`?** - _High betweenness centrality (0.035) - this node is a cross-community bridge._ +- **Why does `mt()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 6`, `Community 10`, `Community 14`, `Community 15`, `Community 17`, `Community 21`, `Community 23`, `Community 25`?** + _High betweenness centrality (0.077) - this node is a cross-community bridge._ +- **Why does `t()` connect `Community 3` to `Community 1`, `Community 2`, `Community 4`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 17`, `Community 20`, `Community 26`, `Community 27`?** + _High betweenness centrality (0.048) - this node is a cross-community bridge._ +- **Why does `wx()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 6`, `Community 7`, `Community 10`, `Community 16`?** + _High betweenness centrality (0.038) - this node is a cross-community bridge._ - **Are the 87 inferred relationships involving `mt()` (e.g. with `pE()` and `xE()`) actually correct?** _`mt()` has 87 INFERRED edges - model-reasoned connections that need verification._ - **Are the 62 inferred relationships involving `fn()` (e.g. with `makeMediaQuery()` and `MockSpeechRecognition()`) actually correct?** diff --git a/locales/ar/common.json b/locales/ar/common.json index ce1949a0..7ded4a12 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "جارٍ التحضير للعالم…", "proforge.loading.structural": "جارٍ النظر في شكل قصتك…", "proforge.pipeline.noneActive": "لا يوجد خط أنابيب نشط", + "proforge.pipeline.reviewRequired": "تجريبي — مراجعتك مطلوبة؛ لا يتم تغيير المخطوطة أبدًا دون موافقتك.", "proforge.pipeline.title": "خط أنابيب المؤلّف الأمثل", "proforge.progress.activeStage": "Active Stage", "proforge.progress.awaitingReviewHint": "⚠️ This stage is awaiting your review. Click the stage button above to review items.", diff --git a/locales/de/common.json b/locales/de/common.json index c336dc2d..b677735d 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Macht sich für die Welt bereit…", "proforge.loading.structural": "Betrachtet die Struktur deiner Geschichte…", "proforge.pipeline.noneActive": "Keine aktive Pipeline", + "proforge.pipeline.reviewRequired": "Experimentell — deine Prüfung ist erforderlich; das Manuskript wird nie ohne deine Zustimmung geändert.", "proforge.pipeline.title": "Ultimative Autoren-Pipeline", "proforge.progress.activeStage": "Aktive Phase", "proforge.progress.awaitingReviewHint": "⚠️ Diese Phase wartet auf Ihre Prüfung. Klicken Sie oben auf die Phasenschaltfläche, um die Einträge zu prüfen.", diff --git a/locales/el/common.json b/locales/el/common.json index 26975c35..88c9cce1 100644 --- a/locales/el/common.json +++ b/locales/el/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Προετοιμασία για τον κόσμο…", "proforge.loading.structural": "Κοιτάζοντας τη μορφή της ιστορίας σας…", "proforge.pipeline.noneActive": "Δεν υπάρχει ενεργός αγωγός", + "proforge.pipeline.reviewRequired": "Πειραματικό — απαιτείται η αξιολόγησή σας· το χειρόγραφο δεν αλλάζει ποτέ χωρίς την έγκρισή σας.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Ενεργό Στάδιο", "proforge.progress.awaitingReviewHint": "⚠️ Αυτό το στάδιο περιμένει την κριτική σας. Κάντε κλικ στο κουμπί του σταδίου παραπάνω για να ελέγξετε τα στοιχεία.", diff --git a/locales/en/common.json b/locales/en/common.json index 6b841689..1cde8de6 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -531,6 +531,7 @@ "proforge.loading.analytics": "Summing up the run…", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.pipeline.noneActive": "No active pipeline", + "proforge.pipeline.reviewRequired": "Experimental — your review is required; the manuscript is never changed without your approval.", "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.", diff --git a/locales/es/common.json b/locales/es/common.json index 8e440f6d..1cbb3d70 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Preparándose para el mundo…", "proforge.loading.structural": "Observando la forma de tu historia…", "proforge.pipeline.noneActive": "Sin pipeline activa", + "proforge.pipeline.reviewRequired": "Experimental: se requiere tu revisión; el manuscrito nunca se modifica sin tu aprobación.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Etapa activa", "proforge.progress.awaitingReviewHint": "⚠️ Esta etapa está esperando tu revisión. Haz clic en el botón de la etapa de arriba para revisar los elementos.", diff --git a/locales/eu/common.json b/locales/eu/common.json index 3ff77923..1929d51f 100644 --- a/locales/eu/common.json +++ b/locales/eu/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Mundurako prestatzen...", "proforge.loading.structural": "Zure istorioaren formari begira...", "proforge.pipeline.noneActive": "Ez dago kanalizazio aktiborik", + "proforge.pipeline.reviewRequired": "Esperimentala — zure berrikuspena beharrezkoa da; eskuizkribua ez da inoiz aldatzen zure oniritzirik gabe.", "proforge.pipeline.title": "Azken egilearen kanalizazioa", "proforge.progress.activeStage": "Etapa Aktiboa", "proforge.progress.awaitingReviewHint": "⚠️ Etapa hau zure iritziaren zain dago. Egin klik goiko faseko botoian elementuak berrikusteko.", diff --git a/locales/fa/common.json b/locales/fa/common.json index dff4b6b9..6dba26ba 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "آماده شدن برای جهان…", "proforge.loading.structural": "با نگاه کردن به شکل داستان شما…", "proforge.pipeline.noneActive": "بدون خط لوله فعال", + "proforge.pipeline.reviewRequired": "آزمایشی — بازبینی شما لازم است؛ نسخه هرگز بدون تأیید شما تغییر نمی‌کند.", "proforge.pipeline.title": "خط لوله نویسنده نهایی", "proforge.progress.activeStage": "مرحله فعال", "proforge.progress.awaitingReviewHint": "⚠️ این مرحله منتظر بررسی شماست. برای بررسی موارد روی دکمه مرحله بالا کلیک کنید.", diff --git a/locales/fi/common.json b/locales/fi/common.json index 4c06d119..bd55d3dd 100644 --- a/locales/fi/common.json +++ b/locales/fi/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Valmistautuminen maailmaan…", "proforge.loading.structural": "Tarinasi muotoa katsoessani…", "proforge.pipeline.noneActive": "Ei aktiivista putkistoa", + "proforge.pipeline.reviewRequired": "Kokeellinen — tarkistuksesi vaaditaan; käsikirjoitusta ei koskaan muuteta ilman hyväksyntääsi.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Aktiivinen vaihe", "proforge.progress.awaitingReviewHint": "⚠️ Tämä vaihe odottaa arvosteluasi. Napsauta yllä olevaa vaihepainiketta tarkastellaksesi kohteita.", diff --git a/locales/fr/common.json b/locales/fr/common.json index e0564a32..7e801503 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Prêt pour le monde…", "proforge.loading.structural": "Examen de la structure de votre histoire…", "proforge.pipeline.noneActive": "Aucune pipeline active", + "proforge.pipeline.reviewRequired": "Expérimental — votre relecture est requise ; le manuscrit n'est jamais modifié sans votre accord.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Étape active", "proforge.progress.awaitingReviewHint": "⚠️ Cette étape attend votre relecture. Cliquez sur le bouton de l'étape ci-dessus pour examiner les éléments.", diff --git a/locales/he/common.json b/locales/he/common.json index bbc96cbd..4b35acc5 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "מתכונן לעולם…", "proforge.loading.structural": "מתבונן במבנה הסיפור שלכם…", "proforge.pipeline.noneActive": "אין צינור פעיל", + "proforge.pipeline.reviewRequired": "ניסיוני — נדרשת הבדיקה שלך; כתב היד לעולם לא משתנה ללא אישורך.", "proforge.pipeline.title": "צינור הסופר האולטימטיבי", "proforge.progress.activeStage": "Active Stage", "proforge.progress.awaitingReviewHint": "⚠️ This stage is awaiting your review. Click the stage button above to review items.", diff --git a/locales/hu/common.json b/locales/hu/common.json index f7d040e9..512db585 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Felkészülés a világra…", "proforge.loading.structural": "Elnézve a történeted alakját…", "proforge.pipeline.noneActive": "Nincs aktív csővezeték", + "proforge.pipeline.reviewRequired": "Kísérleti — szükség van az átnézésedre; a kézirat soha nem módosul a jóváhagyásod nélkül.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Aktív szakasz", "proforge.progress.awaitingReviewHint": "⚠️ Ez a szakasz az értékelésedre vár. Kattintson a fenti szakasz gombra az elemek áttekintéséhez.", diff --git a/locales/is/common.json b/locales/is/common.json index 74a80f34..244bbe37 100644 --- a/locales/is/common.json +++ b/locales/is/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Undirbúningur fyrir heiminn…", "proforge.loading.structural": "Þegar þú horfir á lögun sögunnar þinnar...", "proforge.pipeline.noneActive": "Engin virk leiðsla", + "proforge.pipeline.reviewRequired": "Tilraunaverkefni — yfirferð þín er nauðsynleg; handritinu er aldrei breytt án samþykkis þíns.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Virkt stig", "proforge.progress.awaitingReviewHint": "⚠️ Þetta stig bíður skoðunar þinnar. Smelltu á sviðshnappinn hér að ofan til að skoða atriði.", diff --git a/locales/it/common.json b/locales/it/common.json index d65a3d94..770cb0de 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Pronto per il mondo…", "proforge.loading.structural": "Osservando la forma della tua storia…", "proforge.pipeline.noneActive": "Nessuna pipeline attiva", + "proforge.pipeline.reviewRequired": "Sperimentale — è richiesta la tua revisione; il manoscritto non viene mai modificato senza la tua approvazione.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Fase attiva", "proforge.progress.awaitingReviewHint": "⚠️ Questa fase è in attesa della tua revisione. Fai clic sul pulsante della fase qui sopra per esaminare gli elementi.", diff --git a/locales/ja/common.json b/locales/ja/common.json index 5f5995c6..6af0fd10 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "世界に向けて準備中…", "proforge.loading.structural": "あなたの物語の形を見てみると…", "proforge.pipeline.noneActive": "アクティブなパイプラインがありません", + "proforge.pipeline.reviewRequired": "実験的 — あなたのレビューが必要です。承認なしに原稿が変更されることはありません。", "proforge.pipeline.title": "究極の著者パイプライン", "proforge.progress.activeStage": "アクティブステージ", "proforge.progress.awaitingReviewHint": "⚠️ このステージはあなたのレビューを待っています。項目を確認するには、上のステージ ボタンをクリックしてください。", diff --git a/locales/ko/common.json b/locales/ko/common.json index b04c290d..37230436 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "세상을 준비하는…", "proforge.loading.structural": "이야기의 형태를 살펴보면…", "proforge.pipeline.noneActive": "활성 파이프라인 없음", + "proforge.pipeline.reviewRequired": "실험적 기능 — 검토가 필요합니다. 승인 없이 원고가 변경되지 않습니다.", "proforge.pipeline.title": "궁극적인 작성자 파이프라인", "proforge.progress.activeStage": "액티브 스테이지", "proforge.progress.awaitingReviewHint": "⚠️ 이 단계는 귀하의 검토를 기다리고 있습니다. 항목을 검토하려면 위의 단계 버튼을 클릭하세요.", diff --git a/locales/pt/common.json b/locales/pt/common.json index 97c13724..6a9f77c2 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Preparando-se para o mundo…", "proforge.loading.structural": "Olhando para a forma da sua história…", "proforge.pipeline.noneActive": "Nenhum pipeline ativo", + "proforge.pipeline.reviewRequired": "Experimental — a sua revisão é necessária; o manuscrito nunca é alterado sem a sua aprovação.", "proforge.pipeline.title": "Pipeline final do autor", "proforge.progress.activeStage": "Estágio Ativo", "proforge.progress.awaitingReviewHint": "⚠️ Esta etapa aguarda sua análise. Clique no botão de estágio acima para revisar os itens.", diff --git a/locales/ru/common.json b/locales/ru/common.json index a7b4a0f2..480881ab 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Готовимся к миру…", "proforge.loading.structural": "Глядя на форму вашей истории…", "proforge.pipeline.noneActive": "Нет активного конвейера", + "proforge.pipeline.reviewRequired": "Экспериментально — требуется ваша проверка; рукопись никогда не изменяется без вашего одобрения.", "proforge.pipeline.title": "Конечный авторский конвейер", "proforge.progress.activeStage": "Активная стадия", "proforge.progress.awaitingReviewHint": "⚠️Этот этап ожидает вашего рассмотрения. Нажмите кнопку этапа выше, чтобы просмотреть элементы.", diff --git a/locales/sv/common.json b/locales/sv/common.json index e65e4dfa..b87b2656 100644 --- a/locales/sv/common.json +++ b/locales/sv/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "Förbereder sig för världen...", "proforge.loading.structural": "Titta på din berättelses form...", "proforge.pipeline.noneActive": "Ingen aktiv pipeline", + "proforge.pipeline.reviewRequired": "Experimentell — din granskning krävs; manuskriptet ändras aldrig utan ditt godkännande.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Aktiv scen", "proforge.progress.awaitingReviewHint": "⚠️ Det här steget väntar på din recension. Klicka på scenknappen ovan för att granska objekt.", diff --git a/locales/zh/common.json b/locales/zh/common.json index f2dd521e..1a54a2ba 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -327,6 +327,7 @@ "proforge.loading.publishing": "为世界做好准备……", "proforge.loading.structural": "看看你的故事的形状……", "proforge.pipeline.noneActive": "无活动管道", + "proforge.pipeline.reviewRequired": "实验性功能——需要您审阅;未经您批准,稿件绝不会被修改。", "proforge.pipeline.title": "最终作者管道", "proforge.progress.activeStage": "活跃阶段", "proforge.progress.awaitingReviewHint": "⚠️此阶段正在等待您的审核。单击上面的阶段按钮以查看项目。", diff --git a/public/locales/ar/bundle.json b/public/locales/ar/bundle.json index 4986cb7a..e0005567 100644 --- a/public/locales/ar/bundle.json +++ b/public/locales/ar/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "جارٍ التحضير للعالم…", "proforge.loading.structural": "جارٍ النظر في شكل قصتك…", "proforge.pipeline.noneActive": "لا يوجد خط أنابيب نشط", + "proforge.pipeline.reviewRequired": "تجريبي — مراجعتك مطلوبة؛ لا يتم تغيير المخطوطة أبدًا دون موافقتك.", "proforge.pipeline.title": "خط أنابيب المؤلّف الأمثل", "proforge.progress.activeStage": "Active Stage", "proforge.progress.awaitingReviewHint": "⚠️ This stage is awaiting your review. Click the stage button above to review items.", diff --git a/public/locales/de/bundle.json b/public/locales/de/bundle.json index 3771cca2..413b6271 100644 --- a/public/locales/de/bundle.json +++ b/public/locales/de/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Macht sich für die Welt bereit…", "proforge.loading.structural": "Betrachtet die Struktur deiner Geschichte…", "proforge.pipeline.noneActive": "Keine aktive Pipeline", + "proforge.pipeline.reviewRequired": "Experimentell — deine Prüfung ist erforderlich; das Manuskript wird nie ohne deine Zustimmung geändert.", "proforge.pipeline.title": "Ultimative Autoren-Pipeline", "proforge.progress.activeStage": "Aktive Phase", "proforge.progress.awaitingReviewHint": "⚠️ Diese Phase wartet auf Ihre Prüfung. Klicken Sie oben auf die Phasenschaltfläche, um die Einträge zu prüfen.", diff --git a/public/locales/el/bundle.json b/public/locales/el/bundle.json index f6680e51..aa6c1cce 100644 --- a/public/locales/el/bundle.json +++ b/public/locales/el/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Προετοιμασία για τον κόσμο…", "proforge.loading.structural": "Κοιτάζοντας τη μορφή της ιστορίας σας…", "proforge.pipeline.noneActive": "Δεν υπάρχει ενεργός αγωγός", + "proforge.pipeline.reviewRequired": "Πειραματικό — απαιτείται η αξιολόγησή σας· το χειρόγραφο δεν αλλάζει ποτέ χωρίς την έγκρισή σας.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Ενεργό Στάδιο", "proforge.progress.awaitingReviewHint": "⚠️ Αυτό το στάδιο περιμένει την κριτική σας. Κάντε κλικ στο κουμπί του σταδίου παραπάνω για να ελέγξετε τα στοιχεία.", diff --git a/public/locales/en/bundle.json b/public/locales/en/bundle.json index 16552c32..90705576 100644 --- a/public/locales/en/bundle.json +++ b/public/locales/en/bundle.json @@ -639,6 +639,7 @@ "proforge.loading.analytics": "Summing up the run…", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.pipeline.noneActive": "No active pipeline", + "proforge.pipeline.reviewRequired": "Experimental — your review is required; the manuscript is never changed without your approval.", "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.", diff --git a/public/locales/es/bundle.json b/public/locales/es/bundle.json index 0f57820b..07b087a6 100644 --- a/public/locales/es/bundle.json +++ b/public/locales/es/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Preparándose para el mundo…", "proforge.loading.structural": "Observando la forma de tu historia…", "proforge.pipeline.noneActive": "Sin pipeline activa", + "proforge.pipeline.reviewRequired": "Experimental: se requiere tu revisión; el manuscrito nunca se modifica sin tu aprobación.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Etapa activa", "proforge.progress.awaitingReviewHint": "⚠️ Esta etapa está esperando tu revisión. Haz clic en el botón de la etapa de arriba para revisar los elementos.", diff --git a/public/locales/eu/bundle.json b/public/locales/eu/bundle.json index 96b60d7e..ed4e77f1 100644 --- a/public/locales/eu/bundle.json +++ b/public/locales/eu/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Mundurako prestatzen...", "proforge.loading.structural": "Zure istorioaren formari begira...", "proforge.pipeline.noneActive": "Ez dago kanalizazio aktiborik", + "proforge.pipeline.reviewRequired": "Esperimentala — zure berrikuspena beharrezkoa da; eskuizkribua ez da inoiz aldatzen zure oniritzirik gabe.", "proforge.pipeline.title": "Azken egilearen kanalizazioa", "proforge.progress.activeStage": "Etapa Aktiboa", "proforge.progress.awaitingReviewHint": "⚠️ Etapa hau zure iritziaren zain dago. Egin klik goiko faseko botoian elementuak berrikusteko.", diff --git a/public/locales/fa/bundle.json b/public/locales/fa/bundle.json index 84d5b234..48840aad 100644 --- a/public/locales/fa/bundle.json +++ b/public/locales/fa/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "آماده شدن برای جهان…", "proforge.loading.structural": "با نگاه کردن به شکل داستان شما…", "proforge.pipeline.noneActive": "بدون خط لوله فعال", + "proforge.pipeline.reviewRequired": "آزمایشی — بازبینی شما لازم است؛ نسخه هرگز بدون تأیید شما تغییر نمی‌کند.", "proforge.pipeline.title": "خط لوله نویسنده نهایی", "proforge.progress.activeStage": "مرحله فعال", "proforge.progress.awaitingReviewHint": "⚠️ این مرحله منتظر بررسی شماست. برای بررسی موارد روی دکمه مرحله بالا کلیک کنید.", diff --git a/public/locales/fi/bundle.json b/public/locales/fi/bundle.json index 71a6db18..f6ff50f7 100644 --- a/public/locales/fi/bundle.json +++ b/public/locales/fi/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Valmistautuminen maailmaan…", "proforge.loading.structural": "Tarinasi muotoa katsoessani…", "proforge.pipeline.noneActive": "Ei aktiivista putkistoa", + "proforge.pipeline.reviewRequired": "Kokeellinen — tarkistuksesi vaaditaan; käsikirjoitusta ei koskaan muuteta ilman hyväksyntääsi.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Aktiivinen vaihe", "proforge.progress.awaitingReviewHint": "⚠️ Tämä vaihe odottaa arvosteluasi. Napsauta yllä olevaa vaihepainiketta tarkastellaksesi kohteita.", diff --git a/public/locales/fr/bundle.json b/public/locales/fr/bundle.json index 52648fac..6fba6219 100644 --- a/public/locales/fr/bundle.json +++ b/public/locales/fr/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Prêt pour le monde…", "proforge.loading.structural": "Examen de la structure de votre histoire…", "proforge.pipeline.noneActive": "Aucune pipeline active", + "proforge.pipeline.reviewRequired": "Expérimental — votre relecture est requise ; le manuscrit n'est jamais modifié sans votre accord.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Étape active", "proforge.progress.awaitingReviewHint": "⚠️ Cette étape attend votre relecture. Cliquez sur le bouton de l'étape ci-dessus pour examiner les éléments.", diff --git a/public/locales/he/bundle.json b/public/locales/he/bundle.json index 308dfc6d..8b3514f3 100644 --- a/public/locales/he/bundle.json +++ b/public/locales/he/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "מתכונן לעולם…", "proforge.loading.structural": "מתבונן במבנה הסיפור שלכם…", "proforge.pipeline.noneActive": "אין צינור פעיל", + "proforge.pipeline.reviewRequired": "ניסיוני — נדרשת הבדיקה שלך; כתב היד לעולם לא משתנה ללא אישורך.", "proforge.pipeline.title": "צינור הסופר האולטימטיבי", "proforge.progress.activeStage": "Active Stage", "proforge.progress.awaitingReviewHint": "⚠️ This stage is awaiting your review. Click the stage button above to review items.", diff --git a/public/locales/hu/bundle.json b/public/locales/hu/bundle.json index 4743c1fd..13167e26 100644 --- a/public/locales/hu/bundle.json +++ b/public/locales/hu/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Felkészülés a világra…", "proforge.loading.structural": "Elnézve a történeted alakját…", "proforge.pipeline.noneActive": "Nincs aktív csővezeték", + "proforge.pipeline.reviewRequired": "Kísérleti — szükség van az átnézésedre; a kézirat soha nem módosul a jóváhagyásod nélkül.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Aktív szakasz", "proforge.progress.awaitingReviewHint": "⚠️ Ez a szakasz az értékelésedre vár. Kattintson a fenti szakasz gombra az elemek áttekintéséhez.", diff --git a/public/locales/is/bundle.json b/public/locales/is/bundle.json index c90c5c9b..36f2a909 100644 --- a/public/locales/is/bundle.json +++ b/public/locales/is/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Undirbúningur fyrir heiminn…", "proforge.loading.structural": "Þegar þú horfir á lögun sögunnar þinnar...", "proforge.pipeline.noneActive": "Engin virk leiðsla", + "proforge.pipeline.reviewRequired": "Tilraunaverkefni — yfirferð þín er nauðsynleg; handritinu er aldrei breytt án samþykkis þíns.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Virkt stig", "proforge.progress.awaitingReviewHint": "⚠️ Þetta stig bíður skoðunar þinnar. Smelltu á sviðshnappinn hér að ofan til að skoða atriði.", diff --git a/public/locales/it/bundle.json b/public/locales/it/bundle.json index a0becd6c..ca957a52 100644 --- a/public/locales/it/bundle.json +++ b/public/locales/it/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Pronto per il mondo…", "proforge.loading.structural": "Osservando la forma della tua storia…", "proforge.pipeline.noneActive": "Nessuna pipeline attiva", + "proforge.pipeline.reviewRequired": "Sperimentale — è richiesta la tua revisione; il manoscritto non viene mai modificato senza la tua approvazione.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Fase attiva", "proforge.progress.awaitingReviewHint": "⚠️ Questa fase è in attesa della tua revisione. Fai clic sul pulsante della fase qui sopra per esaminare gli elementi.", diff --git a/public/locales/ja/bundle.json b/public/locales/ja/bundle.json index 5055b4af..33c5229c 100644 --- a/public/locales/ja/bundle.json +++ b/public/locales/ja/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "世界に向けて準備中…", "proforge.loading.structural": "あなたの物語の形を見てみると…", "proforge.pipeline.noneActive": "アクティブなパイプラインがありません", + "proforge.pipeline.reviewRequired": "実験的 — あなたのレビューが必要です。承認なしに原稿が変更されることはありません。", "proforge.pipeline.title": "究極の著者パイプライン", "proforge.progress.activeStage": "アクティブステージ", "proforge.progress.awaitingReviewHint": "⚠️ このステージはあなたのレビューを待っています。項目を確認するには、上のステージ ボタンをクリックしてください。", diff --git a/public/locales/ko/bundle.json b/public/locales/ko/bundle.json index 50d30cde..dee83a5b 100644 --- a/public/locales/ko/bundle.json +++ b/public/locales/ko/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "세상을 준비하는…", "proforge.loading.structural": "이야기의 형태를 살펴보면…", "proforge.pipeline.noneActive": "활성 파이프라인 없음", + "proforge.pipeline.reviewRequired": "실험적 기능 — 검토가 필요합니다. 승인 없이 원고가 변경되지 않습니다.", "proforge.pipeline.title": "궁극적인 작성자 파이프라인", "proforge.progress.activeStage": "액티브 스테이지", "proforge.progress.awaitingReviewHint": "⚠️ 이 단계는 귀하의 검토를 기다리고 있습니다. 항목을 검토하려면 위의 단계 버튼을 클릭하세요.", diff --git a/public/locales/pt/bundle.json b/public/locales/pt/bundle.json index 561deae0..ef35f9b6 100644 --- a/public/locales/pt/bundle.json +++ b/public/locales/pt/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Preparando-se para o mundo…", "proforge.loading.structural": "Olhando para a forma da sua história…", "proforge.pipeline.noneActive": "Nenhum pipeline ativo", + "proforge.pipeline.reviewRequired": "Experimental — a sua revisão é necessária; o manuscrito nunca é alterado sem a sua aprovação.", "proforge.pipeline.title": "Pipeline final do autor", "proforge.progress.activeStage": "Estágio Ativo", "proforge.progress.awaitingReviewHint": "⚠️ Esta etapa aguarda sua análise. Clique no botão de estágio acima para revisar os itens.", diff --git a/public/locales/ru/bundle.json b/public/locales/ru/bundle.json index 4b41a09c..2225323d 100644 --- a/public/locales/ru/bundle.json +++ b/public/locales/ru/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Готовимся к миру…", "proforge.loading.structural": "Глядя на форму вашей истории…", "proforge.pipeline.noneActive": "Нет активного конвейера", + "proforge.pipeline.reviewRequired": "Экспериментально — требуется ваша проверка; рукопись никогда не изменяется без вашего одобрения.", "proforge.pipeline.title": "Конечный авторский конвейер", "proforge.progress.activeStage": "Активная стадия", "proforge.progress.awaitingReviewHint": "⚠️Этот этап ожидает вашего рассмотрения. Нажмите кнопку этапа выше, чтобы просмотреть элементы.", diff --git a/public/locales/sv/bundle.json b/public/locales/sv/bundle.json index 6e4d56a2..bf84c08b 100644 --- a/public/locales/sv/bundle.json +++ b/public/locales/sv/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "Förbereder sig för världen...", "proforge.loading.structural": "Titta på din berättelses form...", "proforge.pipeline.noneActive": "Ingen aktiv pipeline", + "proforge.pipeline.reviewRequired": "Experimentell — din granskning krävs; manuskriptet ändras aldrig utan ditt godkännande.", "proforge.pipeline.title": "Ultimate Author Pipeline", "proforge.progress.activeStage": "Aktiv scen", "proforge.progress.awaitingReviewHint": "⚠️ Det här steget väntar på din recension. Klicka på scenknappen ovan för att granska objekt.", diff --git a/public/locales/zh/bundle.json b/public/locales/zh/bundle.json index c9746822..e927eebb 100644 --- a/public/locales/zh/bundle.json +++ b/public/locales/zh/bundle.json @@ -435,6 +435,7 @@ "proforge.loading.publishing": "为世界做好准备……", "proforge.loading.structural": "看看你的故事的形状……", "proforge.pipeline.noneActive": "无活动管道", + "proforge.pipeline.reviewRequired": "实验性功能——需要您审阅;未经您批准,稿件绝不会被修改。", "proforge.pipeline.title": "最终作者管道", "proforge.progress.activeStage": "活跃阶段", "proforge.progress.awaitingReviewHint": "⚠️此阶段正在等待您的审核。单击上面的阶段按钮以查看项目。", diff --git a/services/proForge/pipelineAgents/supervisorAgent.ts b/services/proForge/pipelineAgents/supervisorAgent.ts index b2a5def5..db33873c 100644 --- a/services/proForge/pipelineAgents/supervisorAgent.ts +++ b/services/proForge/pipelineAgents/supervisorAgent.ts @@ -4,25 +4,47 @@ * Called by the orchestrator after each stage; never invoked as a pipeline stage itself. */ -import type { - CopyEditPlan, - DiagnosticReport, - PipelineAnalyticsReport, - PipelineStage, - ProductionManifest, - PublishingPackage, - QualityGateReport, - StageResult, - StructuralEditPlan, - SupervisionDecision, +import { + type CopyEditPlan, + DEFAULT_QUALITY_THRESHOLDS, + type DiagnosticReport, + type PipelineAnalyticsReport, + type PipelineStage, + type ProductionManifest, + type PublishingPackage, + type QualityGateReport, + type QualityThresholds, + type StageResult, + type StructuralEditPlan, + type SupervisionDecision, } from '../../../features/proForge/types'; import type { OrchestratorContext } from '../proForgeOrchestrator'; export class SupervisorAgent { private readonly context: OrchestratorContext; + private readonly thresholds: QualityThresholds; - constructor(context: OrchestratorContext) { + constructor(context: OrchestratorContext, thresholds?: Partial) { this.context = context; + this.thresholds = { ...DEFAULT_QUALITY_THRESHOLDS, ...thresholds }; + } + + /** + * QNBS-v3: PR6 — a *measured* confidence score instead of a flat constant. It scales with how much + * real signal the stage produced relative to manuscript size (findings per ~1000 words), within a + * pass band, so the score actually varies with the work done rather than always reporting the same + * number. The supervisor still does NO AI calls — this is a heuristic confidence, not editorial + * quality. + */ + private confidenceScore(findings: number, wordCount: number, floor = 60, ceil = 95): number { + if (wordCount <= 0) return Math.round((floor + ceil) / 2); + const perThousand = findings / Math.max(1, wordCount / 1000); + return Math.round(Math.min(ceil, floor + perThousand * 9)); + } + + /** A measured "this looks like a fallback" score: the larger the manuscript, the more suspicious. */ + private suspectScore(wordCount: number): number { + return Math.max(20, 50 - Math.floor(wordCount / 500)); } evaluate( @@ -95,18 +117,28 @@ export class SupervisorAgent { } const wordCount = this.estimateManuscriptWordCount(); - const hasEdits = (output?.edits?.length ?? 0) > 0; - const hasReviewItems = result.reviewItems.length > 0; + const editCount = output?.edits?.length ?? 0; + const signal = editCount + result.reviewItems.length; - // QNBS-v3: A manuscript over 1000 words with zero structural edits is suspicious. - if (!hasEdits && !hasReviewItems && wordCount > 1000) { + // QNBS-v3: A large manuscript with zero structural edits is suspicious (likely a fallback). + if (signal === 0 && wordCount > this.thresholds.largeManuscriptWords) { reasons.push( `No structural edits found for a ${wordCount}-word manuscript — may need human review.`, ); - return { pass: false, retryRecommended: true, qualityScore: 40, reasons }; + return { + pass: false, + retryRecommended: true, + qualityScore: this.suspectScore(wordCount), + reasons, + }; } - return { pass: true, retryRecommended: false, qualityScore: 80, reasons }; + return { + pass: true, + retryRecommended: false, + qualityScore: this.confidenceScore(signal, wordCount), + reasons, + }; } private evaluateProof( @@ -122,19 +154,30 @@ export class SupervisorAgent { } const wordCount = this.estimateManuscriptWordCount(); + const grammarIssues = output?.grammar?.issues?.length ?? 0; + // Proof is more sensitive than structural edits — half the large-manuscript threshold. + const proofThreshold = Math.round(this.thresholds.largeManuscriptWords / 2); const seemsFallback = - output?.overallPass === true && - (output.grammar?.issues?.length ?? 0) === 0 && - wordCount > 500; + output?.overallPass === true && grammarIssues === 0 && wordCount > proofThreshold; if (seemsFallback) { reasons.push( 'Proof passed with zero grammar issues on a substantial manuscript — verify AI ran correctly.', ); - return { pass: false, retryRecommended: true, qualityScore: 40, reasons }; + return { + pass: false, + retryRecommended: true, + qualityScore: this.suspectScore(wordCount), + reasons, + }; } - return { pass: true, retryRecommended: false, qualityScore: 90, reasons }; + return { + pass: true, + retryRecommended: false, + qualityScore: this.confidenceScore(grammarIssues, wordCount, 70, 95), + reasons, + }; } private evaluateLineProse( @@ -142,20 +185,25 @@ export class SupervisorAgent { ): SupervisionDecision { const output = result.agentOutput as { edits?: unknown[] } | undefined; const wordCount = this.estimateManuscriptWordCount(); - const hasEdits = (output?.edits?.length ?? 0) > 0; + const signal = (output?.edits?.length ?? 0) + result.reviewItems.length; // QNBS-v3: A substantial manuscript with zero prose edits suggests the AI call didn't land. - if (!hasEdits && result.reviewItems.length === 0 && wordCount > 1000) { + if (signal === 0 && wordCount > this.thresholds.largeManuscriptWords) { return { pass: false, retryRecommended: true, - qualityScore: 45, + qualityScore: this.suspectScore(wordCount), reasons: [ `No prose edits for a ${wordCount}-word manuscript — verify the AI provider responded.`, ], }; } - return { pass: true, retryRecommended: false, qualityScore: 85, reasons: [] }; + return { + pass: true, + retryRecommended: false, + qualityScore: this.confidenceScore(signal, wordCount), + reasons: [], + }; } private evaluateCopyEdit( @@ -168,19 +216,27 @@ export class SupervisorAgent { (output?.repetitionHits?.length ?? 0) + (output?.formatIssues?.length ?? 0); const wordCount = this.estimateManuscriptWordCount(); + const signal = total + result.reviewItems.length; + // Copy-edit is the least sensitive editing stage — 1.5× the large-manuscript threshold. + const copyEditThreshold = Math.round(this.thresholds.largeManuscriptWords * 1.5); // QNBS-v3: Zero grammar/style/repetition/format findings on a long manuscript is suspicious. - if (total === 0 && result.reviewItems.length === 0 && wordCount > 1500) { + if (signal === 0 && wordCount > copyEditThreshold) { return { pass: false, retryRecommended: true, - qualityScore: 50, + qualityScore: this.suspectScore(wordCount), 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: [] }; + return { + pass: true, + retryRecommended: false, + qualityScore: this.confidenceScore(signal, wordCount), + reasons: [], + }; } private evaluateProduction( diff --git a/services/proForge/proForgeOrchestrator.ts b/services/proForge/proForgeOrchestrator.ts index 3c29f1a0..7e5fe6ba 100644 --- a/services/proForge/proForgeOrchestrator.ts +++ b/services/proForge/proForgeOrchestrator.ts @@ -20,7 +20,11 @@ import type { ReviewItemStatus, SupervisionDecision, } from '../../features/proForge/types'; -import { isEditingStage, nextStage } from '../../features/proForge/types'; +import { + DEFAULT_QUALITY_THRESHOLDS, + isEditingStage, + nextStage, +} from '../../features/proForge/types'; import { logger } from '../logger'; import { planAcceptedManuscriptEdits } from './applyReviewEdits'; // QNBS-v3: stage→agent mapping extracted to a shared registry so the Core Capability Layer can run @@ -164,6 +168,10 @@ export class ProForgeOrchestrator { maxRetries: number, ): Promise { const { dispatch } = this.context; + // QNBS-v3: PR6 — tunable supervisor thresholds from the run config (defaults applied downstream). + const qualityThresholds = this.context.getState().proForge.currentRun?.config.qualityThresholds; + const intakeHardGate = + qualityThresholds?.intakeHardGate ?? DEFAULT_QUALITY_THRESHOLDS.intakeHardGate; // QNBS-v3: Carries the prior attempt's rejection reasons into the next prompt. let retryFeedback = ''; @@ -180,7 +188,7 @@ export class ProForgeOrchestrator { // QNBS-v3: Supervisor evaluates heuristic quality gates before advancing. const { SupervisorAgent } = await import('./pipelineAgents/supervisorAgent'); - const supervisor = new SupervisorAgent(this.context); + const supervisor = new SupervisorAgent(this.context, qualityThresholds); const decision = supervisor.evaluate(stage, result); if (!decision.pass && attempt < maxRetries) { @@ -198,8 +206,8 @@ export class ProForgeOrchestrator { continue; } - // Hard gate: intake with qualityScore < 30 → fail with honest message. - if (stage === 'intake' && decision.qualityScore < 30) { + // Hard gate: intake below the configured threshold → fail with honest message. + if (stage === 'intake' && decision.qualityScore < intakeHardGate) { const message = "The diagnostic couldn't analyze your manuscript. Check your AI provider connection and try again."; dispatch(stageFailed({ stage, error: message })); diff --git a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts index 4083d711..68eec2df 100644 --- a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts +++ b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts @@ -110,10 +110,11 @@ describe('SupervisorAgent', () => { }); describe('evaluateLineProse', () => { - it('passes with score 85 when prose edits are present', () => { + it('passes with a measured score in the pass band when prose edits are present', () => { const result = agent.evaluate('lineProse', { reviewItems: [], agentOutput: { edits: [{}] } }); expect(result.pass).toBe(true); - expect(result.qualityScore).toBe(85); + expect(result.qualityScore).toBeGreaterThanOrEqual(60); + expect(result.qualityScore).toBeLessThanOrEqual(95); }); it('passes for a short manuscript even with zero edits', () => { @@ -297,13 +298,14 @@ describe('SupervisorAgent', () => { // --------------------------------------------------------------------------- describe('evaluateStructural', () => { - it('passes with score 80 when edits are present', () => { + it('passes with a measured score in the pass band when edits are present', () => { const result = agent.evaluate('structural', { reviewItems: [], agentOutput: { edits: [{ id: 'e1' }] }, }); expect(result.pass).toBe(true); - expect(result.qualityScore).toBe(80); + expect(result.qualityScore).toBeGreaterThanOrEqual(60); + expect(result.qualityScore).toBeLessThanOrEqual(95); }); it('passes when no edits but reviewItems are present', () => { @@ -335,7 +337,7 @@ describe('SupervisorAgent', () => { }); expect(result.pass).toBe(false); expect(result.retryRecommended).toBe(true); - expect(result.qualityScore).toBe(40); + expect(result.qualityScore).toBeLessThan(60); expect(result.reasons.some((r) => r.includes('structural edits'))).toBe(true); }); @@ -354,7 +356,7 @@ describe('SupervisorAgent', () => { // --------------------------------------------------------------------------- describe('evaluateProof', () => { - it('passes with score 90 when grammar issues exist', () => { + it('passes with a measured score in the pass band when grammar issues exist', () => { const result = agent.evaluate('proof', { reviewItems: [], agentOutput: { @@ -363,7 +365,8 @@ describe('SupervisorAgent', () => { }, }); expect(result.pass).toBe(true); - expect(result.qualityScore).toBe(90); + expect(result.qualityScore).toBeGreaterThanOrEqual(70); + expect(result.qualityScore).toBeLessThanOrEqual(95); }); it('passes when overallPass is false (not the suspicious pattern)', () => { @@ -384,7 +387,7 @@ describe('SupervisorAgent', () => { }); expect(result.pass).toBe(false); expect(result.retryRecommended).toBe(true); - expect(result.qualityScore).toBe(40); + expect(result.qualityScore).toBeLessThan(70); expect(result.reasons.some((r) => r.includes('zero grammar issues'))).toBe(true); }); @@ -397,13 +400,14 @@ describe('SupervisorAgent', () => { expect(result.pass).toBe(true); }); - it('passes when agentOutput is undefined (no sentinel)', () => { + it('passes with a measured score when agentOutput is undefined (no sentinel)', () => { const result = agent.evaluate('proof', { reviewItems: [], agentOutput: undefined, }); expect(result.pass).toBe(true); - expect(result.qualityScore).toBe(90); + expect(result.qualityScore).toBeGreaterThanOrEqual(70); + expect(result.qualityScore).toBeLessThanOrEqual(95); }); }); @@ -455,4 +459,45 @@ describe('SupervisorAgent', () => { expect(result.pass).toBe(true); }); }); + + // --------------------------------------------------------------------------- + // Tests: PR6 — measured scoring + configurable thresholds + // --------------------------------------------------------------------------- + + describe('measured scoring', () => { + it('scores higher with more findings (not a flat constant)', () => { + const longAgent = new SupervisorAgent(makeContext('word '.repeat(2000))); + const few = longAgent.evaluate('structural', { + reviewItems: [], + agentOutput: { edits: [{ id: 'a' }] }, + }); + const many = longAgent.evaluate('structural', { + reviewItems: [], + agentOutput: { edits: Array.from({ length: 12 }, (_, i) => ({ id: `e${i}` })) }, + }); + expect(many.qualityScore).toBeGreaterThan(few.qualityScore); + }); + }); + + describe('configurable thresholds', () => { + it('a lower largeManuscriptWords threshold flags a smaller manuscript as suspicious', () => { + // ~600 words: passes under the default 1000 threshold... + const ctx = makeContext('word '.repeat(600)); + const dflt = new SupervisorAgent(ctx).evaluate('structural', { + reviewItems: [], + agentOutput: { edits: [] }, + }); + expect(dflt.pass).toBe(true); + // ...but fails when the threshold is lowered to 300. + const strict = new SupervisorAgent(ctx, { largeManuscriptWords: 300 }).evaluate( + 'structural', + { + reviewItems: [], + agentOutput: { edits: [] }, + }, + ); + expect(strict.pass).toBe(false); + expect(strict.retryRecommended).toBe(true); + }); + }); }); From 7319576de1a30c34cceeb62b38ef1ea091fe20d3 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:51:40 +0200 Subject: [PATCH 2/4] fix(proforge): thread qualityThresholds through capability-layer config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeAnt: partialPipelineConfigSchema lacked qualityThresholds, so Zod silently stripped the key for Node/MCP capability callers — overrides were dropped and the supervisor always used DEFAULT_QUALITY_THRESHOLDS. Add qualityThresholdsSchema to the validator and pass config.qualityThresholds into the SupervisorAgent so non-Redux entry points honor the config contract. Co-Authored-By: Claude Opus 4.8 --- services/proForge/proForgeCapabilityLayer.ts | 4 +++- .../proForge/proForgeCapabilitySchemas.ts | 13 ++++++++++ .../proForgeCapabilitySchemas.test.ts | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/services/proForge/proForgeCapabilityLayer.ts b/services/proForge/proForgeCapabilityLayer.ts index c35fa37c..d34d42d8 100644 --- a/services/proForge/proForgeCapabilityLayer.ts +++ b/services/proForge/proForgeCapabilityLayer.ts @@ -105,7 +105,9 @@ export class ProForgeCapabilityLayer { } // QNBS-v3: heuristic quality gate (no AI) — same gate the orchestrator applies between stages. - const supervisor = new SupervisorAgent(context); + // Pass caller-resolved thresholds so capability-layer (Node/MCP) callers can tune the gate; + // SupervisorAgent re-merges against DEFAULT_QUALITY_THRESHOLDS for any omitted field. + const supervisor = new SupervisorAgent(context, config.qualityThresholds); const supervisorDecision = supervisor.evaluate(input.stage, result); return { diff --git a/services/proForge/proForgeCapabilitySchemas.ts b/services/proForge/proForgeCapabilitySchemas.ts index fc80a58c..a3098d42 100644 --- a/services/proForge/proForgeCapabilitySchemas.ts +++ b/services/proForge/proForgeCapabilitySchemas.ts @@ -62,6 +62,16 @@ export const memoryEntrySeedSchema = z.object({ sourceStage: pipelineStageSchema.default('idle'), }); +/** + * Supervisor quality-gate thresholds. Sub-fields are required so the inferred type matches + * `QualityThresholds` exactly (both fields present); callers override the pair as a unit, and the + * SupervisorAgent re-merges against DEFAULT_QUALITY_THRESHOLDS internally. + */ +export const qualityThresholdsSchema = z.object({ + largeManuscriptWords: z.number().int().positive(), + intakeHardGate: z.number().min(0), +}); + /** Overridable run configuration (all optional — merged onto defaults). */ export const partialPipelineConfigSchema = z.object({ genrePreset: z.string().optional(), @@ -73,6 +83,9 @@ export const partialPipelineConfigSchema = z.object({ language: z.string().optional(), useDuckDb: z.boolean().optional(), autoAcceptThreshold: z.number().min(0).max(1).optional(), + // QNBS-v3: PR6 — accept supervisor threshold overrides so Node/MCP capability callers can tune + // quality gates (the schema previously stripped this key, silently dropping caller overrides). + qualityThresholds: qualityThresholdsSchema.optional(), }); export type PartialPipelineConfigInput = z.infer; diff --git a/tests/unit/proForge/proForgeCapabilitySchemas.test.ts b/tests/unit/proForge/proForgeCapabilitySchemas.test.ts index 85119cc1..a0384d09 100644 --- a/tests/unit/proForge/proForgeCapabilitySchemas.test.ts +++ b/tests/unit/proForge/proForgeCapabilitySchemas.test.ts @@ -31,6 +31,30 @@ describe('runStageInputSchema', () => { it('rejects an empty projectId', () => { expect(runStageInputSchema.safeParse({ stage: 'intake', projectId: '' }).success).toBe(false); }); + + // QNBS-v3: PR6 CodeAnt finding — capability-layer callers must be able to override supervisor + // thresholds; previously the schema lacked `qualityThresholds` so Zod silently stripped the key. + it('preserves qualityThresholds overrides for non-Redux callers', () => { + const parsed = runStageInputSchema.parse({ + stage: 'intake', + projectId: 'p1', + config: { qualityThresholds: { largeManuscriptWords: 5000, intakeHardGate: 10 } }, + }); + expect(parsed.config?.qualityThresholds).toEqual({ + largeManuscriptWords: 5000, + intakeHardGate: 10, + }); + }); + + it('rejects a partial / malformed qualityThresholds override', () => { + expect( + runStageInputSchema.safeParse({ + stage: 'intake', + projectId: 'p1', + config: { qualityThresholds: { largeManuscriptWords: -1, intakeHardGate: 10 } }, + }).success, + ).toBe(false); + }); }); describe('getHistoryInputSchema', () => { From 9679f492cf587f14414912098303cb4401aea688 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:37:37 +0200 Subject: [PATCH 3/4] fix(proforge): centralize intake hard gate; bound intakeHardGate to 0-100 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CodeAnt findings on the quality-gate config contract: - Gate intake failure on the supervisor actually flagging it (!decision.pass) plus a sub-floor score, not score alone — a legitimately weak-but-analyzed manuscript no longer mislabels as an AI-provider failure. - Centralize the rule in SupervisorAgent.intakeHardGateFailed so the orchestrator AND the capability layer (Node/MCP runStage) enforce identical behavior; the capability layer now throws STAGE_FAILED on a fallback intake instead of returning a misleading success. - Bound intakeHardGate to 0..100 in the Zod schema so an impossible (>100) threshold can't make every intake fail via misconfiguration. Co-Authored-By: Claude Opus 4.8 --- .../pipelineAgents/supervisorAgent.ts | 11 +++ services/proForge/proForgeCapabilityLayer.ts | 9 +++ .../proForge/proForgeCapabilitySchemas.ts | 4 +- services/proForge/proForgeOrchestrator.ts | 14 ++-- .../pipelineAgents/supervisorAgent.test.ts | 58 ++++++++++++++++ .../proForge/proForgeCapabilityLayer.test.ts | 15 ++++ .../proForgeCapabilitySchemas.test.ts | 11 +++ .../proForge/proForgeOrchestrator.test.ts | 69 +++++++++++++++++++ 8 files changed, 181 insertions(+), 10 deletions(-) diff --git a/services/proForge/pipelineAgents/supervisorAgent.ts b/services/proForge/pipelineAgents/supervisorAgent.ts index db33873c..fd77cdd2 100644 --- a/services/proForge/pipelineAgents/supervisorAgent.ts +++ b/services/proForge/pipelineAgents/supervisorAgent.ts @@ -73,6 +73,17 @@ export class SupervisorAgent { } } + /** + * QNBS-v3: PR6 — centralized intake hard gate, shared by the orchestrator and the capability + * layer so every entry point enforces identical rules. Fires ONLY when the supervisor actually + * flagged intake as failed (fallback / no real analysis) AND the measured score is below the + * configured floor — never on a low-but-genuine score alone, which would mislabel a legitimately + * weak manuscript as an AI-provider failure. + */ + intakeHardGateFailed(decision: SupervisionDecision): boolean { + return !decision.pass && decision.qualityScore < this.thresholds.intakeHardGate; + } + private evaluateIntake( result: Pick, ): SupervisionDecision { diff --git a/services/proForge/proForgeCapabilityLayer.ts b/services/proForge/proForgeCapabilityLayer.ts index d34d42d8..52c294f1 100644 --- a/services/proForge/proForgeCapabilityLayer.ts +++ b/services/proForge/proForgeCapabilityLayer.ts @@ -110,6 +110,15 @@ export class ProForgeCapabilityLayer { const supervisor = new SupervisorAgent(context, config.qualityThresholds); const supervisorDecision = supervisor.evaluate(input.stage, result); + // QNBS-v3: Enforce the SAME intake hard gate the orchestrator applies, so Node/MCP callers + // can't get a "successful" intake from a fallback/unanalyzable diagnostic. Centralized in + // SupervisorAgent.intakeHardGateFailed so both entry points use identical rules. + if (input.stage === 'intake' && supervisor.intakeHardGateFailed(supervisorDecision)) { + throw ProForgeError.stageFailed( + "Intake diagnostic couldn't analyze the manuscript — check the AI provider connection.", + ); + } + return { stage: input.stage, reviewItems: result.reviewItems, diff --git a/services/proForge/proForgeCapabilitySchemas.ts b/services/proForge/proForgeCapabilitySchemas.ts index a3098d42..6095d32e 100644 --- a/services/proForge/proForgeCapabilitySchemas.ts +++ b/services/proForge/proForgeCapabilitySchemas.ts @@ -69,7 +69,9 @@ export const memoryEntrySeedSchema = z.object({ */ export const qualityThresholdsSchema = z.object({ largeManuscriptWords: z.number().int().positive(), - intakeHardGate: z.number().min(0), + // QNBS-v3: intake quality scores are on a 0–100 scale; bound the gate so an impossible + // threshold (>100) can't make every intake fail unconditionally via misconfiguration. + intakeHardGate: z.number().min(0).max(100), }); /** Overridable run configuration (all optional — merged onto defaults). */ diff --git a/services/proForge/proForgeOrchestrator.ts b/services/proForge/proForgeOrchestrator.ts index 7e5fe6ba..c9236023 100644 --- a/services/proForge/proForgeOrchestrator.ts +++ b/services/proForge/proForgeOrchestrator.ts @@ -20,11 +20,7 @@ import type { ReviewItemStatus, SupervisionDecision, } from '../../features/proForge/types'; -import { - DEFAULT_QUALITY_THRESHOLDS, - isEditingStage, - nextStage, -} from '../../features/proForge/types'; +import { isEditingStage, nextStage } from '../../features/proForge/types'; import { logger } from '../logger'; import { planAcceptedManuscriptEdits } from './applyReviewEdits'; // QNBS-v3: stage→agent mapping extracted to a shared registry so the Core Capability Layer can run @@ -170,8 +166,6 @@ export class ProForgeOrchestrator { const { dispatch } = this.context; // QNBS-v3: PR6 — tunable supervisor thresholds from the run config (defaults applied downstream). const qualityThresholds = this.context.getState().proForge.currentRun?.config.qualityThresholds; - const intakeHardGate = - qualityThresholds?.intakeHardGate ?? DEFAULT_QUALITY_THRESHOLDS.intakeHardGate; // QNBS-v3: Carries the prior attempt's rejection reasons into the next prompt. let retryFeedback = ''; @@ -206,8 +200,10 @@ export class ProForgeOrchestrator { continue; } - // Hard gate: intake below the configured threshold → fail with honest message. - if (stage === 'intake' && decision.qualityScore < intakeHardGate) { + // QNBS-v3: Hard gate via the centralized supervisor rule — fails ONLY when intake was + // actually flagged (fallback/no real analysis) AND scored below the floor, so a legitimately + // weak-but-analyzed manuscript is not mislabeled as an AI-provider failure. + if (stage === 'intake' && supervisor.intakeHardGateFailed(decision)) { const message = "The diagnostic couldn't analyze your manuscript. Check your AI provider connection and try again."; dispatch(stageFailed({ stage, error: message })); diff --git a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts index 68eec2df..d993e154 100644 --- a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts +++ b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts @@ -293,6 +293,64 @@ describe('SupervisorAgent', () => { }); }); + // --------------------------------------------------------------------------- + // Tests: intakeHardGateFailed — centralized gate shared by orchestrator + capability layer + // --------------------------------------------------------------------------- + + describe('intakeHardGateFailed', () => { + it('fails a fallback intake (pass:false + score below the floor)', () => { + const decision = agent.evaluate('intake', { + reviewItems: [], + agentOutput: { + isFallback: true, + qualityScore: ZERO_QUALITY_SCORE, + consistencyIssues: [], + structuralGaps: [], + }, + }); + expect(agent.intakeHardGateFailed(decision)).toBe(true); + }); + + it('does NOT fail a legitimately weak-but-analyzed manuscript (pass:true, low score)', () => { + // QNBS-v3: gating on score alone would mislabel a real low-quality analysis as a provider + // failure; the gate must require the supervisor to have actually flagged it (pass:false). + const lowButReal = agent.intakeHardGateFailed({ + pass: true, + retryRecommended: false, + qualityScore: 5, + reasons: [], + }); + expect(lowButReal).toBe(false); + }); + + it('does NOT fail a passing high-score intake', () => { + const decision = agent.evaluate('intake', { + reviewItems: [], + agentOutput: { + qualityScore: REAL_QUALITY_SCORE, + consistencyIssues: [{ id: 'ci-1' }], + structuralGaps: [], + }, + }); + expect(agent.intakeHardGateFailed(decision)).toBe(false); + }); + + it('respects a custom intakeHardGate threshold', () => { + // With the gate floored at 0, even a fallback (score 0) does not trip it. + const lenient = new SupervisorAgent(makeContext(), { intakeHardGate: 0 }); + const decision = lenient.evaluate('intake', { + reviewItems: [], + agentOutput: { + isFallback: true, + qualityScore: ZERO_QUALITY_SCORE, + consistencyIssues: [], + structuralGaps: [], + }, + }); + expect(lenient.intakeHardGateFailed(decision)).toBe(false); + }); + }); + // --------------------------------------------------------------------------- // Tests: structural stage // --------------------------------------------------------------------------- diff --git a/tests/unit/proForge/proForgeCapabilityLayer.test.ts b/tests/unit/proForge/proForgeCapabilityLayer.test.ts index a8dec47f..949e2123 100644 --- a/tests/unit/proForge/proForgeCapabilityLayer.test.ts +++ b/tests/unit/proForge/proForgeCapabilityLayer.test.ts @@ -29,11 +29,16 @@ vi.mock('../../../services/proForge/pipelineAgents/agentRegistry', () => ({ }), })); +// QNBS-v3: controllable supervisor mock — lets a test force the intake hard gate to fire. +const supervisorState = vi.hoisted(() => ({ hardGateFailed: false })); vi.mock('../../../services/proForge/pipelineAgents/supervisorAgent', () => ({ SupervisorAgent: class { evaluate() { return { pass: true, retryRecommended: false, qualityScore: 90, reasons: [] }; } + intakeHardGateFailed() { + return supervisorState.hardGateFailed; + } }, })); @@ -87,6 +92,7 @@ describe('ProForgeCapabilityLayer', () => { beforeEach(() => { ports = makePorts(); layer = createProForgeCapabilityLayer(ports); + supervisorState.hardGateFailed = false; }); describe('permission gating', () => { @@ -147,6 +153,15 @@ describe('ProForgeCapabilityLayer', () => { code: 'NOT_FOUND', }); }); + + // QNBS-v3: PR6 CodeAnt — capability layer must enforce the SAME intake hard gate as the + // orchestrator, so a fallback/unanalyzable intake throws instead of returning "success". + it('throws STAGE_FAILED when the intake hard gate fails', async () => { + supervisorState.hardGateFailed = true; + await expect(layer.runStage({ stage: 'intake', projectId: 'p1' })).rejects.toMatchObject({ + code: 'STAGE_FAILED', + }); + }); }); describe('applyEdits', () => { diff --git a/tests/unit/proForge/proForgeCapabilitySchemas.test.ts b/tests/unit/proForge/proForgeCapabilitySchemas.test.ts index a0384d09..53b0ac0a 100644 --- a/tests/unit/proForge/proForgeCapabilitySchemas.test.ts +++ b/tests/unit/proForge/proForgeCapabilitySchemas.test.ts @@ -55,6 +55,17 @@ describe('runStageInputSchema', () => { }).success, ).toBe(false); }); + + // QNBS-v3: PR6 CodeAnt — intake scores are 0–100; a >100 gate would fail every intake. + it('rejects an intakeHardGate above the 0–100 score scale', () => { + expect( + runStageInputSchema.safeParse({ + stage: 'intake', + projectId: 'p1', + config: { qualityThresholds: { largeManuscriptWords: 1000, intakeHardGate: 150 } }, + }).success, + ).toBe(false); + }); }); describe('getHistoryInputSchema', () => { diff --git a/tests/unit/proForge/proForgeOrchestrator.test.ts b/tests/unit/proForge/proForgeOrchestrator.test.ts index f250ef02..655c47bb 100644 --- a/tests/unit/proForge/proForgeOrchestrator.test.ts +++ b/tests/unit/proForge/proForgeOrchestrator.test.ts @@ -833,6 +833,75 @@ describe('ProForgeOrchestrator', () => { }); }); + describe('integration: intake hard gate', () => { + // QNBS-v3: PR6 CodeAnt — the hard gate must fire on an ACTUAL supervisor failure (fallback / + // unanalyzable), not on a low score alone. + it('fails the run when intake is flagged (pass:false) and scored below the floor', async () => { + const { SupervisorAgent } = await import( + '../../../services/proForge/pipelineAgents/supervisorAgent' + ); + const evaluateSpy = vi.spyOn(SupervisorAgent.prototype, 'evaluate').mockReturnValue({ + pass: false, + retryRecommended: true, + qualityScore: 0, + reasons: ['fallback'], + }); + + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'running', + stages: [], + config: { ...DEFAULT_CONFIG, selectedStages: ['intake'], maxRetries: 0 }, + label: 'Hard Gate', + }, + isRunning: true, + }); + const orch = new ProForgeOrchestrator(ctx); + await orch.executeStage('intake'); + + const { stageFailed } = await import('../../../features/proForge/proForgeSlice'); + expect(vi.mocked(stageFailed)).toHaveBeenCalledWith( + expect.objectContaining({ stage: 'intake' }), + ); + evaluateSpy.mockRestore(); + }); + + it('does NOT fail the run for a legitimately weak-but-analyzed intake (pass:true, low score)', async () => { + const { SupervisorAgent } = await import( + '../../../services/proForge/pipelineAgents/supervisorAgent' + ); + const evaluateSpy = vi.spyOn(SupervisorAgent.prototype, 'evaluate').mockReturnValue({ + pass: true, + retryRecommended: false, + qualityScore: 5, + reasons: [], + }); + + const ctx = makeContext({ + currentRun: { + id: 'run-1', + status: 'running', + stages: [], + config: { ...DEFAULT_CONFIG, selectedStages: ['intake'], maxRetries: 0 }, + label: 'Weak Manuscript', + }, + isRunning: true, + }); + const orch = new ProForgeOrchestrator(ctx); + await orch.executeStage('intake'); + + const { stageCompleted, stageFailed } = await import( + '../../../features/proForge/proForgeSlice' + ); + expect(vi.mocked(stageFailed)).not.toHaveBeenCalled(); + expect(vi.mocked(stageCompleted)).toHaveBeenCalledWith( + expect.objectContaining({ stage: 'intake' }), + ); + evaluateSpy.mockRestore(); + }); + }); + describe('integration: snapshot rollback after failure', () => { // QNBS-v3: When a stage fails mid-run, rollbackTo restores the pre-stage snapshot. it('dispatches restoreSnapshot when rolling back after a failed stage', async () => { From a5eaaacdffbec0ddc3a350eda68118abb18b90bb Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:05:41 +0200 Subject: [PATCH 4/4] fix(proforge): stop double-counting supervisor signal; count all proof findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four CodeAnt findings on the measured confidence scores: - structural/lineProse/copyEdit summed agentOutput edits AND reviewItems, but reviewItems are derived 1:1 from those same edits — inflating confidence. Use Math.max(editCount, reviewItems.length) as the canonical single-source signal so the score is proportional to real work, never doubled. - proof scored only grammar issues, ignoring style/technical/legal findings. Count all proof-stage signal so reports with substantial non-grammar findings aren't mis-scored (and aren't mislabeled as fallbacks). Adds a double-count regression guard + a non-grammar-proof test. Co-Authored-By: Claude Opus 4.8 --- .../pipelineAgents/supervisorAgent.ts | 27 +++++++++---- .../pipelineAgents/supervisorAgent.test.ts | 40 ++++++++++++++++++- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/services/proForge/pipelineAgents/supervisorAgent.ts b/services/proForge/pipelineAgents/supervisorAgent.ts index fd77cdd2..39c836b7 100644 --- a/services/proForge/pipelineAgents/supervisorAgent.ts +++ b/services/proForge/pipelineAgents/supervisorAgent.ts @@ -129,7 +129,10 @@ export class SupervisorAgent { const wordCount = this.estimateManuscriptWordCount(); const editCount = output?.edits?.length ?? 0; - const signal = editCount + result.reviewItems.length; + // QNBS-v3: reviewItems are derived 1:1 from these same edits (StructuralAgent maps plan.edits + // into reviewItems), so the canonical signal is the max of the two — summing would double-count + // and inflate the measured confidence. + const signal = Math.max(editCount, result.reviewItems.length); // QNBS-v3: A large manuscript with zero structural edits is suspicious (likely a fallback). if (signal === 0 && wordCount > this.thresholds.largeManuscriptWords) { @@ -165,15 +168,21 @@ export class SupervisorAgent { } const wordCount = this.estimateManuscriptWordCount(); - const grammarIssues = output?.grammar?.issues?.length ?? 0; + // QNBS-v3: count ALL proof-stage signal (grammar + style + technical + legal), not just grammar + // — a report with substantial non-grammar findings was previously scored as if it found nothing. + const proofFindings = + (output?.grammar?.issues?.length ?? 0) + + (output?.style?.issues?.length ?? 0) + + (output?.technical?.issues?.length ?? 0) + + (output?.legal?.warnings?.length ?? 0); // Proof is more sensitive than structural edits — half the large-manuscript threshold. const proofThreshold = Math.round(this.thresholds.largeManuscriptWords / 2); const seemsFallback = - output?.overallPass === true && grammarIssues === 0 && wordCount > proofThreshold; + output?.overallPass === true && proofFindings === 0 && wordCount > proofThreshold; if (seemsFallback) { reasons.push( - 'Proof passed with zero grammar issues on a substantial manuscript — verify AI ran correctly.', + 'Proof passed with zero issues across grammar/style/technical/legal on a substantial manuscript — verify AI ran correctly.', ); return { pass: false, @@ -186,7 +195,7 @@ export class SupervisorAgent { return { pass: true, retryRecommended: false, - qualityScore: this.confidenceScore(grammarIssues, wordCount, 70, 95), + qualityScore: this.confidenceScore(proofFindings, wordCount, 70, 95), reasons, }; } @@ -196,7 +205,9 @@ export class SupervisorAgent { ): SupervisionDecision { const output = result.agentOutput as { edits?: unknown[] } | undefined; const wordCount = this.estimateManuscriptWordCount(); - const signal = (output?.edits?.length ?? 0) + result.reviewItems.length; + // QNBS-v3: ProseAgent builds reviewItems directly from output.edits, so take the canonical count + // (max), not the sum — summing double-counts and overstates line-prose confidence. + const signal = Math.max(output?.edits?.length ?? 0, result.reviewItems.length); // QNBS-v3: A substantial manuscript with zero prose edits suggests the AI call didn't land. if (signal === 0 && wordCount > this.thresholds.largeManuscriptWords) { @@ -227,7 +238,9 @@ export class SupervisorAgent { (output?.repetitionHits?.length ?? 0) + (output?.formatIssues?.length ?? 0); const wordCount = this.estimateManuscriptWordCount(); - const signal = total + result.reviewItems.length; + // QNBS-v3: reviewItems are generated from these same grammar/style/repetition/format findings, + // so take the canonical count (max), not the sum — summing double-counts confidence. + const signal = Math.max(total, result.reviewItems.length); // Copy-edit is the least sensitive editing stage — 1.5× the large-manuscript threshold. const copyEditThreshold = Math.round(this.thresholds.largeManuscriptWords * 1.5); diff --git a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts index d993e154..5c28a32d 100644 --- a/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts +++ b/tests/unit/proForge/pipelineAgents/supervisorAgent.test.ts @@ -407,6 +407,27 @@ describe('SupervisorAgent', () => { }); expect(result.pass).toBe(true); }); + + it('does NOT double-count edits + their derived reviewItems (canonical max, not sum)', () => { + // QNBS-v3: PR6 CodeAnt — reviewItems are derived 1:1 from edits, so the score with BOTH + // present must equal the score from the single canonical source, never the inflated sum. + const reviewItems = Array.from({ length: 3 }, (_, i) => ({ + id: `ri-${i}`, + stage: 'structural' as const, + type: 'structuralEdit' as const, + severity: 'info' as const, + description: 'derived from edit', + status: 'pending' as const, + confidence: 0.8, + createdAt: new Date(0).toISOString(), + })); + const edits = [{ id: 'e1' }, { id: 'e2' }, { id: 'e3' }]; + + const both = agent.evaluate('structural', { reviewItems, agentOutput: { edits } }); + const editsOnly = agent.evaluate('structural', { reviewItems: [], agentOutput: { edits } }); + // max(3,3) === 3 → identical score; the old `edits + reviewItems` sum (6) would inflate it. + expect(both.qualityScore).toBe(editsOnly.qualityScore); + }); }); // --------------------------------------------------------------------------- @@ -446,7 +467,24 @@ describe('SupervisorAgent', () => { expect(result.pass).toBe(false); expect(result.retryRecommended).toBe(true); expect(result.qualityScore).toBeLessThan(70); - expect(result.reasons.some((r) => r.includes('zero grammar issues'))).toBe(true); + expect(result.reasons.some((r) => r.includes('grammar/style/technical/legal'))).toBe(true); + }); + + it('does NOT flag a long manuscript when proof found non-grammar (style/legal) issues', () => { + // QNBS-v3: PR6 CodeAnt — proof signal must count style/technical/legal findings, not only + // grammar; a clean-grammar report with real style/legal findings is NOT a fallback. + const longContent = 'word '.repeat(510).trim(); + const longAgent = new SupervisorAgent(makeContext(longContent)); + const result = longAgent.evaluate('proof', { + reviewItems: [], + agentOutput: { + overallPass: true, + grammar: { issues: [] }, + style: { issues: [{ id: 's1' }, { id: 's2' }] }, + legal: { warnings: [{ id: 'l1' }] }, + }, + }); + expect(result.pass).toBe(true); }); it('passes for short manuscript with overallPass and zero grammar issues (under 500 words)', () => {