@oscharko-dev/keiko-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -0
- package/dist/assistant-response.d.ts +6 -0
- package/dist/assistant-response.d.ts.map +1 -0
- package/dist/assistant-response.js +12 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +245 -0
- package/dist/chat-handlers.d.ts +48 -0
- package/dist/chat-handlers.d.ts.map +1 -0
- package/dist/chat-handlers.js +821 -0
- package/dist/chat-stream-handlers.d.ts +4 -0
- package/dist/chat-stream-handlers.d.ts.map +1 -0
- package/dist/chat-stream-handlers.js +136 -0
- package/dist/conversation-prompt.d.ts +8 -0
- package/dist/conversation-prompt.d.ts.map +1 -0
- package/dist/conversation-prompt.js +36 -0
- package/dist/conversation-validation.d.ts +26 -0
- package/dist/conversation-validation.d.ts.map +1 -0
- package/dist/conversation-validation.js +125 -0
- package/dist/credentialPersistence.d.ts +23 -0
- package/dist/credentialPersistence.d.ts.map +1 -0
- package/dist/credentialPersistence.js +93 -0
- package/dist/credentialVault.d.ts +30 -0
- package/dist/credentialVault.d.ts.map +1 -0
- package/dist/credentialVault.js +206 -0
- package/dist/csp.d.ts +3 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +75 -0
- package/dist/deps.d.ts +78 -0
- package/dist/deps.d.ts.map +1 -0
- package/dist/deps.js +457 -0
- package/dist/editor/agentRoutes.d.ts +7 -0
- package/dist/editor/agentRoutes.d.ts.map +1 -0
- package/dist/editor/agentRoutes.js +197 -0
- package/dist/editor/assuredGateRunner.d.ts +36 -0
- package/dist/editor/assuredGateRunner.d.ts.map +1 -0
- package/dist/editor/assuredGateRunner.js +100 -0
- package/dist/editor/assuredPreFilter.d.ts +34 -0
- package/dist/editor/assuredPreFilter.d.ts.map +1 -0
- package/dist/editor/assuredPreFilter.js +134 -0
- package/dist/editor/assuredPreFilterRunner.d.ts +31 -0
- package/dist/editor/assuredPreFilterRunner.d.ts.map +1 -0
- package/dist/editor/assuredPreFilterRunner.js +312 -0
- package/dist/editor/builtinLanguageProviders.d.ts +6 -0
- package/dist/editor/builtinLanguageProviders.d.ts.map +1 -0
- package/dist/editor/builtinLanguageProviders.js +221 -0
- package/dist/editor/codingContext.d.ts +12 -0
- package/dist/editor/codingContext.d.ts.map +1 -0
- package/dist/editor/codingContext.js +121 -0
- package/dist/editor/codingContextEvidence.d.ts +7 -0
- package/dist/editor/codingContextEvidence.d.ts.map +1 -0
- package/dist/editor/codingContextEvidence.js +52 -0
- package/dist/editor/codingContextProviders.d.ts +36 -0
- package/dist/editor/codingContextProviders.d.ts.map +1 -0
- package/dist/editor/codingContextProviders.js +348 -0
- package/dist/editor/completionModelEvidence.d.ts +16 -0
- package/dist/editor/completionModelEvidence.d.ts.map +1 -0
- package/dist/editor/completionModelEvidence.js +50 -0
- package/dist/editor/completionRoutes.d.ts +37 -0
- package/dist/editor/completionRoutes.d.ts.map +1 -0
- package/dist/editor/completionRoutes.js +411 -0
- package/dist/editor/contextRoutes.d.ts +6 -0
- package/dist/editor/contextRoutes.d.ts.map +1 -0
- package/dist/editor/contextRoutes.js +411 -0
- package/dist/editor/disposableAssuredExecution.d.ts +22 -0
- package/dist/editor/disposableAssuredExecution.d.ts.map +1 -0
- package/dist/editor/disposableAssuredExecution.js +57 -0
- package/dist/editor/editorCompletionModel.d.ts +47 -0
- package/dist/editor/editorCompletionModel.d.ts.map +1 -0
- package/dist/editor/editorCompletionModel.js +156 -0
- package/dist/editor/editorInlineCompletionModel.d.ts +34 -0
- package/dist/editor/editorInlineCompletionModel.d.ts.map +1 -0
- package/dist/editor/editorInlineCompletionModel.js +112 -0
- package/dist/editor/editorModelTokenBudget.d.ts +46 -0
- package/dist/editor/editorModelTokenBudget.d.ts.map +1 -0
- package/dist/editor/editorModelTokenBudget.js +121 -0
- package/dist/editor/inlineCompletionRateLimiter.d.ts +19 -0
- package/dist/editor/inlineCompletionRateLimiter.d.ts.map +1 -0
- package/dist/editor/inlineCompletionRateLimiter.js +46 -0
- package/dist/editor/inlineCompletionRoutes.d.ts +26 -0
- package/dist/editor/inlineCompletionRoutes.d.ts.map +1 -0
- package/dist/editor/inlineCompletionRoutes.js +404 -0
- package/dist/editor/inlineCompletionTelemetryEvidence.d.ts +5 -0
- package/dist/editor/inlineCompletionTelemetryEvidence.d.ts.map +1 -0
- package/dist/editor/inlineCompletionTelemetryEvidence.js +42 -0
- package/dist/editor/languageCancellation.d.ts +19 -0
- package/dist/editor/languageCancellation.d.ts.map +1 -0
- package/dist/editor/languageCancellation.js +48 -0
- package/dist/editor/languageProvider.d.ts +39 -0
- package/dist/editor/languageProvider.d.ts.map +1 -0
- package/dist/editor/languageProvider.js +11 -0
- package/dist/editor/languageRoutes.d.ts +15 -0
- package/dist/editor/languageRoutes.d.ts.map +1 -0
- package/dist/editor/languageRoutes.js +106 -0
- package/dist/editor/languageSanitize.d.ts +8 -0
- package/dist/editor/languageSanitize.d.ts.map +1 -0
- package/dist/editor/languageSanitize.js +101 -0
- package/dist/editor/languageService.d.ts +36 -0
- package/dist/editor/languageService.d.ts.map +1 -0
- package/dist/editor/languageService.js +93 -0
- package/dist/editor/languageServiceHost.d.ts +14 -0
- package/dist/editor/languageServiceHost.d.ts.map +1 -0
- package/dist/editor/languageServiceHost.js +242 -0
- package/dist/editor/localKnowledgeRetrieval.d.ts +21 -0
- package/dist/editor/localKnowledgeRetrieval.d.ts.map +1 -0
- package/dist/editor/localKnowledgeRetrieval.js +44 -0
- package/dist/editor/patchApplyEvidence.d.ts +21 -0
- package/dist/editor/patchApplyEvidence.d.ts.map +1 -0
- package/dist/editor/patchApplyEvidence.js +87 -0
- package/dist/editor/patchApplyRoutes.d.ts +16 -0
- package/dist/editor/patchApplyRoutes.d.ts.map +1 -0
- package/dist/editor/patchApplyRoutes.js +307 -0
- package/dist/editor/postApplyVerification.d.ts +42 -0
- package/dist/editor/postApplyVerification.d.ts.map +1 -0
- package/dist/editor/postApplyVerification.js +177 -0
- package/dist/editor/testGenerationEvidence.d.ts +6 -0
- package/dist/editor/testGenerationEvidence.d.ts.map +1 -0
- package/dist/editor/testGenerationEvidence.js +72 -0
- package/dist/editor/testGenerationPatch.d.ts +10 -0
- package/dist/editor/testGenerationPatch.d.ts.map +1 -0
- package/dist/editor/testGenerationPatch.js +66 -0
- package/dist/editor/testGenerationRoutes.d.ts +21 -0
- package/dist/editor/testGenerationRoutes.d.ts.map +1 -0
- package/dist/editor/testGenerationRoutes.js +254 -0
- package/dist/editor/testGenerationRunner.d.ts +23 -0
- package/dist/editor/testGenerationRunner.d.ts.map +1 -0
- package/dist/editor/testGenerationRunner.js +120 -0
- package/dist/editor/textOffsets.d.ts +6 -0
- package/dist/editor/textOffsets.d.ts.map +1 -0
- package/dist/editor/textOffsets.js +82 -0
- package/dist/editor/typescriptLanguageProvider.d.ts +3 -0
- package/dist/editor/typescriptLanguageProvider.d.ts.map +1 -0
- package/dist/editor/typescriptLanguageProvider.js +217 -0
- package/dist/evidence.d.ts +28 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +145 -0
- package/dist/files-deny.d.ts +3 -0
- package/dist/files-deny.d.ts.map +1 -0
- package/dist/files-deny.js +12 -0
- package/dist/files.d.ts +97 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +733 -0
- package/dist/gateway-setup.d.ts +10 -0
- package/dist/gateway-setup.d.ts.map +1 -0
- package/dist/gateway-setup.js +896 -0
- package/dist/governed-workflow.d.ts +17 -0
- package/dist/governed-workflow.d.ts.map +1 -0
- package/dist/governed-workflow.js +147 -0
- package/dist/grounded-answer.d.ts +12 -0
- package/dist/grounded-answer.d.ts.map +1 -0
- package/dist/grounded-answer.js +69 -0
- package/dist/grounded-context-index.d.ts +25 -0
- package/dist/grounded-context-index.d.ts.map +1 -0
- package/dist/grounded-context-index.js +169 -0
- package/dist/grounded-document-evidence.d.ts +28 -0
- package/dist/grounded-document-evidence.d.ts.map +1 -0
- package/dist/grounded-document-evidence.js +430 -0
- package/dist/grounded-handoff.d.ts +4 -0
- package/dist/grounded-handoff.d.ts.map +1 -0
- package/dist/grounded-handoff.js +445 -0
- package/dist/grounded-orchestrator.d.ts +43 -0
- package/dist/grounded-orchestrator.d.ts.map +1 -0
- package/dist/grounded-orchestrator.js +1445 -0
- package/dist/grounded-prompt.d.ts +2 -0
- package/dist/grounded-prompt.d.ts.map +1 -0
- package/dist/grounded-prompt.js +17 -0
- package/dist/grounded-qa-hybrid.d.ts +36 -0
- package/dist/grounded-qa-hybrid.d.ts.map +1 -0
- package/dist/grounded-qa-hybrid.js +762 -0
- package/dist/grounded-qa-multi-source.d.ts +38 -0
- package/dist/grounded-qa-multi-source.d.ts.map +1 -0
- package/dist/grounded-qa-multi-source.js +461 -0
- package/dist/grounded-qa.d.ts +45 -0
- package/dist/grounded-qa.d.ts.map +1 -0
- package/dist/grounded-qa.js +877 -0
- package/dist/grounded-rerank.d.ts +26 -0
- package/dist/grounded-rerank.d.ts.map +1 -0
- package/dist/grounded-rerank.js +72 -0
- package/dist/grounded-turn-registry.d.ts +23 -0
- package/dist/grounded-turn-registry.d.ts.map +1 -0
- package/dist/grounded-turn-registry.js +102 -0
- package/dist/headers.d.ts +3 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +22 -0
- package/dist/host-check.d.ts +3 -0
- package/dist/host-check.d.ts.map +1 -0
- package/dist/host-check.js +58 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/load-csp.d.ts +3 -0
- package/dist/load-csp.d.ts.map +1 -0
- package/dist/load-csp.js +100 -0
- package/dist/local-knowledge-grounded-qa.d.ts +42 -0
- package/dist/local-knowledge-grounded-qa.d.ts.map +1 -0
- package/dist/local-knowledge-grounded-qa.js +678 -0
- package/dist/local-knowledge-handlers.d.ts +24 -0
- package/dist/local-knowledge-handlers.d.ts.map +1 -0
- package/dist/local-knowledge-handlers.js +1285 -0
- package/dist/local-knowledge-indexing-registry.d.ts +13 -0
- package/dist/local-knowledge-indexing-registry.d.ts.map +1 -0
- package/dist/local-knowledge-indexing-registry.js +53 -0
- package/dist/localKnowledgeKeyProvider.d.ts +11 -0
- package/dist/localKnowledgeKeyProvider.d.ts.map +1 -0
- package/dist/localKnowledgeKeyProvider.js +48 -0
- package/dist/memory-audit-event-builders.d.ts +21 -0
- package/dist/memory-audit-event-builders.d.ts.map +1 -0
- package/dist/memory-audit-event-builders.js +187 -0
- package/dist/memory-audit-handler.d.ts +23 -0
- package/dist/memory-audit-handler.d.ts.map +1 -0
- package/dist/memory-audit-handler.js +191 -0
- package/dist/memory-capture-policy.d.ts +10 -0
- package/dist/memory-capture-policy.d.ts.map +1 -0
- package/dist/memory-capture-policy.js +44 -0
- package/dist/memory-consolidation-handlers.d.ts +6 -0
- package/dist/memory-consolidation-handlers.d.ts.map +1 -0
- package/dist/memory-consolidation-handlers.js +491 -0
- package/dist/memory-consolidation-registry.d.ts +47 -0
- package/dist/memory-consolidation-registry.d.ts.map +1 -0
- package/dist/memory-consolidation-registry.js +106 -0
- package/dist/memory-conv-handlers.d.ts +8 -0
- package/dist/memory-conv-handlers.d.ts.map +1 -0
- package/dist/memory-conv-handlers.js +369 -0
- package/dist/memory-conversation-context.d.ts +13 -0
- package/dist/memory-conversation-context.d.ts.map +1 -0
- package/dist/memory-conversation-context.js +22 -0
- package/dist/memory-diagnostics.d.ts +29 -0
- package/dist/memory-diagnostics.d.ts.map +1 -0
- package/dist/memory-diagnostics.js +122 -0
- package/dist/memory-embedding.d.ts +21 -0
- package/dist/memory-embedding.d.ts.map +1 -0
- package/dist/memory-embedding.js +264 -0
- package/dist/memory-handlers.d.ts +19 -0
- package/dist/memory-handlers.d.ts.map +1 -0
- package/dist/memory-handlers.js +1204 -0
- package/dist/memory-maintenance-handlers.d.ts +35 -0
- package/dist/memory-maintenance-handlers.d.ts.map +1 -0
- package/dist/memory-maintenance-handlers.js +219 -0
- package/dist/memory-record-builders.d.ts +4 -0
- package/dist/memory-record-builders.d.ts.map +1 -0
- package/dist/memory-record-builders.js +19 -0
- package/dist/memory-retention.d.ts +31 -0
- package/dist/memory-retention.d.ts.map +1 -0
- package/dist/memory-retention.js +151 -0
- package/dist/memory-retrieval-signals.d.ts +12 -0
- package/dist/memory-retrieval-signals.d.ts.map +1 -0
- package/dist/memory-retrieval-signals.js +100 -0
- package/dist/memory-salience.d.ts +12 -0
- package/dist/memory-salience.d.ts.map +1 -0
- package/dist/memory-salience.js +154 -0
- package/dist/memory-scope-sanitizer.d.ts +6 -0
- package/dist/memory-scope-sanitizer.d.ts.map +1 -0
- package/dist/memory-scope-sanitizer.js +106 -0
- package/dist/memory-target-resolver.d.ts +4 -0
- package/dist/memory-target-resolver.d.ts.map +1 -0
- package/dist/memory-target-resolver.js +73 -0
- package/dist/memory-workflow-port.d.ts +14 -0
- package/dist/memory-workflow-port.d.ts.map +1 -0
- package/dist/memory-workflow-port.js +186 -0
- package/dist/private-json.d.ts +3 -0
- package/dist/private-json.d.ts.map +1 -0
- package/dist/private-json.js +62 -0
- package/dist/promptEnhancer/index.d.ts +3 -0
- package/dist/promptEnhancer/index.d.ts.map +1 -0
- package/dist/promptEnhancer/index.js +5 -0
- package/dist/promptEnhancer/orchestrate.d.ts +2 -0
- package/dist/promptEnhancer/orchestrate.d.ts.map +1 -0
- package/dist/promptEnhancer/orchestrate.js +5 -0
- package/dist/promptEnhancer/routes.d.ts +9 -0
- package/dist/promptEnhancer/routes.d.ts.map +1 -0
- package/dist/promptEnhancer/routes.js +205 -0
- package/dist/qualityIntelligence/capsuleAdapter.d.ts +27 -0
- package/dist/qualityIntelligence/capsuleAdapter.d.ts.map +1 -0
- package/dist/qualityIntelligence/capsuleAdapter.js +57 -0
- package/dist/qualityIntelligence/connectorAuthorization.d.ts +22 -0
- package/dist/qualityIntelligence/connectorAuthorization.d.ts.map +1 -0
- package/dist/qualityIntelligence/connectorAuthorization.js +35 -0
- package/dist/qualityIntelligence/connectorErrors.d.ts +16 -0
- package/dist/qualityIntelligence/connectorErrors.d.ts.map +1 -0
- package/dist/qualityIntelligence/connectorErrors.js +56 -0
- package/dist/qualityIntelligence/connectorRoutes.d.ts +7 -0
- package/dist/qualityIntelligence/connectorRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/connectorRoutes.js +167 -0
- package/dist/qualityIntelligence/editRoutes.d.ts +5 -0
- package/dist/qualityIntelligence/editRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/editRoutes.js +293 -0
- package/dist/qualityIntelligence/exportAssembly.d.ts +22 -0
- package/dist/qualityIntelligence/exportAssembly.d.ts.map +1 -0
- package/dist/qualityIntelligence/exportAssembly.js +352 -0
- package/dist/qualityIntelligence/exportRoutes.d.ts +5 -0
- package/dist/qualityIntelligence/exportRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/exportRoutes.js +320 -0
- package/dist/qualityIntelligence/figma/figmaConcurrency.d.ts +8 -0
- package/dist/qualityIntelligence/figma/figmaConcurrency.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaConcurrency.js +34 -0
- package/dist/qualityIntelligence/figma/figmaConnector.d.ts +65 -0
- package/dist/qualityIntelligence/figma/figmaConnector.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaConnector.js +184 -0
- package/dist/qualityIntelligence/figma/figmaConnectorAudit.d.ts +52 -0
- package/dist/qualityIntelligence/figma/figmaConnectorAudit.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaConnectorAudit.js +63 -0
- package/dist/qualityIntelligence/figma/figmaConnectorErrors.d.ts +31 -0
- package/dist/qualityIntelligence/figma/figmaConnectorErrors.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaConnectorErrors.js +220 -0
- package/dist/qualityIntelligence/figma/figmaConnectorMetrics.d.ts +44 -0
- package/dist/qualityIntelligence/figma/figmaConnectorMetrics.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaConnectorMetrics.js +49 -0
- package/dist/qualityIntelligence/figma/figmaConsent.d.ts +39 -0
- package/dist/qualityIntelligence/figma/figmaConsent.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaConsent.js +62 -0
- package/dist/qualityIntelligence/figma/figmaHttpPort.d.ts +28 -0
- package/dist/qualityIntelligence/figma/figmaHttpPort.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaHttpPort.js +70 -0
- package/dist/qualityIntelligence/figma/figmaObservedActions.d.ts +49 -0
- package/dist/qualityIntelligence/figma/figmaObservedActions.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaObservedActions.js +89 -0
- package/dist/qualityIntelligence/figma/figmaReadiness.d.ts +32 -0
- package/dist/qualityIntelligence/figma/figmaReadiness.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaReadiness.js +67 -0
- package/dist/qualityIntelligence/figma/figmaRenderPort.d.ts +29 -0
- package/dist/qualityIntelligence/figma/figmaRenderPort.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaRenderPort.js +93 -0
- package/dist/qualityIntelligence/figma/figmaResnapshot.d.ts +28 -0
- package/dist/qualityIntelligence/figma/figmaResnapshot.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaResnapshot.js +38 -0
- package/dist/qualityIntelligence/figma/figmaRetry.d.ts +31 -0
- package/dist/qualityIntelligence/figma/figmaRetry.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaRetry.js +62 -0
- package/dist/qualityIntelligence/figma/figmaScopeRef.d.ts +9 -0
- package/dist/qualityIntelligence/figma/figmaScopeRef.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaScopeRef.js +18 -0
- package/dist/qualityIntelligence/figma/figmaScopedPagination.d.ts +86 -0
- package/dist/qualityIntelligence/figma/figmaScopedPagination.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaScopedPagination.js +308 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.d.ts +31 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.js +314 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotHash.d.ts +18 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotHash.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotHash.js +63 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotTypes.d.ts +65 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotTypes.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaSnapshotTypes.js +13 -0
- package/dist/qualityIntelligence/figma/figmaTokenSource.d.ts +9 -0
- package/dist/qualityIntelligence/figma/figmaTokenSource.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaTokenSource.js +61 -0
- package/dist/qualityIntelligence/figma/figmaTokenStore.d.ts +19 -0
- package/dist/qualityIntelligence/figma/figmaTokenStore.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaTokenStore.js +156 -0
- package/dist/qualityIntelligence/figma/figmaUrl.d.ts +6 -0
- package/dist/qualityIntelligence/figma/figmaUrl.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/figmaUrl.js +36 -0
- package/dist/qualityIntelligence/figma/index.d.ts +20 -0
- package/dist/qualityIntelligence/figma/index.d.ts.map +1 -0
- package/dist/qualityIntelligence/figma/index.js +26 -0
- package/dist/qualityIntelligence/figmaCodegenRoutes.d.ts +28 -0
- package/dist/qualityIntelligence/figmaCodegenRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/figmaCodegenRoutes.js +165 -0
- package/dist/qualityIntelligence/figmaSnapshotAdapter.d.ts +55 -0
- package/dist/qualityIntelligence/figmaSnapshotAdapter.d.ts.map +1 -0
- package/dist/qualityIntelligence/figmaSnapshotAdapter.js +219 -0
- package/dist/qualityIntelligence/figmaSnapshotOrchestration.d.ts +64 -0
- package/dist/qualityIntelligence/figmaSnapshotOrchestration.d.ts.map +1 -0
- package/dist/qualityIntelligence/figmaSnapshotOrchestration.js +203 -0
- package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +112 -0
- package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1063 -0
- package/dist/qualityIntelligence/figmaSnapshotScreenIds.d.ts +19 -0
- package/dist/qualityIntelligence/figmaSnapshotScreenIds.d.ts.map +1 -0
- package/dist/qualityIntelligence/figmaSnapshotScreenIds.js +75 -0
- package/dist/qualityIntelligence/generationPort.d.ts +15 -0
- package/dist/qualityIntelligence/generationPort.d.ts.map +1 -0
- package/dist/qualityIntelligence/generationPort.js +185 -0
- package/dist/qualityIntelligence/handoffErrors.d.ts +9 -0
- package/dist/qualityIntelligence/handoffErrors.d.ts.map +1 -0
- package/dist/qualityIntelligence/handoffErrors.js +21 -0
- package/dist/qualityIntelligence/handoffRoutes.d.ts +15 -0
- package/dist/qualityIntelligence/handoffRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/handoffRoutes.js +341 -0
- package/dist/qualityIntelligence/index.d.ts +17 -0
- package/dist/qualityIntelligence/index.d.ts.map +1 -0
- package/dist/qualityIntelligence/index.js +36 -0
- package/dist/qualityIntelligence/judgePort.d.ts +30 -0
- package/dist/qualityIntelligence/judgePort.d.ts.map +1 -0
- package/dist/qualityIntelligence/judgePort.js +326 -0
- package/dist/qualityIntelligence/modelSelection.d.ts +58 -0
- package/dist/qualityIntelligence/modelSelection.d.ts.map +1 -0
- package/dist/qualityIntelligence/modelSelection.js +148 -0
- package/dist/qualityIntelligence/reCheckRoutes.d.ts +6 -0
- package/dist/qualityIntelligence/reCheckRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/reCheckRoutes.js +1157 -0
- package/dist/qualityIntelligence/retentionEnforcement.d.ts +13 -0
- package/dist/qualityIntelligence/retentionEnforcement.d.ts.map +1 -0
- package/dist/qualityIntelligence/retentionEnforcement.js +47 -0
- package/dist/qualityIntelligence/retentionRoutes.d.ts +8 -0
- package/dist/qualityIntelligence/retentionRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/retentionRoutes.js +74 -0
- package/dist/qualityIntelligence/reviewRoutes.d.ts +5 -0
- package/dist/qualityIntelligence/reviewRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/reviewRoutes.js +145 -0
- package/dist/qualityIntelligence/reviewStore.d.ts +75 -0
- package/dist/qualityIntelligence/reviewStore.d.ts.map +1 -0
- package/dist/qualityIntelligence/reviewStore.js +170 -0
- package/dist/qualityIntelligence/runExecution.d.ts +36 -0
- package/dist/qualityIntelligence/runExecution.d.ts.map +1 -0
- package/dist/qualityIntelligence/runExecution.js +180 -0
- package/dist/qualityIntelligence/runIngestion.d.ts +70 -0
- package/dist/qualityIntelligence/runIngestion.d.ts.map +1 -0
- package/dist/qualityIntelligence/runIngestion.js +1235 -0
- package/dist/qualityIntelligence/runRegistry.d.ts +31 -0
- package/dist/qualityIntelligence/runRegistry.d.ts.map +1 -0
- package/dist/qualityIntelligence/runRegistry.js +66 -0
- package/dist/qualityIntelligence/runRoutes.d.ts +16 -0
- package/dist/qualityIntelligence/runRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/runRoutes.js +357 -0
- package/dist/qualityIntelligence/traceabilityRoutes.d.ts +5 -0
- package/dist/qualityIntelligence/traceabilityRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/traceabilityRoutes.js +173 -0
- package/dist/qualityIntelligence/uiRoutes.d.ts +7 -0
- package/dist/qualityIntelligence/uiRoutes.d.ts.map +1 -0
- package/dist/qualityIntelligence/uiRoutes.js +336 -0
- package/dist/read-handlers.d.ts +9 -0
- package/dist/read-handlers.d.ts.map +1 -0
- package/dist/read-handlers.js +265 -0
- package/dist/relationship-handlers.d.ts +191 -0
- package/dist/relationship-handlers.d.ts.map +1 -0
- package/dist/relationship-handlers.js +0 -0
- package/dist/routes.d.ts +37 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +507 -0
- package/dist/run-engine.d.ts +25 -0
- package/dist/run-engine.d.ts.map +1 -0
- package/dist/run-engine.js +385 -0
- package/dist/run-handlers.d.ts +9 -0
- package/dist/run-handlers.d.ts.map +1 -0
- package/dist/run-handlers.js +465 -0
- package/dist/run-request.d.ts +17 -0
- package/dist/run-request.d.ts.map +1 -0
- package/dist/run-request.js +219 -0
- package/dist/runs.d.ts +47 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +100 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +152 -0
- package/dist/sink.d.ts +28 -0
- package/dist/sink.d.ts.map +1 -0
- package/dist/sink.js +80 -0
- package/dist/sse-write.d.ts +9 -0
- package/dist/sse-write.d.ts.map +1 -0
- package/dist/sse-write.js +26 -0
- package/dist/sse.d.ts +8 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +27 -0
- package/dist/static.d.ts +5 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +76 -0
- package/dist/store/chats.d.ts +17 -0
- package/dist/store/chats.d.ts.map +1 -0
- package/dist/store/chats.js +624 -0
- package/dist/store/db.d.ts +11 -0
- package/dist/store/db.d.ts.map +1 -0
- package/dist/store/db.js +203 -0
- package/dist/store/errors.d.ts +13 -0
- package/dist/store/errors.d.ts.map +1 -0
- package/dist/store/errors.js +30 -0
- package/dist/store/index.d.ts +7 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +6 -0
- package/dist/store/messages.d.ts +8 -0
- package/dist/store/messages.d.ts.map +1 -0
- package/dist/store/messages.js +149 -0
- package/dist/store/paths.d.ts +5 -0
- package/dist/store/paths.d.ts.map +1 -0
- package/dist/store/paths.js +84 -0
- package/dist/store/projects.d.ts +8 -0
- package/dist/store/projects.d.ts.map +1 -0
- package/dist/store/projects.js +59 -0
- package/dist/store/relationship-audit.d.ts +42 -0
- package/dist/store/relationship-audit.d.ts.map +1 -0
- package/dist/store/relationship-audit.js +155 -0
- package/dist/store/relationships.d.ts +191 -0
- package/dist/store/relationships.d.ts.map +1 -0
- package/dist/store/relationships.js +724 -0
- package/dist/store/schema.d.ts +4 -0
- package/dist/store/schema.d.ts.map +1 -0
- package/dist/store/schema.js +220 -0
- package/dist/store/types.d.ts +29 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +8 -0
- package/dist/store/validation.d.ts +7 -0
- package/dist/store/validation.d.ts.map +1 -0
- package/dist/store/validation.js +117 -0
- package/dist/store-handlers.d.ts +17 -0
- package/dist/store-handlers.d.ts.map +1 -0
- package/dist/store-handlers.js +872 -0
- package/dist/terminal-errors.d.ts +22 -0
- package/dist/terminal-errors.d.ts.map +1 -0
- package/dist/terminal-errors.js +45 -0
- package/dist/terminal-evidence.d.ts +21 -0
- package/dist/terminal-evidence.d.ts.map +1 -0
- package/dist/terminal-evidence.js +65 -0
- package/dist/terminal-routes.d.ts +10 -0
- package/dist/terminal-routes.d.ts.map +1 -0
- package/dist/terminal-routes.js +219 -0
- package/dist/terminal.d.ts +68 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +855 -0
- package/package.json +52 -0
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
// Quality Intelligence run-source ingestion (Epic #270, Issue #278).
|
|
2
|
+
//
|
|
3
|
+
// Converts the inline sources of a start-run request into content-bearing evidence atoms +
|
|
4
|
+
// browser-safe source envelopes, reusing the pure-domain ingestion + hardening helpers from
|
|
5
|
+
// `@oscharko-dev/keiko-quality-intelligence`. The server tier owns IO (it is the only layer that
|
|
6
|
+
// may touch the filesystem); the pure domain owns splitting + hashing. Oversize and unsupported
|
|
7
|
+
// inputs fail with user-actionable errors (#278 AC) before any model prompt is built.
|
|
8
|
+
import { realpathSync } from "node:fs";
|
|
9
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
10
|
+
import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
|
|
11
|
+
import { redact, sha256Hex } from "@oscharko-dev/keiko-security";
|
|
12
|
+
import { QualityIntelligenceGeneration, QualityIntelligenceHardening, QualityIntelligenceFigma, isUnsafeFormatCodePoint, stripUnsafeFormatChars, } from "@oscharko-dev/keiko-quality-intelligence";
|
|
13
|
+
import { detectWorkspaceAt, discoverWithStats, buildContextPackFromFiles, readWorkspaceFile, isDenied, DEFAULT_CONTEXT_REQUEST, DEFAULT_READ_OPTIONS, WorkspaceError, FileTooLargeError, PathDeniedError, PathEscapeError, WorkspaceReadError, } from "@oscharko-dev/keiko-workspace";
|
|
14
|
+
const MAX_TOTAL_ATOMS = 120;
|
|
15
|
+
const MAX_LABEL_CHARS = 120;
|
|
16
|
+
// Mirrors the Chat multi-source cap (MAX_CONNECTED_SOURCES / MAX_SCOPES = 16). Sources beyond this
|
|
17
|
+
// are dropped before ingestion with a user-actionable coverage notice (Epic #729, Issue #730).
|
|
18
|
+
const MAX_QI_SOURCES = 16;
|
|
19
|
+
/**
|
|
20
|
+
* Fair per-source atom budget — mirrors Chat's splitExplorationBudget floor-division semantics
|
|
21
|
+
* (grounded-qa-multi-source.ts): the global budget is shared evenly so no single source starves the
|
|
22
|
+
* others. A single source keeps the whole budget.
|
|
23
|
+
*/
|
|
24
|
+
function perSourceAtomBudget(total, sourceCount) {
|
|
25
|
+
if (sourceCount <= 1)
|
|
26
|
+
return total;
|
|
27
|
+
return Math.max(1, Math.floor(total / sourceCount));
|
|
28
|
+
}
|
|
29
|
+
// Global per-run evidence byte budget. Each source kind (workspace / file / capsule /
|
|
30
|
+
// figma-snapshot) was previously allowed ~192KB INDEPENDENTLY, so N large sources summed to N×192KB
|
|
31
|
+
// and blew the model prompt cap (MAX_PROMPT_BYTES = 256KB) — failing the entire N+1 run with
|
|
32
|
+
// QI_PROMPT_TOO_LARGE (Epic #729 headline). The byte budget is now a single global pool split fairly
|
|
33
|
+
// across sources, mirroring the atom-budget split, so the merged evidence text stays bounded
|
|
34
|
+
// regardless of N. A single source keeps the full budget (identical to the prior single-source
|
|
35
|
+
// behaviour). figma-snapshot uses the same split via figmaScreenDocs (mirrors processCapsuleDocs).
|
|
36
|
+
const EVIDENCE_BUDGET_BYTES = 196_608;
|
|
37
|
+
// Never starve a source below this many bytes — a tiny share is still usable context.
|
|
38
|
+
const MIN_SOURCE_BUDGET_BYTES = 4_096;
|
|
39
|
+
// Bound productive vision calls for one Figma snapshot source. The deterministic structural
|
|
40
|
+
// baseline still covers every parseable screen; vision hints are additive and sampled first-in-order.
|
|
41
|
+
const MAX_FIGMA_VISION_AUGMENTED_SCREENS = 12;
|
|
42
|
+
/**
|
|
43
|
+
* Fair per-source UTF-8 byte budget — the byte analogue of {@link perSourceAtomBudget}. Floor-divides
|
|
44
|
+
* the global evidence byte pool across sources (Chat byte-split parity) with a non-zero floor so the
|
|
45
|
+
* summed canonical text of all sources stays under the model prompt ceiling and no single large
|
|
46
|
+
* source can exhaust the budget for the others (Epic #729 / #730 multi-source containment).
|
|
47
|
+
*/
|
|
48
|
+
function perSourceByteBudget(sourceCount) {
|
|
49
|
+
if (sourceCount <= 1)
|
|
50
|
+
return EVIDENCE_BUDGET_BYTES;
|
|
51
|
+
return Math.max(MIN_SOURCE_BUDGET_BYTES, Math.floor(EVIDENCE_BUDGET_BYTES / sourceCount));
|
|
52
|
+
}
|
|
53
|
+
export class QiIngestionError extends Error {
|
|
54
|
+
code;
|
|
55
|
+
constructor(code, message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.name = "QiIngestionError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Credential token shapes mirrored from keiko-contracts `fieldLooksUnsafe` so a connected
|
|
62
|
+
// source's display label can never echo a secret back to the browser-surfaced envelope
|
|
63
|
+
// (#277/#278 envelope display-surface invariant — the label is the only user-derived envelope
|
|
64
|
+
// field; localRef/origin/integrityHash are server-built and hash-derived).
|
|
65
|
+
const CREDENTIAL_LABEL_SHAPES = [
|
|
66
|
+
/AKIA[0-9A-Z]{12,}/gu,
|
|
67
|
+
/(?:ghp_|gho_|github_pat_)[A-Za-z0-9_]{20,}/gu,
|
|
68
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/gu,
|
|
69
|
+
/sk-[A-Za-z0-9]{16,}/gu,
|
|
70
|
+
/\bBearer\s+\S+/giu,
|
|
71
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/gu,
|
|
72
|
+
];
|
|
73
|
+
// Make a source label single-line AND spoof-safe with one code-point scan (the `no-control-regex`
|
|
74
|
+
// lint rule forbids a control-range regex literal, and a scan is the established in-package idiom —
|
|
75
|
+
// mirrors generationPort.scrubEvidenceText):
|
|
76
|
+
// - C0 controls (incl. tab/newline/CR) and DEL become a SPACE, so a multi-line or control-laden
|
|
77
|
+
// label can never glue a second line of content into the streamed displayLabel (#277/#278).
|
|
78
|
+
// - Bidi overrides/isolates, zero-width/BOM, LRM/RLM, the Arabic letter mark, and C1 controls are
|
|
79
|
+
// DROPPED outright. These are invisible or reorder surrounding text, so a source filename /
|
|
80
|
+
// capsule id cannot smuggle a right-to-left or zero-width spoof into the browser-streamed
|
|
81
|
+
// envelope display surface. The drop set is the SHARED `isUnsafeFormatCodePoint` predicate used
|
|
82
|
+
// by the candidate-text scrubber (keiko-quality-intelligence stripUnsafeFormatChars), so the
|
|
83
|
+
// source-label path is symmetric with the persisted/exported candidate-text path (Epic #729;
|
|
84
|
+
// the bidi/zero-width display-hygiene class of #280/#284). C0/DEL are handled first (→ space)
|
|
85
|
+
// because a single-line label spaces line breaks rather than gluing them.
|
|
86
|
+
function stripUnsafeLabelChars(value) {
|
|
87
|
+
let out = "";
|
|
88
|
+
for (const ch of value) {
|
|
89
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
90
|
+
if (cp <= 0x1f || cp === 0x7f) {
|
|
91
|
+
out += " ";
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (isUnsafeFormatCodePoint(cp)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
out += ch;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
const sanitiseLabel = (label) => {
|
|
102
|
+
// Strip any URL authority — ANY scheme (http, file, s3, ftp, …), not just http(s) — plus the
|
|
103
|
+
// well-known credential token shapes, so a browser-supplied label never carries a URL or secret
|
|
104
|
+
// into the envelope display surface that is streamed back to the client (#277/#278).
|
|
105
|
+
let cleaned = label.replace(/[a-z][a-z0-9+.-]*:\/\/\S+/giu, " ");
|
|
106
|
+
for (const shape of CREDENTIAL_LABEL_SHAPES)
|
|
107
|
+
cleaned = cleaned.replace(shape, " ");
|
|
108
|
+
// Map control characters (newline, CR, tab, NUL, DEL, …) to spaces and DROP bidi-override,
|
|
109
|
+
// zero-width, BOM, and C1 spoofing code points so a multi-line, control-laden, or
|
|
110
|
+
// visually-reordered label can never carry a second line of content into — or spoof the reading
|
|
111
|
+
// order of — the browser-streamed envelope displayLabel. Without the control→space step the
|
|
112
|
+
// absolute-path basename-collapse below (which splits on "/" only) would keep a trailing
|
|
113
|
+
// "\n<more content>" glued inside the final path segment, defeating the basename defence; without
|
|
114
|
+
// the bidi/zero-width drop a crafted filename could reorder the displayed label (#277/#278
|
|
115
|
+
// envelope display-surface invariant; Epic #729 symmetry with the candidate-text scrubber).
|
|
116
|
+
cleaned = stripUnsafeLabelChars(cleaned);
|
|
117
|
+
cleaned = cleaned.trim();
|
|
118
|
+
// Collapse an absolute POSIX / Windows-drive / UNC path label to its final segment so the
|
|
119
|
+
// display label never leaks the filesystem layout (the basename is the useful display token).
|
|
120
|
+
if (/^(?:\/|[A-Za-z]:[\\/]|\\\\)/u.test(cleaned)) {
|
|
121
|
+
const segments = cleaned.split(/[\\/]/u).filter((s) => s.length > 0);
|
|
122
|
+
cleaned = (segments[segments.length - 1] ?? "").trim();
|
|
123
|
+
}
|
|
124
|
+
const safe = cleaned.length === 0 ? "Untitled source" : cleaned;
|
|
125
|
+
return safe.length > MAX_LABEL_CHARS ? `${safe.slice(0, MAX_LABEL_CHARS - 1)}…` : safe;
|
|
126
|
+
};
|
|
127
|
+
// Reject a source whose absolute path (any segment) names a denied credential location. isDenied
|
|
128
|
+
// inspects EVERY path segment, so a denied ancestor cannot be hidden by rooting a read deeper. Shared
|
|
129
|
+
// by the folder and single-file paths so both honour the same containment guard (Epic #729 security).
|
|
130
|
+
// Also rejects the symlink variant (assertRealPathNotDenied) so a benign-named link cannot resolve
|
|
131
|
+
// into a protected location.
|
|
132
|
+
function assertNotDenied(absPath, label, noun) {
|
|
133
|
+
if (isDenied(absPath)) {
|
|
134
|
+
throw new QiIngestionError("QI_SOURCE_DENIED", `${noun} "${label}" is in a protected location.`);
|
|
135
|
+
}
|
|
136
|
+
assertRealPathNotDenied(absPath, label, noun);
|
|
137
|
+
}
|
|
138
|
+
// Defense-in-depth against a symlinked workspace root. The keiko-workspace deny gate (readWorkspaceFile)
|
|
139
|
+
// inspects only the path RELATIVE to the realpath'd root, so a denied segment AT or ABOVE the connected
|
|
140
|
+
// root is invisible to it: a benign-named "~/docs" symlink whose real target is "~/.aws" lets a
|
|
141
|
+
// supported file inside it read through to the model, even though the lexical assertNotDenied above sees
|
|
142
|
+
// only "docs". Re-running the deny gate over the REAL (symlink-resolved) absolute path rejects it. The
|
|
143
|
+
// lexical check above already covers the no-symlink case, so this only ADDS denials when realpath
|
|
144
|
+
// diverges into a protected location; a non-existent target surfaces later as NOT_FOUND, so a failed
|
|
145
|
+
// realpath is a deliberate no-op here. (#713 single-file security review: "deny-list still applies";
|
|
146
|
+
// #729 folder-root parity — both ingest paths share this boundary blind spot.)
|
|
147
|
+
function assertRealPathNotDenied(absPath, label, noun) {
|
|
148
|
+
let realPath;
|
|
149
|
+
try {
|
|
150
|
+
realPath = realpathSync(absPath);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (realPath !== absPath && isDenied(realPath)) {
|
|
156
|
+
throw new QiIngestionError("QI_SOURCE_DENIED", `${noun} "${label}" is in a protected location.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Escape the field delimiter ("|") and the escape character ("\") in each user/path-controlled
|
|
160
|
+
// field so a label or content value can never inject a raw delimiter and forge another source's
|
|
161
|
+
// envelope id. The strictly-increasing loop index already disambiguates sources today; escaping
|
|
162
|
+
// makes the pre-image injective on its own — robust even if the fields were ever reordered —
|
|
163
|
+
// closing the latent cross-source provenance-spoofing surface flagged by the #732 composition
|
|
164
|
+
// security audit. A value with no "\" or "|" encodes to itself, so clean labels/paths keep their
|
|
165
|
+
// existing envelope id (and the atom ids derived from it), preserving re-check stability.
|
|
166
|
+
const escapeEnvelopeField = (value) => value.split("\\").join("\\\\").split("|").join("\\|");
|
|
167
|
+
export const envelopeIdFor = (index, label, content) => {
|
|
168
|
+
const digest = sha256Hex(`qi-src-v1|${String(index)}|${escapeEnvelopeField(label)}|${escapeEnvelopeField(content)}`).slice(0, 24);
|
|
169
|
+
return QualityIntelligence.asQualityIntelligenceSourceEnvelopeId(`qi-src-${digest}`);
|
|
170
|
+
};
|
|
171
|
+
const REQUIREMENTS_ENVELOPE_PREFIX = "qi-src-req-";
|
|
172
|
+
const requirementsEnvelopeIdFor = (index) => {
|
|
173
|
+
const digest = sha256Hex(`qi-src-req-v1|${String(index)}`).slice(0, 24);
|
|
174
|
+
return QualityIntelligence.asQualityIntelligenceSourceEnvelopeId(`${REQUIREMENTS_ENVELOPE_PREFIX}${digest}`);
|
|
175
|
+
};
|
|
176
|
+
const stableLocalRef = (prefix, value) => `${prefix}:${sha256Hex(value).slice(0, 24)}`;
|
|
177
|
+
const replacementGroupIdFor = (envelopeId, stableKey) => sha256Hex(`qi-replace-v1|${String(envelopeId)}|${stableKey}`);
|
|
178
|
+
const auditSummaryIdFor = (runId) => QualityIntelligence.asQualityIntelligenceAuditSummaryId(`qi-audit-${sha256Hex(runId).slice(0, 24)}`);
|
|
179
|
+
function ingestRequirements(source, index, registeredAt) {
|
|
180
|
+
const text = typeof source.text === "string" ? source.text : "";
|
|
181
|
+
const oversize = QualityIntelligenceHardening.assertSourceSize(text);
|
|
182
|
+
if (!oversize.ok) {
|
|
183
|
+
throw new QiIngestionError("QI_SOURCE_TOO_LARGE", "A requirements source exceeds the size limit.");
|
|
184
|
+
}
|
|
185
|
+
const label = sanitiseLabel(source.label);
|
|
186
|
+
const envelopeId = requirementsEnvelopeIdFor(index);
|
|
187
|
+
const atoms = QualityIntelligenceGeneration.splitRequirementsIntoAtoms(text, { envelopeId });
|
|
188
|
+
if (atoms.length === 0) {
|
|
189
|
+
throw new QiIngestionError("QI_SOURCE_EMPTY", `Source "${label}" produced no usable requirement statements.`);
|
|
190
|
+
}
|
|
191
|
+
const envelope = {
|
|
192
|
+
id: envelopeId,
|
|
193
|
+
kind: "human-context",
|
|
194
|
+
displayLabel: label,
|
|
195
|
+
provenance: {
|
|
196
|
+
origin: "requirements",
|
|
197
|
+
registeredAt,
|
|
198
|
+
integrityHashSha256Hex: sha256Hex(text),
|
|
199
|
+
},
|
|
200
|
+
localRef: `req:${String(index)}`,
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
envelope,
|
|
204
|
+
atoms: atoms.map((entry, ordinal) => Object.freeze({
|
|
205
|
+
...entry,
|
|
206
|
+
replacementGroupId: replacementGroupIdFor(envelopeId, `requirements:${String(index)}`),
|
|
207
|
+
replacementOrdinal: ordinal,
|
|
208
|
+
})),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const WORKSPACE_BUDGET_BYTES = 196_608;
|
|
212
|
+
const WORKSPACE_MAX_BYTES_PER_FILE = 16_384;
|
|
213
|
+
const CODE_EXTENSION = /\.(?:ts|tsx|js|jsx|mjs|cjs|py|java|go|rb|rs|cs|cpp|cc|c|h|hpp|kt|swift|php|scala|sql)$/iu;
|
|
214
|
+
const REQUIREMENT_TEXT_EXTENSION = /\.(?:md|markdown|txt|text|rst|adoc|asciidoc|org)$/iu;
|
|
215
|
+
const atomKindForPath = (path) => CODE_EXTENSION.test(path) ? "code-fragment" : "document-excerpt";
|
|
216
|
+
const workspaceAtom = (entry, envelopeId) => {
|
|
217
|
+
// entry.excerpt is already redacted by keiko-workspace; prefix the path so the model can
|
|
218
|
+
// attribute generated cases to a file.
|
|
219
|
+
const canonicalText = `${entry.path}\n${entry.excerpt}`;
|
|
220
|
+
// The atom id is derived from the file PATH only — never the file's position in the discovery
|
|
221
|
+
// order (Epic #735 drift correctness). A path is unique within a workspace envelope, so the id is
|
|
222
|
+
// stable when other files are added/removed/reordered; an unchanged file keeps its id (and its
|
|
223
|
+
// canonicalHash), so drift detection never falsely orphans it. Content changes are caught by the
|
|
224
|
+
// canonicalHash diff, not the id. (v1 folded in the array index, which shifted on add/remove and
|
|
225
|
+
// false-orphaned every candidate of an otherwise-unchanged folder — see reCheckRoutes drift.)
|
|
226
|
+
const digest = sha256Hex(`qi-atom-ws-v2|${String(envelopeId)}|${entry.path}`).slice(0, 32);
|
|
227
|
+
const atom = {
|
|
228
|
+
kind: atomKindForPath(entry.path),
|
|
229
|
+
id: QualityIntelligence.asQualityIntelligenceEvidenceAtomId(`qi-atom-${digest}`),
|
|
230
|
+
sourceEnvelopeId: envelopeId,
|
|
231
|
+
canonicalHashSha256Hex: sha256Hex(canonicalText),
|
|
232
|
+
redactionStatus: "redacted",
|
|
233
|
+
lifecycleStatus: "draft",
|
|
234
|
+
};
|
|
235
|
+
return { atom, canonicalText };
|
|
236
|
+
};
|
|
237
|
+
const requirementAtomIdFor = (envelopeId, path, statement) => {
|
|
238
|
+
const digest = sha256Hex(`qi-atom-doc-req-v1|${String(envelopeId)}|${path}|${statement}`).slice(0, 32);
|
|
239
|
+
return QualityIntelligence.asQualityIntelligenceEvidenceAtomId(`qi-atom-${digest}`);
|
|
240
|
+
};
|
|
241
|
+
const stripRequirementDocumentStructure = (text) => text
|
|
242
|
+
.split(/\r?\n/u)
|
|
243
|
+
.filter((line) => !/^\s{0,3}#{1,6}\s+\S/u.test(line))
|
|
244
|
+
.join("\n");
|
|
245
|
+
function documentRequirementAtoms(entry, envelopeId) {
|
|
246
|
+
if (!REQUIREMENT_TEXT_EXTENSION.test(entry.path))
|
|
247
|
+
return Object.freeze([]);
|
|
248
|
+
const split = QualityIntelligenceGeneration.splitRequirementsIntoAtoms(stripRequirementDocumentStructure(entry.excerpt), {
|
|
249
|
+
envelopeId,
|
|
250
|
+
maxAtoms: MAX_TOTAL_ATOMS,
|
|
251
|
+
});
|
|
252
|
+
if (split.length <= 1)
|
|
253
|
+
return Object.freeze([]);
|
|
254
|
+
return Object.freeze(split.map((requirement, ordinal) => {
|
|
255
|
+
const canonicalText = `${entry.path}\n${requirement.canonicalText}`;
|
|
256
|
+
const atom = {
|
|
257
|
+
kind: "requirement",
|
|
258
|
+
id: requirementAtomIdFor(envelopeId, entry.path, requirement.canonicalText),
|
|
259
|
+
sourceEnvelopeId: envelopeId,
|
|
260
|
+
canonicalHashSha256Hex: sha256Hex(canonicalText),
|
|
261
|
+
redactionStatus: "redacted",
|
|
262
|
+
lifecycleStatus: "draft",
|
|
263
|
+
};
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
atom: Object.freeze(atom),
|
|
266
|
+
canonicalText,
|
|
267
|
+
replacementGroupId: replacementGroupIdFor(envelopeId, `document:${entry.path}`),
|
|
268
|
+
replacementOrdinal: ordinal,
|
|
269
|
+
});
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
function atomsForWorkspaceEntry(entry, envelopeId) {
|
|
273
|
+
const requirementAtoms = documentRequirementAtoms(entry, envelopeId);
|
|
274
|
+
return requirementAtoms.length > 0
|
|
275
|
+
? requirementAtoms
|
|
276
|
+
: Object.freeze([workspaceAtom(entry, envelopeId)]);
|
|
277
|
+
}
|
|
278
|
+
// Ingest a local folder by REUSING keiko-workspace traversal + redaction (no independent
|
|
279
|
+
// repository traversal — Issue #278 stop condition). Each selected, already-redacted context
|
|
280
|
+
// entry becomes one content-bearing atom under a single repository-context envelope.
|
|
281
|
+
function ingestWorkspace(source, index, registeredAt, byteBudget) {
|
|
282
|
+
const label = sanitiseLabel(source.label);
|
|
283
|
+
// Reject a folder whose ROOT names a denied credential location (lexically or via a symlinked root):
|
|
284
|
+
// connecting e.g. ~/.aws AS A FOLDER would otherwise ingest credential files whose RELATIVE paths
|
|
285
|
+
// ("credentials", "config.json") never trip the per-file deny check (#729 security).
|
|
286
|
+
assertNotDenied(resolve(source.path), label, "Folder");
|
|
287
|
+
let workspace;
|
|
288
|
+
try {
|
|
289
|
+
workspace = detectWorkspaceAt(source.path);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof WorkspaceError) {
|
|
293
|
+
throw new QiIngestionError("QI_WORKSPACE_NOT_FOUND", "The selected folder could not be opened as a workspace.");
|
|
294
|
+
}
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
const { files } = discoverWithStats(workspace, DEFAULT_CONTEXT_REQUEST.discovery);
|
|
298
|
+
const pack = buildContextPackFromFiles(workspace, {
|
|
299
|
+
...DEFAULT_CONTEXT_REQUEST,
|
|
300
|
+
budgetBytes: Math.min(WORKSPACE_BUDGET_BYTES, byteBudget),
|
|
301
|
+
maxBytesPerFile: WORKSPACE_MAX_BYTES_PER_FILE,
|
|
302
|
+
}, files);
|
|
303
|
+
if (pack.selected.length === 0) {
|
|
304
|
+
throw new QiIngestionError("QI_SOURCE_EMPTY", `No readable files were found in "${label}".`);
|
|
305
|
+
}
|
|
306
|
+
const envelopeId = envelopeIdFor(index, label, pack.workspaceRoot);
|
|
307
|
+
const envelope = {
|
|
308
|
+
id: envelopeId,
|
|
309
|
+
kind: "repository-context",
|
|
310
|
+
displayLabel: label,
|
|
311
|
+
provenance: {
|
|
312
|
+
origin: "workspace",
|
|
313
|
+
registeredAt,
|
|
314
|
+
integrityHashSha256Hex: sha256Hex(`${pack.workspaceRoot}|${pack.selected.map((e) => e.path).join(",")}`),
|
|
315
|
+
},
|
|
316
|
+
localRef: stableLocalRef("workspace", pack.workspaceRoot),
|
|
317
|
+
};
|
|
318
|
+
const atoms = pack.selected.flatMap((entry) => atomsForWorkspaceEntry(entry, envelopeId));
|
|
319
|
+
return { envelope, atoms };
|
|
320
|
+
}
|
|
321
|
+
// A single Fachkonzept document may use the full per-run workspace byte budget — it is the only
|
|
322
|
+
// file — bounded so an oversized file fails with a user-actionable error instead of silently
|
|
323
|
+
// dominating the prompt budget.
|
|
324
|
+
const SINGLE_FILE_MAX_BYTES = WORKSPACE_BUDGET_BYTES;
|
|
325
|
+
// Text-like single-file documents share the strict NUL-byte check because they are expected to be
|
|
326
|
+
// ordinary UTF-8-ish text. Code files reuse the shared CODE_EXTENSION set above.
|
|
327
|
+
const DOC_TEXT_EXTENSION = /\.(?:md|markdown|txt|text|rst|adoc|asciidoc|json|ya?ml|xml|html?|csv|tsv|ini|toml|cfg|conf|properties|tex|org)$/iu;
|
|
328
|
+
// PDF and DOCX are accepted in single-file mode for parity with folder-backed QI: the read path
|
|
329
|
+
// stays keiko-workspace only (best-effort UTF-8/redaction reads, no dedicated document parser). A
|
|
330
|
+
// genuinely text-based PDF/DOCX decodes to usable prose and is ingested; a compressed/binary one
|
|
331
|
+
// decodes to NUL bytes + control-character noise (the real text lives in DEFLATE streams) and is
|
|
332
|
+
// rejected with a user-actionable error rather than silently feeding the model garbage (#713).
|
|
333
|
+
const BEST_EFFORT_DOCUMENT_EXTENSION = /\.(?:pdf|docx)$/iu;
|
|
334
|
+
const isSupportedFilePath = (path) => CODE_EXTENSION.test(path) ||
|
|
335
|
+
DOC_TEXT_EXTENSION.test(path) ||
|
|
336
|
+
BEST_EFFORT_DOCUMENT_EXTENSION.test(path);
|
|
337
|
+
const requiresStrictTextGuard = (path) => CODE_EXTENSION.test(path) || DOC_TEXT_EXTENSION.test(path);
|
|
338
|
+
// A best-effort document whose decoded text is dominated by binary noise carries none of the
|
|
339
|
+
// document's actual prose. Reject above this control-character density so the model never receives
|
|
340
|
+
// unusable content. Kept low (10%) but non-zero so that prose with stray control bytes still reads.
|
|
341
|
+
const DOCUMENT_CONTROL_CHAR_RATIO_LIMIT = 0.1;
|
|
342
|
+
// A control character (excluding ordinary tab/newline/CR), DEL, or the Unicode replacement char —
|
|
343
|
+
// the residue of decoding compressed/binary bytes as UTF-8. Printable prose (incl. umlauts/ß) is
|
|
344
|
+
// never counted.
|
|
345
|
+
function isControlChar(code) {
|
|
346
|
+
const allowedWhitespace = code === 0x09 || code === 0x0a || code === 0x0d;
|
|
347
|
+
return (code < 0x20 && !allowedWhitespace) || code === 0x7f || code === 0xfffd;
|
|
348
|
+
}
|
|
349
|
+
// Detect a best-effort PDF/DOCX read that produced binary noise instead of extractable text. A NUL
|
|
350
|
+
// byte is the canonical binary marker; a high control-character ratio catches mojibake without one.
|
|
351
|
+
// German prose (umlauts, ß) is fully printable, so it never trips this guard.
|
|
352
|
+
function looksBinaryDocument(text) {
|
|
353
|
+
if (text.includes("\u0000"))
|
|
354
|
+
return true;
|
|
355
|
+
let control = 0;
|
|
356
|
+
let total = 0;
|
|
357
|
+
for (const ch of text) {
|
|
358
|
+
total += 1;
|
|
359
|
+
if (isControlChar(ch.codePointAt(0) ?? 0))
|
|
360
|
+
control += 1;
|
|
361
|
+
}
|
|
362
|
+
return total > 0 && control / total > DOCUMENT_CONTROL_CHAR_RATIO_LIMIT;
|
|
363
|
+
}
|
|
364
|
+
const documentFormatLabel = (path) => (/\.pdf$/iu.test(path) ? "PDF" : "Word");
|
|
365
|
+
// A best-effort PDF/DOCX whose decoded text is binary noise (compressed streams, ZIP members)
|
|
366
|
+
// carries none of the document's prose. Reject with actionable guidance instead of ingesting
|
|
367
|
+
// garbage the model cannot use — a text-based PDF/DOCX still ingests normally (#713).
|
|
368
|
+
function assertBestEffortDocumentText(absFile, label, text) {
|
|
369
|
+
if (!BEST_EFFORT_DOCUMENT_EXTENSION.test(absFile) || !looksBinaryDocument(text))
|
|
370
|
+
return;
|
|
371
|
+
throw new QiIngestionError("QI_SOURCE_UNSUPPORTED", `File "${label}" is a ${documentFormatLabel(absFile)} document whose text could not be ` +
|
|
372
|
+
`extracted. Export it to Markdown or plain text and connect that instead.`);
|
|
373
|
+
}
|
|
374
|
+
// Resolve a single file's workspace root and read it through the keiko-workspace boundary-checked
|
|
375
|
+
// read path (`readWorkspaceFile`: lexical containment -> deny rules -> symlink realpath gate -> size
|
|
376
|
+
// cap -> redaction). Every workspace failure is mapped to a safe, user-actionable QiIngestionError.
|
|
377
|
+
function readSingleFileContent(absFile, label) {
|
|
378
|
+
let workspace;
|
|
379
|
+
try {
|
|
380
|
+
workspace = detectWorkspaceAt(dirname(absFile));
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
if (error instanceof WorkspaceError) {
|
|
384
|
+
throw new QiIngestionError("QI_WORKSPACE_NOT_FOUND", "The selected file could not be opened.");
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
return readWorkspaceFile(workspace, relative(workspace.root, absFile), {
|
|
390
|
+
...DEFAULT_READ_OPTIONS,
|
|
391
|
+
maxBytes: SINGLE_FILE_MAX_BYTES,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (error instanceof FileTooLargeError) {
|
|
396
|
+
throw new QiIngestionError("QI_SOURCE_TOO_LARGE", `File "${label}" exceeds the single-file size limit.`);
|
|
397
|
+
}
|
|
398
|
+
if (error instanceof PathDeniedError || error instanceof PathEscapeError) {
|
|
399
|
+
throw new QiIngestionError("QI_SOURCE_DENIED", `File "${label}" is in a protected location.`);
|
|
400
|
+
}
|
|
401
|
+
if (error instanceof WorkspaceReadError) {
|
|
402
|
+
throw new QiIngestionError("QI_WORKSPACE_NOT_FOUND", "The selected file could not be read.");
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Ingest a single local file by REUSING the keiko-workspace boundary-checked read path. No
|
|
408
|
+
// independent file reader is added (Issue #713 stop condition). The same protections that guard the
|
|
409
|
+
// folder path apply identically to the single file; binary / unsupported / oversized / denied /
|
|
410
|
+
// empty inputs each fail with a safe, user-actionable code before any model prompt.
|
|
411
|
+
function ingestFile(source, index, registeredAt, byteBudget) {
|
|
412
|
+
const label = sanitiseLabel(source.label);
|
|
413
|
+
if (!isAbsolute(source.path)) {
|
|
414
|
+
throw new QiIngestionError("QI_BAD_SOURCE", "File source paths must be absolute local paths.");
|
|
415
|
+
}
|
|
416
|
+
const absFile = resolve(source.path);
|
|
417
|
+
if (!isSupportedFilePath(absFile)) {
|
|
418
|
+
throw new QiIngestionError("QI_SOURCE_UNSUPPORTED", `File "${label}" is not a supported single-file document.`);
|
|
419
|
+
}
|
|
420
|
+
// Reject any path whose segments name a denied credential directory or file (.ssh, .aws, .env,
|
|
421
|
+
// *.pem, id_rsa, …) — lexically or after symlink resolution — regardless of the workspace root below.
|
|
422
|
+
assertNotDenied(absFile, label, "File");
|
|
423
|
+
const content = readSingleFileContent(absFile, label);
|
|
424
|
+
// keiko-workspace decodes as UTF-8; a NUL byte is the canonical binary marker. A binary file that
|
|
425
|
+
// slipped past the strict text/code gate (e.g. a mis-named ".txt") is rejected here, never
|
|
426
|
+
// partially ingested. PDF/DOCX get a dedicated binary-noise check below (their own format).
|
|
427
|
+
if (requiresStrictTextGuard(absFile) && content.text.includes("\u0000")) {
|
|
428
|
+
throw new QiIngestionError("QI_SOURCE_UNSUPPORTED", `File "${label}" appears to be binary, not text.`);
|
|
429
|
+
}
|
|
430
|
+
assertBestEffortDocumentText(absFile, label, content.text);
|
|
431
|
+
if (content.text.trim().length === 0) {
|
|
432
|
+
throw new QiIngestionError("QI_SOURCE_EMPTY", `File "${label}" produced no usable content.`);
|
|
433
|
+
}
|
|
434
|
+
// Bound the single file's contributed text to this source's fair share of the global evidence byte
|
|
435
|
+
// budget so a large file cannot, alongside other connected sources, blow the model prompt cap and
|
|
436
|
+
// fail the whole N+1 run (Epic #729). A lone connected file keeps the full budget unchanged
|
|
437
|
+
// (byteBudget === EVIDENCE_BUDGET_BYTES, so boundedText === content.text).
|
|
438
|
+
const boundedText = truncateToUtf8Bytes(content.text, byteBudget);
|
|
439
|
+
const envelopeId = envelopeIdFor(index, label, content.relativePath);
|
|
440
|
+
const envelope = {
|
|
441
|
+
id: envelopeId,
|
|
442
|
+
kind: "repository-context",
|
|
443
|
+
displayLabel: label,
|
|
444
|
+
provenance: {
|
|
445
|
+
origin: "file",
|
|
446
|
+
registeredAt,
|
|
447
|
+
integrityHashSha256Hex: sha256Hex(`${content.relativePath}|${boundedText}`),
|
|
448
|
+
},
|
|
449
|
+
localRef: stableLocalRef("file", absFile),
|
|
450
|
+
};
|
|
451
|
+
const atoms = atomsForWorkspaceEntry({ path: content.relativePath, excerpt: boundedText }, envelopeId);
|
|
452
|
+
return { envelope, atoms };
|
|
453
|
+
}
|
|
454
|
+
// A single capsule document may use the per-document byte budget; the whole capsule corpus is
|
|
455
|
+
// bounded by CAPSULE_BUDGET_BYTES. These mirror the folder path's per-file and per-run budgets so a
|
|
456
|
+
// large capsule degrades gracefully (a bounded prompt) instead of failing the entire run with
|
|
457
|
+
// QI_PROMPT_TOO_LARGE (Epic #710, Issue #717 — resilience parity with workspace/file sources).
|
|
458
|
+
const CAPSULE_MAX_BYTES_PER_DOCUMENT = WORKSPACE_MAX_BYTES_PER_FILE;
|
|
459
|
+
const CAPSULE_BUDGET_BYTES = WORKSPACE_BUDGET_BYTES;
|
|
460
|
+
const utf8Encoder = new TextEncoder();
|
|
461
|
+
const utf8ByteLength = (text) => utf8Encoder.encode(text).length;
|
|
462
|
+
// Truncate to at most maxBytes UTF-8 bytes without splitting a multi-byte code point.
|
|
463
|
+
function truncateToUtf8Bytes(text, maxBytes) {
|
|
464
|
+
if (utf8ByteLength(text) <= maxBytes)
|
|
465
|
+
return text;
|
|
466
|
+
let out = "";
|
|
467
|
+
let bytes = 0;
|
|
468
|
+
for (const cp of text) {
|
|
469
|
+
const cpBytes = utf8ByteLength(cp);
|
|
470
|
+
if (bytes + cpBytes > maxBytes)
|
|
471
|
+
break;
|
|
472
|
+
out += cp;
|
|
473
|
+
bytes += cpBytes;
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
// Redact every member document and cap it. The LK corpus text is NOT redacted at index time — the
|
|
478
|
+
// workspace/file paths redact at read time via keiko-workspace, so the capsule path MUST redact
|
|
479
|
+
// here to honour the atom's redactionStatus:"redacted" and the epic's no-credential-leakage DoD
|
|
480
|
+
// (Epic #710, Issue #717). Each document is capped to the per-document budget and ingestion stops
|
|
481
|
+
// once the cumulative corpus reaches the per-run budget so an oversized capsule degrades gracefully.
|
|
482
|
+
function processCapsuleDocs(docs, byteBudget) {
|
|
483
|
+
// The per-run corpus budget is the smaller of the capsule's own ceiling and this source's fair
|
|
484
|
+
// share of the global evidence byte budget (Epic #729 N+1 split). The per-document cap is likewise
|
|
485
|
+
// never larger than the per-run budget so the always-included first document cannot exceed it.
|
|
486
|
+
const perRunBudget = Math.min(CAPSULE_BUDGET_BYTES, byteBudget);
|
|
487
|
+
const perDocBudget = Math.min(CAPSULE_MAX_BYTES_PER_DOCUMENT, perRunBudget);
|
|
488
|
+
const processed = [];
|
|
489
|
+
let totalBytes = 0;
|
|
490
|
+
for (const doc of docs) {
|
|
491
|
+
const capped = truncateToUtf8Bytes(redact(stripUnsafeFormatChars(doc.text)), perDocBudget);
|
|
492
|
+
if (capped.trim().length === 0)
|
|
493
|
+
continue;
|
|
494
|
+
const bytes = utf8ByteLength(capped);
|
|
495
|
+
// Always include the first usable document (capped to ≤ the per-document budget); thereafter
|
|
496
|
+
// stop before a document would push the corpus past the per-run budget so the total stays
|
|
497
|
+
// bounded (never the raw corpus) and the run never hard-fails on QI_PROMPT_TOO_LARGE.
|
|
498
|
+
if (processed.length > 0 && totalBytes + bytes > perRunBudget)
|
|
499
|
+
break;
|
|
500
|
+
processed.push({ documentId: doc.documentId, text: capped });
|
|
501
|
+
totalBytes += bytes;
|
|
502
|
+
}
|
|
503
|
+
return processed;
|
|
504
|
+
}
|
|
505
|
+
// Build one evidence atom per capsule document. Reuses the workspace atom shape so the model sees
|
|
506
|
+
// structured text (documentId prefix + body), consistent with folder/file sources. The text is
|
|
507
|
+
// already redacted + capped by processCapsuleDocs, so redactionStatus:"redacted" is truthful.
|
|
508
|
+
function capsuleDocAtom(docId, text, envelopeId, fingerprintText = text) {
|
|
509
|
+
const canonicalText = `${docId}\n${text}`;
|
|
510
|
+
const fingerprintCanonicalText = `${docId}\n${fingerprintText}`;
|
|
511
|
+
// Derive the atom id from the stable document id only — never its position in the corpus order
|
|
512
|
+
// (Epic #735 drift correctness, mirrors workspaceAtom). A capsule document id (and a Figma
|
|
513
|
+
// screen id) is unique within its envelope, so adding/removing a sibling document never shifts an
|
|
514
|
+
// unchanged document's atom id and never false-orphans its candidates. Content edits are caught by
|
|
515
|
+
// the canonicalHash diff.
|
|
516
|
+
const digest = sha256Hex(`qi-atom-cap-v2|${String(envelopeId)}|${docId}`).slice(0, 32);
|
|
517
|
+
const atom = {
|
|
518
|
+
kind: "document-excerpt",
|
|
519
|
+
id: QualityIntelligence.asQualityIntelligenceEvidenceAtomId(`qi-atom-${digest}`),
|
|
520
|
+
sourceEnvelopeId: envelopeId,
|
|
521
|
+
canonicalHashSha256Hex: sha256Hex(fingerprintCanonicalText),
|
|
522
|
+
redactionStatus: "redacted",
|
|
523
|
+
lifecycleStatus: "draft",
|
|
524
|
+
};
|
|
525
|
+
return { atom, canonicalText };
|
|
526
|
+
}
|
|
527
|
+
// Shared builder for capsule and capsule-set sources: both resolve to a flat list of corpus
|
|
528
|
+
// documents that are redacted, budget-capped, and mapped to one local-knowledge-capsule envelope
|
|
529
|
+
// plus per-document atoms (Epic #710, Issue #716/#717).
|
|
530
|
+
function buildCapsuleSource(build, byteBudget) {
|
|
531
|
+
if (build.rawDocs.length === 0) {
|
|
532
|
+
throw new QiIngestionError("QI_CAPSULE_UNAVAILABLE", build.emptyError);
|
|
533
|
+
}
|
|
534
|
+
const docs = processCapsuleDocs(build.rawDocs, byteBudget);
|
|
535
|
+
if (docs.length === 0) {
|
|
536
|
+
throw new QiIngestionError("QI_SOURCE_EMPTY", `Source "${build.label}" produced no usable content.`);
|
|
537
|
+
}
|
|
538
|
+
const joinedText = docs.map((d) => d.text).join("\n");
|
|
539
|
+
const envelopeId = envelopeIdFor(build.index, build.label, build.envelopeKey);
|
|
540
|
+
const envelope = {
|
|
541
|
+
id: envelopeId,
|
|
542
|
+
kind: "local-knowledge-capsule",
|
|
543
|
+
displayLabel: build.label,
|
|
544
|
+
provenance: {
|
|
545
|
+
origin: build.origin,
|
|
546
|
+
registeredAt: build.registeredAt,
|
|
547
|
+
integrityHashSha256Hex: sha256Hex(joinedText),
|
|
548
|
+
},
|
|
549
|
+
localRef: build.scopeRef,
|
|
550
|
+
};
|
|
551
|
+
const atoms = docs.map((d) => capsuleDocAtom(d.documentId, d.text, envelopeId));
|
|
552
|
+
return { envelope, atoms };
|
|
553
|
+
}
|
|
554
|
+
function ingestCapsule(source, index, registeredAt, resolver, byteBudget) {
|
|
555
|
+
const label = sanitiseLabel(source.label);
|
|
556
|
+
return buildCapsuleSource({
|
|
557
|
+
label,
|
|
558
|
+
index,
|
|
559
|
+
registeredAt,
|
|
560
|
+
envelopeKey: source.capsuleId,
|
|
561
|
+
scopeRef: stableLocalRef("capsule", source.capsuleId),
|
|
562
|
+
origin: `local-knowledge-capsule:${source.capsuleId}`,
|
|
563
|
+
rawDocs: resolver.capsule(source.capsuleId),
|
|
564
|
+
emptyError: `Capsule "${label}" has no indexed content or could not be opened.`,
|
|
565
|
+
}, byteBudget);
|
|
566
|
+
}
|
|
567
|
+
function ingestCapsuleSet(source, index, registeredAt, resolver, byteBudget) {
|
|
568
|
+
const label = sanitiseLabel(source.label);
|
|
569
|
+
return buildCapsuleSource({
|
|
570
|
+
label,
|
|
571
|
+
index,
|
|
572
|
+
registeredAt,
|
|
573
|
+
envelopeKey: source.capsuleSetId,
|
|
574
|
+
scopeRef: stableLocalRef("capsule-set", source.capsuleSetId),
|
|
575
|
+
origin: `local-knowledge-capsule-set:${source.capsuleSetId}`,
|
|
576
|
+
rawDocs: resolver.capsuleSet(source.capsuleSetId),
|
|
577
|
+
emptyError: `Capsule set "${label}" has no indexed content or could not be opened.`,
|
|
578
|
+
}, byteBudget);
|
|
579
|
+
}
|
|
580
|
+
function isPromiseLike(value) {
|
|
581
|
+
return typeof value.then === "function";
|
|
582
|
+
}
|
|
583
|
+
function collectIrStats(node, stats) {
|
|
584
|
+
const next = {
|
|
585
|
+
imageFillCount: stats.imageFillCount + node.imageFills.length,
|
|
586
|
+
textNodeCount: stats.textNodeCount + (node.text !== undefined && node.text.trim().length > 0 ? 1 : 0),
|
|
587
|
+
semanticNodeCount: stats.semanticNodeCount +
|
|
588
|
+
(node.interactionHint === "button" ||
|
|
589
|
+
node.interactionHint === "input" ||
|
|
590
|
+
node.interactionHint === "link"
|
|
591
|
+
? 1
|
|
592
|
+
: 0),
|
|
593
|
+
};
|
|
594
|
+
return node.children.reduce((acc, child) => collectIrStats(child, acc), next);
|
|
595
|
+
}
|
|
596
|
+
function screenNeedsVisionAugmentation(ir, baseline) {
|
|
597
|
+
const stats = collectIrStats(ir.root, {
|
|
598
|
+
imageFillCount: 0,
|
|
599
|
+
textNodeCount: 0,
|
|
600
|
+
semanticNodeCount: 0,
|
|
601
|
+
});
|
|
602
|
+
const structuralItems = baseline.items.filter((item) => item.category !== "screen-render").length;
|
|
603
|
+
return (stats.imageFillCount > 0 ||
|
|
604
|
+
stats.textNodeCount === 0 ||
|
|
605
|
+
stats.semanticNodeCount === 0 ||
|
|
606
|
+
structuralItems === 0);
|
|
607
|
+
}
|
|
608
|
+
function figmaVisionRequest(record, screen, baselineText) {
|
|
609
|
+
return {
|
|
610
|
+
snapshotRunId: record.runId,
|
|
611
|
+
screenId: screen.screenId,
|
|
612
|
+
image: screen.image,
|
|
613
|
+
imageRelativePath: screen.image.relativePath,
|
|
614
|
+
baselineText,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function mergeFigmaVisionHints(baselineText, hints) {
|
|
618
|
+
return {
|
|
619
|
+
text: QualityIntelligenceFigma.mergeVisionHints(baselineText, hints).text,
|
|
620
|
+
fingerprintText: baselineText,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/** Vision-augment one screen's baseline text without ever overriding it (additive only). */
|
|
624
|
+
function visionAugmentedScreenText(baseline, ir, record, screen, vision) {
|
|
625
|
+
const baselineText = QualityIntelligenceFigma.renderBaselineText(baseline);
|
|
626
|
+
if (vision === undefined ||
|
|
627
|
+
!isRenderedScreenRow(screen) ||
|
|
628
|
+
!screenNeedsVisionAugmentation(ir, baseline)) {
|
|
629
|
+
return { text: baselineText, fingerprintText: baselineText };
|
|
630
|
+
}
|
|
631
|
+
const hints = vision(figmaVisionRequest(record, screen, baselineText));
|
|
632
|
+
return isPromiseLike(hints)
|
|
633
|
+
? { text: baselineText, fingerprintText: baselineText }
|
|
634
|
+
: mergeFigmaVisionHints(baselineText, hints);
|
|
635
|
+
}
|
|
636
|
+
async function visionAugmentedScreenTextAsync(baseline, ir, record, screen, vision) {
|
|
637
|
+
const baselineText = QualityIntelligenceFigma.renderBaselineText(baseline);
|
|
638
|
+
if (vision === undefined ||
|
|
639
|
+
!isRenderedScreenRow(screen) ||
|
|
640
|
+
!screenNeedsVisionAugmentation(ir, baseline)) {
|
|
641
|
+
return { text: baselineText, fingerprintText: baselineText };
|
|
642
|
+
}
|
|
643
|
+
const hints = await vision(figmaVisionRequest(record, screen, baselineText));
|
|
644
|
+
return mergeFigmaVisionHints(baselineText, hints);
|
|
645
|
+
}
|
|
646
|
+
function isRenderedScreenRow(row) {
|
|
647
|
+
return "image" in row;
|
|
648
|
+
}
|
|
649
|
+
// Parse every screen's opaque irJson once; an unparseable screen is dropped (never crashes the run).
|
|
650
|
+
// Rendered screens win if a corrupt record ever duplicates an id; structural-only rows then fill
|
|
651
|
+
// the non-rendered/capped coverage gap for QI.
|
|
652
|
+
function parseScreens(record) {
|
|
653
|
+
const parsed = [];
|
|
654
|
+
const seen = new Set();
|
|
655
|
+
for (const row of record.screens) {
|
|
656
|
+
const ir = QualityIntelligenceFigma.parseScreenIr(row.irJson);
|
|
657
|
+
if (ir !== undefined) {
|
|
658
|
+
parsed.push({ row, ir });
|
|
659
|
+
seen.add(row.screenId);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
for (const row of record.structuralScreens ?? []) {
|
|
663
|
+
if (seen.has(row.screenId))
|
|
664
|
+
continue;
|
|
665
|
+
const ir = QualityIntelligenceFigma.parseScreenIr(row.irJson);
|
|
666
|
+
if (ir !== undefined) {
|
|
667
|
+
parsed.push({ row, ir });
|
|
668
|
+
seen.add(row.screenId);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return parsed;
|
|
672
|
+
}
|
|
673
|
+
function scopedParsedScreens(record, screenIds) {
|
|
674
|
+
const parsed = parseScreens(record);
|
|
675
|
+
// Absent screenIds → whole snapshot (the contract's "absent = whole"). An explicitly EMPTY scope
|
|
676
|
+
// matches NOTHING — it must NEVER widen to the whole board. The route layer already rejects an empty
|
|
677
|
+
// array; this is the defense-in-depth invariant guaranteeing a scoped request can never silently fall
|
|
678
|
+
// back to every screen even if an internal caller passes an empty list.
|
|
679
|
+
if (screenIds === undefined)
|
|
680
|
+
return parsed;
|
|
681
|
+
if (screenIds.length === 0)
|
|
682
|
+
return [];
|
|
683
|
+
const wanted = new Set(screenIds);
|
|
684
|
+
return parsed.filter((screen) => wanted.has(screen.row.screenId));
|
|
685
|
+
}
|
|
686
|
+
// Canonicalise a scoped screen-id list (trim → drop empties → dedupe → sort) so the derived envelope
|
|
687
|
+
// id / provenance ref is stable regardless of the order or duplicates a caller passed. The route layer
|
|
688
|
+
// already canonicalises incoming requests; this keeps direct/internal callers consistent too.
|
|
689
|
+
function canonicalFigmaScreenIds(screenIds) {
|
|
690
|
+
return [...new Set(screenIds.map((id) => id.trim()).filter((id) => id.length > 0))].sort();
|
|
691
|
+
}
|
|
692
|
+
function figmaSnapshotSourceRef(source) {
|
|
693
|
+
if (source.screenIds === undefined)
|
|
694
|
+
return source.snapshotRunId;
|
|
695
|
+
const canonical = canonicalFigmaScreenIds(source.screenIds);
|
|
696
|
+
return canonical.length === 0
|
|
697
|
+
? source.snapshotRunId
|
|
698
|
+
: `${source.snapshotRunId}#${canonical.join(",")}`;
|
|
699
|
+
}
|
|
700
|
+
function imageSourceRef(source) {
|
|
701
|
+
return `${source.sourceKind}:${source.snapshotRunId}#${source.screenId}`;
|
|
702
|
+
}
|
|
703
|
+
function imageDescriptionBaseline(screenId, screenName) {
|
|
704
|
+
return ("No structural JSON is attached to this source. Describe the visible UI screenshot for " +
|
|
705
|
+
"test generation. Focus on user-visible purpose, controls, form fields, labels, states, " +
|
|
706
|
+
`layout groups, and likely validation affordances.\nScreen id: ${screenId}\nScreen name: ${screenName}`);
|
|
707
|
+
}
|
|
708
|
+
function imageDescriptionText(screenId, screenName, hints) {
|
|
709
|
+
const cleaned = hints
|
|
710
|
+
.map((hint) => redactFigmaAtomText(hint).trim())
|
|
711
|
+
.filter((hint) => hint.length > 0);
|
|
712
|
+
if (cleaned.length === 0)
|
|
713
|
+
return undefined;
|
|
714
|
+
return [
|
|
715
|
+
`Image description for ${screenName} (${screenId})`,
|
|
716
|
+
"",
|
|
717
|
+
...cleaned.map((hint) => `- ${hint}`),
|
|
718
|
+
].join("\n");
|
|
719
|
+
}
|
|
720
|
+
function parsedRenderedScreenForImageSource(record, source, label) {
|
|
721
|
+
const parsed = parseScreens(record).find((screen) => screen.row.screenId === source.screenId);
|
|
722
|
+
if (parsed === undefined || !isRenderedScreenRow(parsed.row)) {
|
|
723
|
+
throw new QiIngestionError("QI_IMAGE_UNAVAILABLE", `Image "${label}" could not be found in the stored Figma snapshot.`);
|
|
724
|
+
}
|
|
725
|
+
return { row: parsed.row, ir: parsed.ir };
|
|
726
|
+
}
|
|
727
|
+
function imageDescriptionUnavailable(label) {
|
|
728
|
+
return new QiIngestionError("QI_IMAGE_DESCRIPTION_UNAVAILABLE", `Image "${label}" could not be described. Configure an image-input capable model for Quality Intelligence image sources.`);
|
|
729
|
+
}
|
|
730
|
+
function imageDescriptionDocFromHints(screenId, screenName, hints, byteBudget) {
|
|
731
|
+
const text = imageDescriptionText(screenId, screenName, hints);
|
|
732
|
+
if (text === undefined)
|
|
733
|
+
return undefined;
|
|
734
|
+
const perDocBudget = Math.min(CAPSULE_MAX_BYTES_PER_DOCUMENT, byteBudget);
|
|
735
|
+
const capped = truncateToUtf8Bytes(redactFigmaAtomText(text), perDocBudget);
|
|
736
|
+
if (capped.trim().length === 0)
|
|
737
|
+
return undefined;
|
|
738
|
+
return {
|
|
739
|
+
documentId: `Image description: ${figmaDocumentId(screenId, screenName)}`,
|
|
740
|
+
text: capped,
|
|
741
|
+
fingerprintText: capped,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
// Distinguish "the selected screens are not present in this snapshot" from "the snapshot produced no
|
|
745
|
+
// usable baseline" so a scoped run surfaces a precise, user-actionable reason (audit: clear
|
|
746
|
+
// screen-not-found error) in the streamed skippedSources notice. The code stays QI_SOURCE_EMPTY so the
|
|
747
|
+
// N+1-resilience skip and the drift "all orphaned" allowance keep their existing control flow — only
|
|
748
|
+
// the message is sharpened.
|
|
749
|
+
function figmaSnapshotEmptyError(record, source, label) {
|
|
750
|
+
if (source.screenIds !== undefined) {
|
|
751
|
+
const requested = canonicalFigmaScreenIds(source.screenIds);
|
|
752
|
+
const present = new Set(parseScreens(record).map((screen) => screen.row.screenId));
|
|
753
|
+
const missing = requested.filter((id) => !present.has(id));
|
|
754
|
+
if (requested.length > 0 && missing.length === requested.length) {
|
|
755
|
+
return new QiIngestionError("QI_SOURCE_EMPTY", `Figma snapshot "${label}": none of the selected screens (${missing.join(", ")}) exist in this snapshot.`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return new QiIngestionError("QI_SOURCE_EMPTY", `Figma snapshot "${label}" produced no usable screen baseline.`);
|
|
759
|
+
}
|
|
760
|
+
// Derive the deterministic navigation/flow/coverage test items per screen from the parsed screens +
|
|
761
|
+
// the snapshot's raw inter-screen links (#811). Composes into the baseline below through #754's
|
|
762
|
+
// `extraItems` seam. When the snapshot carries no `links` (an older record), every screen maps to no
|
|
763
|
+
// nav items and the baseline is identical to the IR-only path — purely additive.
|
|
764
|
+
function navItemsByScreen(parsed, links) {
|
|
765
|
+
const irResult = {
|
|
766
|
+
screens: parsed.map((p) => p.ir),
|
|
767
|
+
links,
|
|
768
|
+
tokens: { colors: [], typography: [], spacing: [], radius: [] },
|
|
769
|
+
reduction: { inputNodeCount: 0, keptNodeCount: 0, removedNodeCount: 0, removedRatio: 0 },
|
|
770
|
+
};
|
|
771
|
+
return QualityIntelligenceFigma.deriveNavTestItemsByScreen(QualityIntelligenceFigma.deriveNavGraph(irResult));
|
|
772
|
+
}
|
|
773
|
+
// Derive the deterministic accessibility test items per screen from the parsed screens (#812).
|
|
774
|
+
// Composes into the baseline below through #754's `extraItems` seam, ALONGSIDE the navigation items
|
|
775
|
+
// (concatenated, never replacing them). Model-free: a screen with no colour/box/interactive nodes of
|
|
776
|
+
// interest maps to no a11y items, so the baseline is identical to the IR-only path — purely additive.
|
|
777
|
+
function a11yItemsByScreen(parsed) {
|
|
778
|
+
return QualityIntelligenceFigma.deriveA11yTestItemsByScreen(parsed.map((p) => p.ir));
|
|
779
|
+
}
|
|
780
|
+
function figmaDocumentId(screenId, screenName) {
|
|
781
|
+
// Correct order (the #734 strip-before-redact rule): strip format chars first to
|
|
782
|
+
// de-obfuscate any zero-width-split secret, then redact (now catches raw AND ZW-split
|
|
783
|
+
// secrets → emits "[REDACTED]"), then sanitiseLabel for display safety (collapses
|
|
784
|
+
// newlines, URLs, paths). Mirrors redactFigmaAtomText = redact(stripUnsafeFormatChars)
|
|
785
|
+
// below, with sanitiseLabel as the final display-safe step.
|
|
786
|
+
const safeName = sanitiseLabel(redact(stripUnsafeFormatChars(screenName)));
|
|
787
|
+
return `${screenId} (${truncateToUtf8Bytes(safeName, MAX_LABEL_CHARS)})`;
|
|
788
|
+
}
|
|
789
|
+
// Strip Unicode bidi-override / zero-width / C1 spoofing code points from the untrusted Figma-derived
|
|
790
|
+
// atom text BEFORE secret redaction. Two reasons for this order (the #734 strip-before-redact rule):
|
|
791
|
+
// stripping first DE-OBFUSCATES a zero-width-split secret so the redactor can still match it, and it
|
|
792
|
+
// removes the bidi/zero-width chars that would otherwise ride a Figma screen name or prototype trigger
|
|
793
|
+
// verbatim into the QI atom text and every downstream export (bidi-spoofing of generated test titles).
|
|
794
|
+
// Symmetric with the candidate-text path (buildRequirementExcerpt) and the source-label path
|
|
795
|
+
// (sanitiseLabel / stripUnsafeLabelChars). TAB/LF/CR are preserved so the multi-line baseline structure
|
|
796
|
+
// stays intact; clean inputs are byte-identical, so the atom hash and budget accounting are unchanged.
|
|
797
|
+
const redactFigmaAtomText = (text) => redact(stripUnsafeFormatChars(text));
|
|
798
|
+
function consumeVisionProviderForScreen(vision, remainingVisionScreens, ir, baseline) {
|
|
799
|
+
if (vision === undefined ||
|
|
800
|
+
remainingVisionScreens <= 0 ||
|
|
801
|
+
!screenNeedsVisionAugmentation(ir, baseline)) {
|
|
802
|
+
return { provider: undefined, remaining: remainingVisionScreens };
|
|
803
|
+
}
|
|
804
|
+
return { provider: vision, remaining: remainingVisionScreens - 1 };
|
|
805
|
+
}
|
|
806
|
+
function corpusDocFromFigmaScreen(row, ir, augmented, perDocBudget) {
|
|
807
|
+
const capped = truncateToUtf8Bytes(redactFigmaAtomText(augmented.text), perDocBudget);
|
|
808
|
+
if (capped.trim().length === 0)
|
|
809
|
+
return undefined;
|
|
810
|
+
const fingerprintText = truncateToUtf8Bytes(redactFigmaAtomText(augmented.fingerprintText), perDocBudget);
|
|
811
|
+
return {
|
|
812
|
+
doc: {
|
|
813
|
+
documentId: figmaDocumentId(row.screenId, ir.name),
|
|
814
|
+
text: capped,
|
|
815
|
+
fingerprintText,
|
|
816
|
+
},
|
|
817
|
+
bytes: utf8ByteLength(capped),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function figmaDocFitsBudget(docs, totalBytes, nextBytes, perRunBudget) {
|
|
821
|
+
return docs.length === 0 || totalBytes + nextBytes <= perRunBudget;
|
|
822
|
+
}
|
|
823
|
+
// Derive the redacted, budget-capped canonical text for every parseable screen. Each screen's
|
|
824
|
+
// deterministic structural baseline (#754) is augmented additively with its navigation/flow test
|
|
825
|
+
// items (#811) AND its accessibility test items (#812) — concatenated, neither replacing the other —
|
|
826
|
+
// through the `extraItems` seam, then optionally with vision hints. The per-run byte budget bounds
|
|
827
|
+
// the cumulative corpus so an oversized board never hard-fails on QI_PROMPT_TOO_LARGE.
|
|
828
|
+
// The byteBudget is the caller's fair share of the global evidence pool (Epic #729 N+1 split)
|
|
829
|
+
// so a figma-snapshot source never consumes more than its fair slice alongside other sources.
|
|
830
|
+
function figmaScreenDocs(record, vision, byteBudget, screenIds) {
|
|
831
|
+
// Mirror processCapsuleDocs (:558-563): the per-run corpus budget is the smaller of the capsule's
|
|
832
|
+
// own ceiling and this source's fair share of the global evidence byte budget (Epic #729 N+1
|
|
833
|
+
// split). The per-document cap is likewise never larger than the per-run budget.
|
|
834
|
+
const perRunBudget = Math.min(CAPSULE_BUDGET_BYTES, byteBudget);
|
|
835
|
+
const perDocBudget = Math.min(CAPSULE_MAX_BYTES_PER_DOCUMENT, perRunBudget);
|
|
836
|
+
const parsed = scopedParsedScreens(record, screenIds);
|
|
837
|
+
const navItems = navItemsByScreen(parsed, record.links ?? []);
|
|
838
|
+
const a11yItems = a11yItemsByScreen(parsed);
|
|
839
|
+
const docs = [];
|
|
840
|
+
let totalBytes = 0;
|
|
841
|
+
let remainingVisionScreens = MAX_FIGMA_VISION_AUGMENTED_SCREENS;
|
|
842
|
+
for (const { row, ir } of parsed) {
|
|
843
|
+
const extraItems = [...(navItems.get(ir.id) ?? []), ...(a11yItems.get(ir.id) ?? [])];
|
|
844
|
+
const baseline = QualityIntelligenceFigma.deriveScreenTestBaseline(ir, extraItems);
|
|
845
|
+
const visionSlot = isRenderedScreenRow(row)
|
|
846
|
+
? consumeVisionProviderForScreen(vision, remainingVisionScreens, ir, baseline)
|
|
847
|
+
: { provider: undefined, remaining: remainingVisionScreens };
|
|
848
|
+
remainingVisionScreens = visionSlot.remaining;
|
|
849
|
+
const augmented = visionAugmentedScreenText(baseline, ir, record, row, visionSlot.provider);
|
|
850
|
+
const nextDoc = corpusDocFromFigmaScreen(row, ir, augmented, perDocBudget);
|
|
851
|
+
if (nextDoc === undefined)
|
|
852
|
+
continue;
|
|
853
|
+
if (!figmaDocFitsBudget(docs, totalBytes, nextDoc.bytes, perRunBudget))
|
|
854
|
+
break;
|
|
855
|
+
docs.push(nextDoc.doc);
|
|
856
|
+
totalBytes += nextDoc.bytes;
|
|
857
|
+
}
|
|
858
|
+
return docs;
|
|
859
|
+
}
|
|
860
|
+
async function figmaScreenDocsAsync(record, vision, byteBudget, screenIds) {
|
|
861
|
+
const perRunBudget = Math.min(CAPSULE_BUDGET_BYTES, byteBudget);
|
|
862
|
+
const perDocBudget = Math.min(CAPSULE_MAX_BYTES_PER_DOCUMENT, perRunBudget);
|
|
863
|
+
const parsed = scopedParsedScreens(record, screenIds);
|
|
864
|
+
const navItems = navItemsByScreen(parsed, record.links ?? []);
|
|
865
|
+
const a11yItems = a11yItemsByScreen(parsed);
|
|
866
|
+
const docs = [];
|
|
867
|
+
let totalBytes = 0;
|
|
868
|
+
let remainingVisionScreens = MAX_FIGMA_VISION_AUGMENTED_SCREENS;
|
|
869
|
+
for (const { row, ir } of parsed) {
|
|
870
|
+
const extraItems = [...(navItems.get(ir.id) ?? []), ...(a11yItems.get(ir.id) ?? [])];
|
|
871
|
+
const baseline = QualityIntelligenceFigma.deriveScreenTestBaseline(ir, extraItems);
|
|
872
|
+
const visionSlot = isRenderedScreenRow(row)
|
|
873
|
+
? consumeVisionProviderForScreen(vision, remainingVisionScreens, ir, baseline)
|
|
874
|
+
: { provider: undefined, remaining: remainingVisionScreens };
|
|
875
|
+
remainingVisionScreens = visionSlot.remaining;
|
|
876
|
+
const augmented = await visionAugmentedScreenTextAsync(baseline, ir, record, row, visionSlot.provider);
|
|
877
|
+
const nextDoc = corpusDocFromFigmaScreen(row, ir, augmented, perDocBudget);
|
|
878
|
+
if (nextDoc === undefined)
|
|
879
|
+
continue;
|
|
880
|
+
if (!figmaDocFitsBudget(docs, totalBytes, nextDoc.bytes, perRunBudget))
|
|
881
|
+
break;
|
|
882
|
+
docs.push(nextDoc.doc);
|
|
883
|
+
totalBytes += nextDoc.bytes;
|
|
884
|
+
}
|
|
885
|
+
return docs;
|
|
886
|
+
}
|
|
887
|
+
function ingestFigmaSnapshot(source, index, registeredAt, loader, vision, byteBudget) {
|
|
888
|
+
const label = sanitiseLabel(source.label);
|
|
889
|
+
const record = loader(source.snapshotRunId);
|
|
890
|
+
if (record === undefined) {
|
|
891
|
+
throw new QiIngestionError("QI_FIGMA_SNAPSHOT_UNAVAILABLE", `Figma snapshot "${label}" could not be found or read. Build the snapshot first.`);
|
|
892
|
+
}
|
|
893
|
+
if (record.screens.length === 0 && (record.structuralScreens?.length ?? 0) === 0) {
|
|
894
|
+
throw new QiIngestionError("QI_SOURCE_EMPTY", `Figma snapshot "${label}" has no screens.`);
|
|
895
|
+
}
|
|
896
|
+
const docs = figmaScreenDocs(record, vision, byteBudget, source.screenIds);
|
|
897
|
+
if (docs.length === 0) {
|
|
898
|
+
throw figmaSnapshotEmptyError(record, source, label);
|
|
899
|
+
}
|
|
900
|
+
const joinedFingerprintText = docs.map((d) => d.fingerprintText ?? d.text).join("\n");
|
|
901
|
+
const sourceRef = figmaSnapshotSourceRef(source);
|
|
902
|
+
const envelopeId = envelopeIdFor(index, label, sourceRef);
|
|
903
|
+
// A stored Figma Snapshot is figma evidence, not repository context. Use the dedicated
|
|
904
|
+
// `figma-evidence` envelope kind (#278 AC2 "represented as an explicit connector-backed source"
|
|
905
|
+
// + AC4 citation/audit attribution) so the persisted envelope, source-mix priority, and any
|
|
906
|
+
// kind-grouped audit rollup classify it correctly instead of folding it into repo context.
|
|
907
|
+
const envelope = {
|
|
908
|
+
id: envelopeId,
|
|
909
|
+
kind: "figma-evidence",
|
|
910
|
+
displayLabel: label,
|
|
911
|
+
provenance: {
|
|
912
|
+
origin: `figma-snapshot:${sourceRef}`,
|
|
913
|
+
registeredAt,
|
|
914
|
+
integrityHashSha256Hex: sha256Hex(joinedFingerprintText),
|
|
915
|
+
},
|
|
916
|
+
localRef: stableLocalRef("figma-snapshot", sourceRef),
|
|
917
|
+
};
|
|
918
|
+
const atoms = docs.map((d) => capsuleDocAtom(d.documentId, d.text, envelopeId, d.fingerprintText ?? d.text));
|
|
919
|
+
return { envelope, atoms };
|
|
920
|
+
}
|
|
921
|
+
async function ingestFigmaSnapshotAsync(source, index, registeredAt, loader, vision, byteBudget) {
|
|
922
|
+
const label = sanitiseLabel(source.label);
|
|
923
|
+
const record = loader(source.snapshotRunId);
|
|
924
|
+
if (record === undefined) {
|
|
925
|
+
throw new QiIngestionError("QI_FIGMA_SNAPSHOT_UNAVAILABLE", `Figma snapshot "${label}" could not be found or read. Build the snapshot first.`);
|
|
926
|
+
}
|
|
927
|
+
if (record.screens.length === 0 && (record.structuralScreens?.length ?? 0) === 0) {
|
|
928
|
+
throw new QiIngestionError("QI_SOURCE_EMPTY", `Figma snapshot "${label}" has no screens.`);
|
|
929
|
+
}
|
|
930
|
+
const docs = await figmaScreenDocsAsync(record, vision, byteBudget, source.screenIds);
|
|
931
|
+
if (docs.length === 0) {
|
|
932
|
+
throw figmaSnapshotEmptyError(record, source, label);
|
|
933
|
+
}
|
|
934
|
+
const joinedFingerprintText = docs.map((d) => d.fingerprintText ?? d.text).join("\n");
|
|
935
|
+
const sourceRef = figmaSnapshotSourceRef(source);
|
|
936
|
+
const envelopeId = envelopeIdFor(index, label, sourceRef);
|
|
937
|
+
const envelope = {
|
|
938
|
+
id: envelopeId,
|
|
939
|
+
kind: "figma-evidence",
|
|
940
|
+
displayLabel: label,
|
|
941
|
+
provenance: {
|
|
942
|
+
origin: `figma-snapshot:${sourceRef}`,
|
|
943
|
+
registeredAt,
|
|
944
|
+
integrityHashSha256Hex: sha256Hex(joinedFingerprintText),
|
|
945
|
+
},
|
|
946
|
+
localRef: stableLocalRef("figma-snapshot", sourceRef),
|
|
947
|
+
};
|
|
948
|
+
const atoms = docs.map((d) => capsuleDocAtom(d.documentId, d.text, envelopeId, d.fingerprintText ?? d.text));
|
|
949
|
+
return { envelope, atoms };
|
|
950
|
+
}
|
|
951
|
+
function buildImageSourceEnvelope(source, label, index, registeredAt, doc) {
|
|
952
|
+
const sourceRef = imageSourceRef(source);
|
|
953
|
+
const envelopeId = envelopeIdFor(index, label, sourceRef);
|
|
954
|
+
const envelope = {
|
|
955
|
+
id: envelopeId,
|
|
956
|
+
kind: "figma-evidence",
|
|
957
|
+
displayLabel: label,
|
|
958
|
+
provenance: {
|
|
959
|
+
origin: `image:${sourceRef}`,
|
|
960
|
+
registeredAt,
|
|
961
|
+
integrityHashSha256Hex: sha256Hex(doc.fingerprintText ?? doc.text),
|
|
962
|
+
},
|
|
963
|
+
localRef: stableLocalRef("image", sourceRef),
|
|
964
|
+
};
|
|
965
|
+
return {
|
|
966
|
+
envelope,
|
|
967
|
+
atoms: [capsuleDocAtom(doc.documentId, doc.text, envelopeId, doc.fingerprintText ?? doc.text)],
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function imageDescriptionDoc(record, source, label, vision, byteBudget) {
|
|
971
|
+
if (vision === undefined)
|
|
972
|
+
return undefined;
|
|
973
|
+
const { row, ir } = parsedRenderedScreenForImageSource(record, source, label);
|
|
974
|
+
const hints = vision(figmaVisionRequest(record, row, imageDescriptionBaseline(row.screenId, ir.name)));
|
|
975
|
+
if (isPromiseLike(hints))
|
|
976
|
+
return undefined;
|
|
977
|
+
return imageDescriptionDocFromHints(row.screenId, ir.name, hints, byteBudget);
|
|
978
|
+
}
|
|
979
|
+
async function imageDescriptionDocAsync(record, source, label, vision, byteBudget) {
|
|
980
|
+
if (vision === undefined)
|
|
981
|
+
return undefined;
|
|
982
|
+
const { row, ir } = parsedRenderedScreenForImageSource(record, source, label);
|
|
983
|
+
const hints = await vision(figmaVisionRequest(record, row, imageDescriptionBaseline(row.screenId, ir.name)));
|
|
984
|
+
return imageDescriptionDocFromHints(row.screenId, ir.name, hints, byteBudget);
|
|
985
|
+
}
|
|
986
|
+
function ingestImageSource(source, index, registeredAt, loader, vision, byteBudget) {
|
|
987
|
+
const label = sanitiseLabel(source.label);
|
|
988
|
+
const record = loader(source.snapshotRunId);
|
|
989
|
+
if (record === undefined) {
|
|
990
|
+
throw new QiIngestionError("QI_IMAGE_UNAVAILABLE", `Image "${label}" could not be found or read. Build the Figma snapshot first.`);
|
|
991
|
+
}
|
|
992
|
+
const doc = imageDescriptionDoc(record, source, label, vision, byteBudget);
|
|
993
|
+
if (doc === undefined)
|
|
994
|
+
throw imageDescriptionUnavailable(label);
|
|
995
|
+
return buildImageSourceEnvelope(source, label, index, registeredAt, doc);
|
|
996
|
+
}
|
|
997
|
+
async function ingestImageSourceAsync(source, index, registeredAt, loader, vision, byteBudget) {
|
|
998
|
+
const label = sanitiseLabel(source.label);
|
|
999
|
+
const record = loader(source.snapshotRunId);
|
|
1000
|
+
if (record === undefined) {
|
|
1001
|
+
throw new QiIngestionError("QI_IMAGE_UNAVAILABLE", `Image "${label}" could not be found or read. Build the Figma snapshot first.`);
|
|
1002
|
+
}
|
|
1003
|
+
const doc = await imageDescriptionDocAsync(record, source, label, vision, byteBudget);
|
|
1004
|
+
if (doc === undefined)
|
|
1005
|
+
throw imageDescriptionUnavailable(label);
|
|
1006
|
+
return buildImageSourceEnvelope(source, label, index, registeredAt, doc);
|
|
1007
|
+
}
|
|
1008
|
+
function ingestCapsuleSource(source, index, registeredAt, capsuleResolver, byteBudget) {
|
|
1009
|
+
if (capsuleResolver === undefined) {
|
|
1010
|
+
throw new QiIngestionError("QI_CAPSULE_UNAVAILABLE", source.kind === "capsule"
|
|
1011
|
+
? "Capsule sources are unavailable: the Local Knowledge store is not configured."
|
|
1012
|
+
: "Capsule-set sources are unavailable: the Local Knowledge store is not configured.");
|
|
1013
|
+
}
|
|
1014
|
+
return source.kind === "capsule"
|
|
1015
|
+
? ingestCapsule(source, index, registeredAt, capsuleResolver, byteBudget)
|
|
1016
|
+
: ingestCapsuleSet(source, index, registeredAt, capsuleResolver, byteBudget);
|
|
1017
|
+
}
|
|
1018
|
+
function ingestStoredFigmaSource(source, index, registeredAt, figmaSnapshotLoader, figmaVision, byteBudget) {
|
|
1019
|
+
if (figmaSnapshotLoader === undefined) {
|
|
1020
|
+
throw new QiIngestionError(source.kind === "image" ? "QI_IMAGE_UNAVAILABLE" : "QI_FIGMA_SNAPSHOT_UNAVAILABLE", source.kind === "image"
|
|
1021
|
+
? "Image sources are unavailable: the evidence directory is not configured."
|
|
1022
|
+
: "Figma-snapshot sources are unavailable: the evidence directory is not configured.");
|
|
1023
|
+
}
|
|
1024
|
+
return source.kind === "image"
|
|
1025
|
+
? ingestImageSource(source, index, registeredAt, figmaSnapshotLoader, figmaVision, byteBudget)
|
|
1026
|
+
: ingestFigmaSnapshot(source, index, registeredAt, figmaSnapshotLoader, figmaVision, byteBudget);
|
|
1027
|
+
}
|
|
1028
|
+
function ingestOne(source, index, registeredAt, capsuleResolver, figmaSnapshotLoader, figmaVision, byteBudget) {
|
|
1029
|
+
switch (source.kind) {
|
|
1030
|
+
case "requirements":
|
|
1031
|
+
return ingestRequirements(source, index, registeredAt);
|
|
1032
|
+
case "workspace":
|
|
1033
|
+
return ingestWorkspace(source, index, registeredAt, byteBudget);
|
|
1034
|
+
case "file":
|
|
1035
|
+
return ingestFile(source, index, registeredAt, byteBudget);
|
|
1036
|
+
case "capsule":
|
|
1037
|
+
case "capsule-set":
|
|
1038
|
+
return ingestCapsuleSource(source, index, registeredAt, capsuleResolver, byteBudget);
|
|
1039
|
+
case "figma-snapshot":
|
|
1040
|
+
case "image":
|
|
1041
|
+
return ingestStoredFigmaSource(source, index, registeredAt, figmaSnapshotLoader, figmaVision, byteBudget);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function ingestOneAsync(source, index, registeredAt, capsuleResolver, figmaSnapshotLoader, figmaVision, byteBudget) {
|
|
1045
|
+
if (source.kind !== "figma-snapshot" && source.kind !== "image") {
|
|
1046
|
+
return ingestOne(source, index, registeredAt, capsuleResolver, figmaSnapshotLoader, figmaVision, byteBudget);
|
|
1047
|
+
}
|
|
1048
|
+
if (figmaSnapshotLoader === undefined) {
|
|
1049
|
+
throw new QiIngestionError(source.kind === "image" ? "QI_IMAGE_UNAVAILABLE" : "QI_FIGMA_SNAPSHOT_UNAVAILABLE", source.kind === "image"
|
|
1050
|
+
? "Image sources are unavailable: the evidence directory is not configured."
|
|
1051
|
+
: "Figma-snapshot sources are unavailable: the evidence directory is not configured.");
|
|
1052
|
+
}
|
|
1053
|
+
if (source.kind === "image") {
|
|
1054
|
+
return ingestImageSourceAsync(source, index, registeredAt, figmaSnapshotLoader, figmaVision, byteBudget);
|
|
1055
|
+
}
|
|
1056
|
+
return ingestFigmaSnapshotAsync(source, index, registeredAt, figmaSnapshotLoader, figmaVision, byteBudget);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Ingest one source into the accumulator (Epic #729 N+1 resilience). On a per-source QiIngestionError
|
|
1060
|
+
* the source is recorded as skipped and ingestion continues with the rest; a genuine (non-coded) bug
|
|
1061
|
+
* still throws so it is never silently swallowed. Each successful source takes its fair atom share,
|
|
1062
|
+
* bounded by the global cap so the total never exceeds it.
|
|
1063
|
+
*/
|
|
1064
|
+
function ingestSourceInto(acc, source, index, input, budgets) {
|
|
1065
|
+
let ingested;
|
|
1066
|
+
try {
|
|
1067
|
+
ingested = ingestOne(source, index, input.registeredAt, input.capsuleResolver, input.figmaSnapshotLoader, input.figmaVision, budgets.byteBudget);
|
|
1068
|
+
}
|
|
1069
|
+
catch (error) {
|
|
1070
|
+
if (!(error instanceof QiIngestionError))
|
|
1071
|
+
throw error;
|
|
1072
|
+
acc.firstSkipError ??= error;
|
|
1073
|
+
acc.skippedSources.push({
|
|
1074
|
+
label: sanitiseLabel(source.label),
|
|
1075
|
+
kind: source.kind,
|
|
1076
|
+
code: error.code,
|
|
1077
|
+
message: error.message,
|
|
1078
|
+
});
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const { envelope, atoms } = ingested;
|
|
1082
|
+
const take = Math.min(budgets.atomBudget, MAX_TOTAL_ATOMS - acc.ingestedAtoms.length);
|
|
1083
|
+
const taken = take <= 0 ? [] : atoms.slice(0, take);
|
|
1084
|
+
acc.envelopes.push(envelope);
|
|
1085
|
+
acc.ingestedAtoms.push(...taken);
|
|
1086
|
+
acc.sourceSummaries.push({
|
|
1087
|
+
label: envelope.displayLabel,
|
|
1088
|
+
kind: source.kind,
|
|
1089
|
+
atomCount: taken.length,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
async function ingestSourceIntoAsync(acc, source, index, input, budgets) {
|
|
1093
|
+
let ingested;
|
|
1094
|
+
try {
|
|
1095
|
+
ingested = await ingestOneAsync(source, index, input.registeredAt, input.capsuleResolver, input.figmaSnapshotLoader, input.figmaVision, budgets.byteBudget);
|
|
1096
|
+
}
|
|
1097
|
+
catch (error) {
|
|
1098
|
+
if (!(error instanceof QiIngestionError))
|
|
1099
|
+
throw error;
|
|
1100
|
+
acc.firstSkipError ??= error;
|
|
1101
|
+
acc.skippedSources.push({
|
|
1102
|
+
label: sanitiseLabel(source.label),
|
|
1103
|
+
kind: source.kind,
|
|
1104
|
+
code: error.code,
|
|
1105
|
+
message: error.message,
|
|
1106
|
+
});
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const { envelope, atoms } = ingested;
|
|
1110
|
+
const take = Math.min(budgets.atomBudget, MAX_TOTAL_ATOMS - acc.ingestedAtoms.length);
|
|
1111
|
+
const taken = take <= 0 ? [] : atoms.slice(0, take);
|
|
1112
|
+
acc.envelopes.push(envelope);
|
|
1113
|
+
acc.ingestedAtoms.push(...taken);
|
|
1114
|
+
acc.sourceSummaries.push({
|
|
1115
|
+
label: envelope.displayLabel,
|
|
1116
|
+
kind: source.kind,
|
|
1117
|
+
atomCount: taken.length,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
function emptyDriftIngestionResult(input, droppedSourceCount, skippedSources) {
|
|
1121
|
+
return {
|
|
1122
|
+
envelopes: [],
|
|
1123
|
+
ingestedAtoms: [],
|
|
1124
|
+
provenanceRefs: {
|
|
1125
|
+
envelopeIds: [],
|
|
1126
|
+
auditSummaryId: auditSummaryIdFor(input.runId),
|
|
1127
|
+
},
|
|
1128
|
+
sourceSummaries: [],
|
|
1129
|
+
droppedSourceCount,
|
|
1130
|
+
skippedSources,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function allowEmptyDriftIngestion(input, acc, droppedSourceCount) {
|
|
1134
|
+
if (input.allowEmpty !== true)
|
|
1135
|
+
return undefined;
|
|
1136
|
+
const blockingSkip = acc.skippedSources.find((source) => source.code !== "QI_SOURCE_EMPTY");
|
|
1137
|
+
if (blockingSkip !== undefined) {
|
|
1138
|
+
throw new QiIngestionError(blockingSkip.code, blockingSkip.message);
|
|
1139
|
+
}
|
|
1140
|
+
return emptyDriftIngestionResult(input, droppedSourceCount, acc.skippedSources);
|
|
1141
|
+
}
|
|
1142
|
+
export function ingestInlineSources(input) {
|
|
1143
|
+
// Read through the typed property in the loop: `Array.isArray` would widen a local binding of the
|
|
1144
|
+
// readonly union array to `any[]`, so the guard checks length on the typed property directly.
|
|
1145
|
+
const allSources = input.request.sources;
|
|
1146
|
+
if (allSources.length === 0) {
|
|
1147
|
+
throw new QiIngestionError("QI_NO_SOURCES", "At least one source is required to start a run.");
|
|
1148
|
+
}
|
|
1149
|
+
// Cap the source count BEFORE ingestion (no partial work for dropped sources), then split BOTH the
|
|
1150
|
+
// global atom budget and the global evidence BYTE budget fairly so no single source starves the
|
|
1151
|
+
// others and the merged prompt stays under the model ceiling regardless of N (Chat N+1 parity,
|
|
1152
|
+
// #730) — preventing one large source from hard-failing the whole run with QI_PROMPT_TOO_LARGE.
|
|
1153
|
+
const sources = allSources.slice(0, MAX_QI_SOURCES);
|
|
1154
|
+
const droppedSourceCount = allSources.length - sources.length;
|
|
1155
|
+
const budgets = {
|
|
1156
|
+
atomBudget: perSourceAtomBudget(MAX_TOTAL_ATOMS, sources.length),
|
|
1157
|
+
byteBudget: perSourceByteBudget(sources.length),
|
|
1158
|
+
};
|
|
1159
|
+
// N+1 resilience (Chat parity): each source is ingested independently; a source that produces
|
|
1160
|
+
// nothing usable is skipped + recorded so the healthy ones still produce the run. The run fails only
|
|
1161
|
+
// when EVERY source fails — re-raising the FIRST coded error so a single bad source keeps its
|
|
1162
|
+
// specific, user-actionable code + message (unchanged single-source UX).
|
|
1163
|
+
const acc = {
|
|
1164
|
+
envelopes: [],
|
|
1165
|
+
ingestedAtoms: [],
|
|
1166
|
+
sourceSummaries: [],
|
|
1167
|
+
skippedSources: [],
|
|
1168
|
+
};
|
|
1169
|
+
for (let i = 0; i < sources.length; i += 1) {
|
|
1170
|
+
const source = sources[i];
|
|
1171
|
+
if (source === undefined)
|
|
1172
|
+
continue;
|
|
1173
|
+
ingestSourceInto(acc, source, i, input, budgets);
|
|
1174
|
+
}
|
|
1175
|
+
if (acc.ingestedAtoms.length === 0) {
|
|
1176
|
+
const emptyDrift = allowEmptyDriftIngestion(input, acc, droppedSourceCount);
|
|
1177
|
+
if (emptyDrift !== undefined)
|
|
1178
|
+
return emptyDrift;
|
|
1179
|
+
throw (acc.firstSkipError ??
|
|
1180
|
+
new QiIngestionError("QI_SOURCE_EMPTY", "No usable evidence was produced from the sources."));
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
envelopes: acc.envelopes,
|
|
1184
|
+
ingestedAtoms: acc.ingestedAtoms,
|
|
1185
|
+
provenanceRefs: {
|
|
1186
|
+
envelopeIds: acc.envelopes.map((e) => String(e.id)),
|
|
1187
|
+
auditSummaryId: auditSummaryIdFor(input.runId),
|
|
1188
|
+
},
|
|
1189
|
+
sourceSummaries: acc.sourceSummaries,
|
|
1190
|
+
droppedSourceCount,
|
|
1191
|
+
skippedSources: acc.skippedSources,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
export async function ingestInlineSourcesAsync(input) {
|
|
1195
|
+
const allSources = input.request.sources;
|
|
1196
|
+
if (allSources.length === 0) {
|
|
1197
|
+
throw new QiIngestionError("QI_NO_SOURCES", "At least one source is required to start a run.");
|
|
1198
|
+
}
|
|
1199
|
+
const sources = allSources.slice(0, MAX_QI_SOURCES);
|
|
1200
|
+
const droppedSourceCount = allSources.length - sources.length;
|
|
1201
|
+
const budgets = {
|
|
1202
|
+
atomBudget: perSourceAtomBudget(MAX_TOTAL_ATOMS, sources.length),
|
|
1203
|
+
byteBudget: perSourceByteBudget(sources.length),
|
|
1204
|
+
};
|
|
1205
|
+
const acc = {
|
|
1206
|
+
envelopes: [],
|
|
1207
|
+
ingestedAtoms: [],
|
|
1208
|
+
sourceSummaries: [],
|
|
1209
|
+
skippedSources: [],
|
|
1210
|
+
};
|
|
1211
|
+
for (let i = 0; i < sources.length; i += 1) {
|
|
1212
|
+
const source = sources[i];
|
|
1213
|
+
if (source === undefined)
|
|
1214
|
+
continue;
|
|
1215
|
+
await ingestSourceIntoAsync(acc, source, i, input, budgets);
|
|
1216
|
+
}
|
|
1217
|
+
if (acc.ingestedAtoms.length === 0) {
|
|
1218
|
+
const emptyDrift = allowEmptyDriftIngestion(input, acc, droppedSourceCount);
|
|
1219
|
+
if (emptyDrift !== undefined)
|
|
1220
|
+
return emptyDrift;
|
|
1221
|
+
throw (acc.firstSkipError ??
|
|
1222
|
+
new QiIngestionError("QI_SOURCE_EMPTY", "No usable evidence was produced from the sources."));
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
envelopes: acc.envelopes,
|
|
1226
|
+
ingestedAtoms: acc.ingestedAtoms,
|
|
1227
|
+
provenanceRefs: {
|
|
1228
|
+
envelopeIds: acc.envelopes.map((e) => String(e.id)),
|
|
1229
|
+
auditSummaryId: auditSummaryIdFor(input.runId),
|
|
1230
|
+
},
|
|
1231
|
+
sourceSummaries: acc.sourceSummaries,
|
|
1232
|
+
droppedSourceCount,
|
|
1233
|
+
skippedSources: acc.skippedSources,
|
|
1234
|
+
};
|
|
1235
|
+
}
|