talos-code 0.0.1 → 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/CHANGELOG.md +87 -0
- package/LICENSE +661 -0
- package/README.md +116 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +100 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +244 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +46 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +92 -0
- package/dist/i18n/en/errors.js +267 -0
- package/dist/i18n/en/firstrun.js +101 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +774 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +331 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +265 -0
- package/dist/runtime/talos-composition.js +1011 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +4098 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +560 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +38 -0
- package/dist/tui/components/terminal-shell.js +214 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +148 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +56 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +43 -0
- package/dist/tui/overlays/approval-dialog.js +642 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +77 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +171 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +289 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +791 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +82 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +27 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +64 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { talosResearchRegistrableHost } from './independence.mjs'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* PORTO FEDELE di AVM/mobile/src/lib/research/researchCitationExport.ts (118 righe, 11/09/2026).
|
|
5
|
+
*
|
|
6
|
+
* ⛔⛔ EXPORT-06 — le fonti in un formato che un gestore di bibliografie legge.
|
|
7
|
+
*
|
|
8
|
+
* Il rapporto esce già in Markdown e PDF, che sono per una persona. Chi fa un
|
|
9
|
+
* lavoro serio con delle fonti però le mette in Zotero, Mendeley o EndNote, e
|
|
10
|
+
* quei programmi parlano **BibTeX** e **RIS**. Senza, ogni riferimento va
|
|
11
|
+
* ricopiato a mano — ed è esattamente il punto in cui una bibliografia si
|
|
12
|
+
* sporca.
|
|
13
|
+
*
|
|
14
|
+
* ## ⛔ Cosa NON esce, e perché è una decisione di privacy
|
|
15
|
+
*
|
|
16
|
+
* Una citazione descrive una PAGINA, non chi l'ha letta. Da qui non escono la
|
|
17
|
+
* query che l'ha trovata, il modello che l'ha giudicata, l'identificativo della
|
|
18
|
+
* ricerca né la chat da cui viene. Un file di bibliografia finisce in una
|
|
19
|
+
* cartella condivisa, in un allegato, in un repository: è il posto meno
|
|
20
|
+
* controllato in cui un dato personale possa arrivare.
|
|
21
|
+
*
|
|
22
|
+
* ⇒ La forma in ingresso dichiara i quattro campi che servono e **basta**: ciò
|
|
23
|
+
* che il chiamante passa in più non viene nemmeno letto.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} TalosResearchCitation
|
|
28
|
+
* @property {string} url
|
|
29
|
+
* @property {string} title
|
|
30
|
+
* @property {string | null} publishedAt ISO, o `null` quando la pagina non la dichiara. Non si inventa.
|
|
31
|
+
* @property {string} accessedAt Quando l'abbiamo letta: per una pagina web è il campo che conta di più.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* L'anno, solo se la data c'è ed è leggibile.
|
|
36
|
+
* @param {string | null} iso
|
|
37
|
+
* @returns {string | null}
|
|
38
|
+
*/
|
|
39
|
+
function anno(iso) {
|
|
40
|
+
if (!iso) return null
|
|
41
|
+
const trovato = /^(\d{4})/.exec(iso.trim())
|
|
42
|
+
return trovato ? trovato[1] : null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ⛔ Le graffe e le barre rovesce nel titolo ROMPONO il file.
|
|
47
|
+
*
|
|
48
|
+
* In BibTeX una graffa apre un gruppo: un titolo che ne contiene una spaiata fa
|
|
49
|
+
* fallire il parsing dell'intera voce — e i gestori non lo dicono, saltano la
|
|
50
|
+
* voce. Si tolgono, invece di scappare: un titolo è testo da leggere, non
|
|
51
|
+
* codice, e nessun lettore ha bisogno delle graffe originali.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} testo
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function bibtexSicuro(testo) {
|
|
57
|
+
return testo
|
|
58
|
+
.replace(/[{}\\]/g, '')
|
|
59
|
+
.replace(/\s+/g, ' ')
|
|
60
|
+
.trim()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* In RIS ogni riga è un campo: un a capo dentro un valore spezza il record.
|
|
65
|
+
* @param {string} testo
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function risSicuro(testo) {
|
|
69
|
+
return testo.replace(/\s+/g, ' ').trim()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* La chiave della voce: dominio + anno, e un suffisso quando serve.
|
|
74
|
+
*
|
|
75
|
+
* ⛔ Due fonti dello stesso dominio nello stesso anno darebbero la stessa
|
|
76
|
+
* chiave, e un file con due chiavi uguali non è valido: il gestore ne butta una
|
|
77
|
+
* **in silenzio**. Chi esporta se ne accorge quando gli manca una citazione in
|
|
78
|
+
* fondo a un lavoro finito.
|
|
79
|
+
*
|
|
80
|
+
* @param {readonly TalosResearchCitation[]} citations
|
|
81
|
+
* @returns {string[]}
|
|
82
|
+
*/
|
|
83
|
+
function chiaviDistinte(citations) {
|
|
84
|
+
/** @type {Map<string, number>} */
|
|
85
|
+
const viste = new Map()
|
|
86
|
+
return citations.map((citation) => {
|
|
87
|
+
const host = talosResearchRegistrableHost(citation.url) ?? 'fonte'
|
|
88
|
+
const base = `${host.replace(/[^a-z0-9]/gi, '')}${anno(citation.publishedAt) ?? ''}`
|
|
89
|
+
const quante = viste.get(base) ?? 0
|
|
90
|
+
viste.set(base, quante + 1)
|
|
91
|
+
// La prima resta pulita: `example2026`, poi `example2026b`, `example2026c`.
|
|
92
|
+
return quante === 0 ? base : `${base}${String.fromCharCode(97 + quante)}`
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {readonly TalosResearchCitation[]} citations
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
export function talosResearchBibtex(citations) {
|
|
101
|
+
if (citations.length === 0) return ''
|
|
102
|
+
const chiavi = chiaviDistinte(citations)
|
|
103
|
+
|
|
104
|
+
return citations.map((citation, indice) => {
|
|
105
|
+
const righe = [
|
|
106
|
+
` title = {${bibtexSicuro(citation.title)}}`,
|
|
107
|
+
` url = {${citation.url.trim()}}`,
|
|
108
|
+
` urldate = {${citation.accessedAt.trim()}}`,
|
|
109
|
+
]
|
|
110
|
+
const quando = anno(citation.publishedAt)
|
|
111
|
+
// ⛔ Un anno inventato è peggio di un anno mancante: chi cita si fida.
|
|
112
|
+
if (quando) righe.splice(1, 0, ` year = {${quando}}`)
|
|
113
|
+
// `@misc` è il tipo per una risorsa online: `@article` prometterebbe una
|
|
114
|
+
// rivista, e una pagina web non lo è.
|
|
115
|
+
return `@misc{${chiavi[indice]},\n${righe.join(',\n')},\n}`
|
|
116
|
+
}).join('\n\n')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {readonly TalosResearchCitation[]} citations
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function talosResearchRis(citations) {
|
|
124
|
+
if (citations.length === 0) return ''
|
|
125
|
+
|
|
126
|
+
return citations.map((citation) => {
|
|
127
|
+
const righe = [
|
|
128
|
+
// ELEC = risorsa elettronica, il tipo giusto per una pagina web.
|
|
129
|
+
'TY - ELEC',
|
|
130
|
+
`TI - ${risSicuro(citation.title)}`,
|
|
131
|
+
`UR - ${citation.url.trim()}`,
|
|
132
|
+
]
|
|
133
|
+
const quando = anno(citation.publishedAt)
|
|
134
|
+
if (quando) righe.push(`PY - ${quando}`)
|
|
135
|
+
// Y2 è la data di consultazione, e RIS la vuole con le barre.
|
|
136
|
+
righe.push(`Y2 - ${citation.accessedAt.trim().replace(/-/g, '/')}`)
|
|
137
|
+
// ⛔ `ER` chiude il record e la sua riga finisce con uno spazio: è la
|
|
138
|
+
// forma che i gestori si aspettano, e senza non riconoscono la fine.
|
|
139
|
+
righe.push('ER - ')
|
|
140
|
+
return righe.join('\n')
|
|
141
|
+
}).join('\n\n')
|
|
142
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PORTO FEDELE di AVM/mobile/src/lib/research/researchCollector.ts (155 righe, 11/09/2026).
|
|
3
|
+
*
|
|
4
|
+
* Raccogliere quanto vale una linea d'indagine, e tenerlo.
|
|
5
|
+
*
|
|
6
|
+
* R-3. La raccolta in sé non ha niente di notevole — cerca, leggi i primi
|
|
7
|
+
* risultati, scrivi cos'hanno detto. La decisione che conta è che il TESTO
|
|
8
|
+
* ESTRATTO SI TIENE, ed è l'unica cosa in questa fase che nessun altro fa.
|
|
9
|
+
*
|
|
10
|
+
* Ogni prodotto di ricerca sul mercato conserva link. Un link è una promessa su
|
|
11
|
+
* una pagina, e le pagine marciscono: vengono modificate, messe dietro un
|
|
12
|
+
* paywall, spostate, o semplicemente spente. Un dossier fatto di link degrada
|
|
13
|
+
* in silenzio — le citazioni sembrano a posto finché qualcuno non ne clicca una
|
|
14
|
+
* — e non si può ricontrollare affatto, perché la cosa che citava non c'è più.
|
|
15
|
+
* Tenere il passaggio rende il dossier interrogabile un anno dopo, rende
|
|
16
|
+
* possibile la ri-verifica di R12, ed è impossibile da copiare per chiunque il
|
|
17
|
+
* cui prodotto sia un elenco di URL.
|
|
18
|
+
*
|
|
19
|
+
* L'altra regola qui è quella che questo progetto ha imparato a caro prezzo:
|
|
20
|
+
* una fonte che non si è potuta leggere si REGISTRA, non si butta mai. Una
|
|
21
|
+
* raccolta che torna in silenzio con quattro fonti invece di sei sembra un tema
|
|
22
|
+
* povero invece di uno scaricamento rotto, e la differenza è tutto ciò che
|
|
23
|
+
* decide se la risposta si possa credere.
|
|
24
|
+
*
|
|
25
|
+
* ⛔ L'I/O entra da `deps` (`search`, `read`): i test usano finti, mai la rete.
|
|
26
|
+
*
|
|
27
|
+
* ## L6 (11/09/2026) — la cache e il budget entrano dalla STESSA porta
|
|
28
|
+
*
|
|
29
|
+
* Due cose nuove, e nessuna delle due è logica di raccolta: sono I/O, quindi
|
|
30
|
+
* arrivano da `deps` come `search` e `read`, e senza di loro questo file si
|
|
31
|
+
* comporta ESATTAMENTE come il porto fedele dal mobile.
|
|
32
|
+
*
|
|
33
|
+
* - `deps.cache` (`fetch-cache.mjs`): le linee d'indagine di una ricerca
|
|
34
|
+
* profonda partono dalla stessa domanda e aprono le stesse pagine. Con la
|
|
35
|
+
* cache la pagina si apre una volta per corsa; senza, ogni ramo ripaga.
|
|
36
|
+
* - `deps.budget` (`page-budget.mjs`): quanto di una pagina vede il modello,
|
|
37
|
+
* con il resto CONSERVATO e un riferimento leggibile.
|
|
38
|
+
*
|
|
39
|
+
* ⛔ Il testo INTERO resta in `source.text`, ed è deliberato: è il dossier, ed è
|
|
40
|
+
* ciò che `talosResearchLocate` (`verification.mjs:201`) legge per decidere se
|
|
41
|
+
* un passaggio citato esiste davvero. Cercare nella finestra farebbe risultare
|
|
42
|
+
* INVENTATA una citazione onesta caduta nel mezzo tagliato — il verso peggiore.
|
|
43
|
+
* Al modello va `source.window`, che è una cosa diversa e si chiama diversamente.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {import('./run.mjs').TalosResearchBranch} TalosResearchBranch
|
|
48
|
+
* @typedef {import('./run.mjs').TalosResearchSpend} TalosResearchSpend
|
|
49
|
+
* @typedef {import('./page-budget.mjs').TalosResearchPageReference} TalosResearchPageReference
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { TALOS_RESEARCH_PAGE_BUDGET, TALOS_RESEARCH_HEAD_SHARE, talosResearchPageBudget } from './page-budget.mjs';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} TalosResearchSource
|
|
56
|
+
* @property {string} url
|
|
57
|
+
* @property {string} title
|
|
58
|
+
* @property {string | null} publishedAt Quello che dice la FONTE, o null. Mai la data di oggi, mai un'ipotesi.
|
|
59
|
+
* @property {string} text
|
|
60
|
+
* Il testo leggibile, com'era il giorno in cui è stato letto. Questo È il
|
|
61
|
+
* dossier. Tutto quello che viene dopo — la sintesi, il controllo delle
|
|
62
|
+
* citazioni, la ri-verifica un anno dopo — legge questo e non la rete.
|
|
63
|
+
* @property {'page' | 'snippet'} obtained
|
|
64
|
+
* Quanta parte della fonte è davvero qui. Uno snippet è quello che il motore
|
|
65
|
+
* di ricerca ha mostrato; una pagina è quello che la pagina ha detto.
|
|
66
|
+
* Dichiarato perché un'affermazione sostenuta solo da uno snippet è una prova
|
|
67
|
+
* più debole, e nasconderlo farebbe sembrare uguali le due cose.
|
|
68
|
+
* @property {string} [window]
|
|
69
|
+
* L6. Quanto di `text` va al modello: testa + marcatore + coda quando la
|
|
70
|
+
* pagina supera il budget, `text` identico quando non lo supera o quando
|
|
71
|
+
* nessun budget è configurato. ⛔ Mai la fonte della verità: quella è `text`.
|
|
72
|
+
* @property {number} [omitted] Caratteri che la finestra non mostra. Contati, mai stimati.
|
|
73
|
+
* @property {boolean} [fromCache]
|
|
74
|
+
* L6. Questa pagina NON è stata riaperta: l'aveva già pagata un altro ramo
|
|
75
|
+
* della stessa corsa. Il registro la conta a parte da quelle aperte davvero —
|
|
76
|
+
* «9 pagine, 4 servite dalla cache» dice una cosa che «9 pagine» non dice.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {object} TalosResearchUnreachable
|
|
81
|
+
* @property {string} url
|
|
82
|
+
* @property {string} reason
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {object} TalosResearchCollection
|
|
87
|
+
* @property {string} branchId
|
|
88
|
+
* @property {string} query
|
|
89
|
+
* @property {readonly TalosResearchSource[]} sources
|
|
90
|
+
* @property {readonly TalosResearchUnreachable[]} unreachable Mai vuota in silenzio: quello che non si è potuto leggere ha un nome.
|
|
91
|
+
* @property {TalosResearchSpend} spend
|
|
92
|
+
* MISURATA, non stimata. Il piano indovina; il giro conta.
|
|
93
|
+
* ⛔ L6: è la SPESA, quindi ciò che la cache ha servito NON è qui dentro —
|
|
94
|
+
* una pagina non riaperta non è stata pagata, e contarla renderebbe invisibile
|
|
95
|
+
* il guadagno proprio nel numero fatto per mostrare il costo. Quanto è arrivato
|
|
96
|
+
* gratis sta in `fromCache`, così chi legge ha tutte e due le cifre e nessuna
|
|
97
|
+
* delle due mente.
|
|
98
|
+
* @property {{searches: number, pages: number}} fromCache
|
|
99
|
+
* L6. Quante ricerche e quante pagine sono arrivate dalla cache della corsa.
|
|
100
|
+
* `spend.pages + fromCache.pages` è il numero di fonti ottenute.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @typedef {object} TalosResearchCollectorDeps
|
|
105
|
+
* @property {(query: string, maxResults: number) => Promise<readonly {url:string, title:string, snippet:string, publishedAt:string|null}[]>} search
|
|
106
|
+
* @property {(url: string) => Promise<{title:string, text:string, publishedAt:string|null} | null>} read
|
|
107
|
+
* @property {import('./fetch-cache.mjs').TalosResearchFetchCache} [cache]
|
|
108
|
+
* L6. Se c'è, una ricerca o una pagina già viste in questa corsa non si
|
|
109
|
+
* ripagano. Se non c'è, questo file fa esattamente quello che faceva prima.
|
|
110
|
+
* @property {string} [provider]
|
|
111
|
+
* Chi risponde. Entra nella chiave: due fornitori non danno la stessa pagina,
|
|
112
|
+
* e servire l'una per l'altra sarebbe una bugia silenziosa.
|
|
113
|
+
* @property {object} [budget] L6. Se assente, nessuna finestra: `window === text`.
|
|
114
|
+
* @property {number} [budget.cap] Caratteri mostrati al modello. Predefinito `TALOS_RESEARCH_PAGE_BUDGET`.
|
|
115
|
+
* @property {number} [budget.headShare] Quota in testa. Predefinita `TALOS_RESEARCH_HEAD_SHARE`.
|
|
116
|
+
* @property {number} [budget.keep]
|
|
117
|
+
* Quanto testo si CONSERVA per fonte. Predefinito `MAX_CHARS_PER_SOURCE`
|
|
118
|
+
* (20.000, il tetto del telefono). Sul desktop il dossier sta su disco e non
|
|
119
|
+
* in un database solo: chi lo sa alza questo, e il budget resta sotto.
|
|
120
|
+
* @property {(url: string) => (TalosResearchPageReference | null)} [budget.reference]
|
|
121
|
+
* Dove il chiamante ha depositato il testo intero di quella pagina, e la
|
|
122
|
+
* chiamata esatta per sfogliarlo. ⛔ Non si inventa: chi non ha un deposito
|
|
123
|
+
* non passa niente e il marcatore tace invece di promettere un percorso.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Quanto si tiene di una pagina.
|
|
128
|
+
*
|
|
129
|
+
* Un articolo lungo sono decine di migliaia di caratteri e un telefono tiene
|
|
130
|
+
* l'intero dossier in un database solo. Si taglia qui invece che al momento
|
|
131
|
+
* della sintesi: quello che si tiene è quello che si potrà ri-verificare più
|
|
132
|
+
* tardi, quindi il confine appartiene a dove la prova è CONSERVATA e non a dove
|
|
133
|
+
* capita che venga letta.
|
|
134
|
+
*/
|
|
135
|
+
const MAX_CHARS_PER_SOURCE = 20_000;
|
|
136
|
+
|
|
137
|
+
/** Quattro caratteri per token è la regola pratica consueta per la prosa. */
|
|
138
|
+
const CHARS_PER_TOKEN = 4;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} text
|
|
142
|
+
* @param {number} [tetto]
|
|
143
|
+
* @returns {string}
|
|
144
|
+
*/
|
|
145
|
+
function trim(text, tetto = MAX_CHARS_PER_SOURCE) {
|
|
146
|
+
const squeezed = text.replace(/\s+/g, ' ').trim();
|
|
147
|
+
return squeezed.length > tetto ? squeezed.slice(0, tetto) : squeezed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {TalosResearchCollectorDeps} deps
|
|
152
|
+
* @param {TalosResearchBranch} branch
|
|
153
|
+
* @returns {Promise<TalosResearchCollection>}
|
|
154
|
+
*/
|
|
155
|
+
export async function talosResearchCollect(deps, branch) {
|
|
156
|
+
const wanted = Math.max(1, branch.estimate.pages);
|
|
157
|
+
|
|
158
|
+
/*
|
|
159
|
+
* ⛔ Senza cache questa è la funzione identità sul produttore: la porta è
|
|
160
|
+
* sempre la stessa, quindi non ci sono due percorsi da tenere allineati — e
|
|
161
|
+
* un ramo che esiste solo quando una dipendenza c'è è un ramo che nessun test
|
|
162
|
+
* predefinito attraversa.
|
|
163
|
+
*/
|
|
164
|
+
const cache = deps.cache ?? null;
|
|
165
|
+
/**
|
|
166
|
+
* @template T
|
|
167
|
+
* @param {import('./fetch-cache.mjs').TalosResearchFetchDescriptor} descrittore
|
|
168
|
+
* @param {() => Promise<T>} produttore
|
|
169
|
+
* @returns {Promise<{value: T, fromCache: boolean}>}
|
|
170
|
+
*/
|
|
171
|
+
const attorno = async (descrittore, produttore) => (
|
|
172
|
+
cache ? cache.around(descrittore, produttore) : { value: await produttore(), fromCache: false }
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const conserva = Math.max(1, Math.trunc(deps.budget?.keep ?? MAX_CHARS_PER_SOURCE));
|
|
176
|
+
/** @param {string} testo @param {string} url */
|
|
177
|
+
const finestra = (testo, url) => {
|
|
178
|
+
if (!deps.budget) return { window: testo, omitted: 0 };
|
|
179
|
+
const tagliata = talosResearchPageBudget(testo, {
|
|
180
|
+
cap: deps.budget.cap ?? TALOS_RESEARCH_PAGE_BUDGET,
|
|
181
|
+
headShare: deps.budget.headShare ?? TALOS_RESEARCH_HEAD_SHARE,
|
|
182
|
+
reference: deps.budget.reference ? deps.budget.reference(url) : null,
|
|
183
|
+
});
|
|
184
|
+
return { window: tagliata.window, omitted: tagliata.omitted };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const ricerca = await attorno(
|
|
188
|
+
{ kind: 'search', query: branch.question, limit: wanted, provider: deps.provider },
|
|
189
|
+
() => deps.search(branch.question, wanted),
|
|
190
|
+
);
|
|
191
|
+
const found = ricerca.value;
|
|
192
|
+
|
|
193
|
+
/** @type {TalosResearchSource[]} */
|
|
194
|
+
const sources = [];
|
|
195
|
+
/** @type {TalosResearchUnreachable[]} */
|
|
196
|
+
const unreachable = [];
|
|
197
|
+
let pages = 0;
|
|
198
|
+
let pagineDallaCache = 0;
|
|
199
|
+
|
|
200
|
+
for (const result of found) {
|
|
201
|
+
/** @type {{title:string, text:string, publishedAt:string|null} | null} */
|
|
202
|
+
let extracted = null;
|
|
203
|
+
let dallaCache = false;
|
|
204
|
+
try {
|
|
205
|
+
const letta = await attorno(
|
|
206
|
+
{ kind: 'extract', url: result.url, provider: deps.provider },
|
|
207
|
+
() => deps.read(result.url),
|
|
208
|
+
);
|
|
209
|
+
extracted = letta.value;
|
|
210
|
+
dallaCache = letta.fromCache;
|
|
211
|
+
} catch (failure) {
|
|
212
|
+
unreachable.push({
|
|
213
|
+
url: result.url,
|
|
214
|
+
reason: failure instanceof Error ? failure.message : 'unknown',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (extracted && extracted.text.trim().length > 0) {
|
|
219
|
+
if (dallaCache) pagineDallaCache += 1;
|
|
220
|
+
else pages += 1;
|
|
221
|
+
const testo = trim(extracted.text, conserva);
|
|
222
|
+
sources.push({
|
|
223
|
+
url: result.url,
|
|
224
|
+
title: extracted.title || result.title,
|
|
225
|
+
// La data della pagina vince su quella del motore di ricerca: una è
|
|
226
|
+
// l'editore che parla, l'altra è un indice che indovina.
|
|
227
|
+
publishedAt: extracted.publishedAt ?? result.publishedAt,
|
|
228
|
+
text: testo,
|
|
229
|
+
obtained: 'page',
|
|
230
|
+
...finestra(testo, result.url),
|
|
231
|
+
fromCache: dallaCache,
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Si ripiega sullo snippet invece di buttare la fonte. Un risultato di
|
|
237
|
+
// ricerca che non si è potuto aprire dice comunque qualcosa, e dirlo con
|
|
238
|
+
// `obtained: 'snippet'` è onesto in un modo in cui il silenzio non è.
|
|
239
|
+
if (!extracted && unreachable.every((entry) => entry.url !== result.url)) {
|
|
240
|
+
unreachable.push({ url: result.url, reason: 'unreadable' });
|
|
241
|
+
}
|
|
242
|
+
if (result.snippet.trim().length > 0) {
|
|
243
|
+
const testo = trim(result.snippet, conserva);
|
|
244
|
+
sources.push({
|
|
245
|
+
url: result.url,
|
|
246
|
+
title: result.title,
|
|
247
|
+
publishedAt: result.publishedAt,
|
|
248
|
+
text: testo,
|
|
249
|
+
obtained: 'snippet',
|
|
250
|
+
...finestra(testo, result.url),
|
|
251
|
+
fromCache: dallaCache,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
* ⛔ Si conta la FINESTRA, non il testo conservato: i token si pagano su ciò
|
|
258
|
+
* che arriva al modello, e conservare di più non costa un token. Senza budget
|
|
259
|
+
* la finestra È il testo, quindi questo numero è identico a prima.
|
|
260
|
+
*/
|
|
261
|
+
const characters = sources.reduce((total, source) => total + (source.window ?? source.text).length, 0);
|
|
262
|
+
return {
|
|
263
|
+
branchId: branch.id,
|
|
264
|
+
query: branch.question,
|
|
265
|
+
sources,
|
|
266
|
+
unreachable,
|
|
267
|
+
spend: {
|
|
268
|
+
searches: ricerca.fromCache ? 0 : 1,
|
|
269
|
+
pages,
|
|
270
|
+
// Quello che è stato davvero assorbito, contato dal testo che è qui.
|
|
271
|
+
tokens: Math.ceil(characters / CHARS_PER_TOKEN),
|
|
272
|
+
},
|
|
273
|
+
fromCache: { searches: ricerca.fromCache ? 1 : 0, pages: pagineDallaCache },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** BC-49, contratto v1. Fonti e decisioni nel rapporto del 12/09/2026.
|
|
2
|
+
* Checkpoint applicativi completi, mai frammenti di JSON del fornitore.
|
|
3
|
+
* Il tetto riguarda gli argomenti, non promette una durata del ragionamento.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { percorsoGiornale, scriviRapporto } from '../research-store.mjs';
|
|
8
|
+
|
|
9
|
+
export const LIMITE_PARTE_RAPPORTO_BYTE = 4096;
|
|
10
|
+
const MASSIMO_PARTI = 512;
|
|
11
|
+
const code = new Map();
|
|
12
|
+
const impronta = testo => createHash('sha256').update(testo, 'utf8').digest('hex');
|
|
13
|
+
const ordinato = valore => Array.isArray(valore) ? valore.map(ordinato)
|
|
14
|
+
: valore && typeof valore === 'object' ? Object.fromEntries(Object.keys(valore).sort().map(k => [k, ordinato(valore[k])])) : valore;
|
|
15
|
+
const serializza = valore => JSON.stringify(ordinato(valore));
|
|
16
|
+
|
|
17
|
+
function controlla(contenuto) {
|
|
18
|
+
const { parte, testo, affermazioni, fonti } = contenuto;
|
|
19
|
+
if (!parte || !Number.isSafeInteger(parte.indice) || parte.indice < 1 || parte.indice > MASSIMO_PARTI || typeof parte.ultima !== 'boolean') {
|
|
20
|
+
return 'La parte richiede un indice intero da 1 a 512 e ultima vero o falso.';
|
|
21
|
+
}
|
|
22
|
+
if (typeof testo !== 'string' || !Array.isArray(affermazioni) || !Array.isArray(fonti)) return 'Ogni parte richiede testo e gli elenchi affermazioni e fonti, anche vuoti.';
|
|
23
|
+
if (affermazioni.some(a => !a || typeof a !== 'object' || Array.isArray(a) || typeof a.testo !== 'string' || !a.testo.trim()
|
|
24
|
+
|| !((typeof a.fonte === 'string' && a.fonte.trim()) || Number.isSafeInteger(a.fonte)))) return 'Ogni affermazione richiede testo e fonte; usa il suo URL completo.';
|
|
25
|
+
if (fonti.some(f => !f || typeof f.url !== 'string' || !/^https?:\/\//i.test(f.url.trim()))) return 'Ogni fonte richiede un URL HTTP(S) completo.';
|
|
26
|
+
if (Buffer.byteLength(JSON.stringify(contenuto), 'utf8') > LIMITE_PARTE_RAPPORTO_BYTE) return `La parte supera ${LIMITE_PARTE_RAPPORTO_BYTE} byte: dividila prima di riprovare con lo stesso indice.`;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Replay rigoroso delle parti; gli altri eventi restano proprietà del motore L9/BC-44. */
|
|
31
|
+
export function rileggiPartiRapporto(eventi) {
|
|
32
|
+
const parti = [];
|
|
33
|
+
let conclusione = null;
|
|
34
|
+
for (const evento of eventi) {
|
|
35
|
+
if (evento?.kind === 'deposit_part') {
|
|
36
|
+
const contenuto = evento.contenuto;
|
|
37
|
+
if (!contenuto || controlla(contenuto) || evento.versione !== 1 || evento.indice !== contenuto.parte.indice
|
|
38
|
+
|| evento.impronta !== impronta(serializza(contenuto))) throw new Error('Integrità delle parti non verificabile: impronta o contenuto del giornale non valido.');
|
|
39
|
+
const precedente = parti[evento.indice - 1];
|
|
40
|
+
if (precedente) {
|
|
41
|
+
if (precedente.impronta !== evento.impronta) throw new Error('Integrità del giornale: due contenuti diversi per la stessa parte.');
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (evento.indice !== parti.length + 1 || parti.at(-1)?.contenuto.parte.ultima || conclusione) throw new Error('Integrità del giornale: manca una parte oppure il deposito è già chiuso.');
|
|
45
|
+
parti.push(evento);
|
|
46
|
+
} else if (evento?.kind === 'deposit_finished') {
|
|
47
|
+
if (!parti.at(-1)?.contenuto.parte.ultima || evento.ultimaImpronta !== parti.at(-1).impronta
|
|
48
|
+
|| !/^[a-f0-9]{64}$/.test(evento.impronta ?? '') || !evento.risultato?.ok) throw new Error('Integrità della chiusura del deposito non verificabile.');
|
|
49
|
+
conclusione = evento;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { parti, conclusione, prossimaParte: parti.length + 1 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function assembla(parti) {
|
|
56
|
+
return {
|
|
57
|
+
testo: parti.map(p => p.contenuto.testo).join(''),
|
|
58
|
+
affermazioni: parti.flatMap(p => p.contenuto.affermazioni),
|
|
59
|
+
fonti: parti.flatMap(p => p.contenuto.fonti),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Un solo scrittore per ricerca nel processo che possiede la corsa; conferma dopo fsync. */
|
|
64
|
+
export async function depositaParteRapporto(argomenti, { leggiGiornaleFn, accodaEventoFn, leggiRapportoFn, valida, finalizza, clock }) {
|
|
65
|
+
const { cartella, id, testo, affermazioni, fonti, parte, byteArgomenti } = argomenti;
|
|
66
|
+
const contenuto = structuredClone({ testo, affermazioni, fonti, parte });
|
|
67
|
+
const motivo = controlla(contenuto);
|
|
68
|
+
if (motivo) return { ok: false, motivo };
|
|
69
|
+
if (byteArgomenti !== undefined && (!Number.isSafeInteger(byteArgomenti) || byteArgomenti > LIMITE_PARTE_RAPPORTO_BYTE)) {
|
|
70
|
+
return { ok: false, motivo: `Gli argomenti generati superano ${LIMITE_PARTE_RAPPORTO_BYTE} byte. Dividi testo e prove; riprova lo stesso indice.` };
|
|
71
|
+
}
|
|
72
|
+
const chiave = `${resolve(cartella)}\0${id}`;
|
|
73
|
+
const precedente = code.get(chiave) ?? Promise.resolve();
|
|
74
|
+
const corrente = precedente.catch(() => {}).then(async () => {
|
|
75
|
+
const { eventi } = await leggiGiornaleFn({ cartella, id, rigoroso: true });
|
|
76
|
+
const stato = rileggiPartiRapporto(eventi);
|
|
77
|
+
const hash = impronta(serializza(contenuto));
|
|
78
|
+
const registrata = stato.parti[parte.indice - 1];
|
|
79
|
+
if (registrata && registrata.impronta !== hash) return { ok: false, motivo: `La parte ${parte.indice} è già registrata con un altro contenuto. Non riscriverla; prossima parte: ${stato.prossimaParte}.` };
|
|
80
|
+
if (!registrata && (parte.indice !== stato.prossimaParte || stato.parti.at(-1)?.contenuto.parte.ultima)) {
|
|
81
|
+
return { ok: false, motivo: `Ordine non valido: prossima parte ${stato.prossimaParte}. Una chiusura già registrata si ritenta identica.` };
|
|
82
|
+
}
|
|
83
|
+
const evento = registrata ?? {
|
|
84
|
+
kind: 'deposit_part', versione: 1, at: clock().toISOString(), indice: parte.indice,
|
|
85
|
+
impronta: hash, byte: byteArgomenti ?? Buffer.byteLength(JSON.stringify(contenuto), 'utf8'), contenuto,
|
|
86
|
+
};
|
|
87
|
+
const tutte = registrata ? stato.parti : [...stato.parti, evento];
|
|
88
|
+
const unito = parte.ultima ? assembla(tutte) : null;
|
|
89
|
+
if (unito) {
|
|
90
|
+
const controllo = valida(unito);
|
|
91
|
+
if (!controllo.ok) return controllo;
|
|
92
|
+
}
|
|
93
|
+
if (!registrata) await accodaEventoFn({ cartella, id, evento, separaRiga: true });
|
|
94
|
+
if (!parte.ultima) return {
|
|
95
|
+
ok: true, parziale: true, prossimaParte: tutte.length + 1,
|
|
96
|
+
messaggio: `Parte ${parte.indice} registrata (${evento.byte} byte). Prossima parte: ${tutte.length + 1}. Attendi questa conferma prima di proseguire; il rapporto sarà pronto soltanto dopo l'ultima parte.`,
|
|
97
|
+
contenutoRegistrato: `\n${JSON.stringify(evento)}\n`, percorsoRegistrato: percorsoGiornale(cartella, id),
|
|
98
|
+
};
|
|
99
|
+
if (stato.conclusione) {
|
|
100
|
+
const documento = await leggiRapportoFn({ cartella, id });
|
|
101
|
+
if (typeof documento !== 'string' || impronta(documento) !== stato.conclusione.impronta) throw new Error('Il rapporto concluso non coincide con l’impronta nel giornale: ripristinare il file prima di riprovare.');
|
|
102
|
+
return { ...stato.conclusione.risultato, documento, giaScritto: true };
|
|
103
|
+
}
|
|
104
|
+
const composto = await finalizza(unito);
|
|
105
|
+
if (!composto.ok) return composto;
|
|
106
|
+
await scriviRapporto({ cartella, id, testo: composto.documento });
|
|
107
|
+
const risultato = Object.fromEntries(['ok', 'affermazioni', 'fonti', 'senzaPassaggio', 'bilancio', 'giudice'].filter(k => composto[k] !== undefined).map(k => [k, composto[k]]));
|
|
108
|
+
await accodaEventoFn({ cartella, id, separaRiga: true, evento: {
|
|
109
|
+
kind: 'deposit_finished', versione: 1, at: clock().toISOString(), ultimaImpronta: hash,
|
|
110
|
+
impronta: impronta(composto.documento), byte: Buffer.byteLength(composto.documento, 'utf8'), risultato,
|
|
111
|
+
} });
|
|
112
|
+
return { ...composto, giaScritto: true };
|
|
113
|
+
});
|
|
114
|
+
code.set(chiave, corrente);
|
|
115
|
+
try { return await corrente; }
|
|
116
|
+
finally { if (code.get(chiave) === corrente) code.delete(chiave); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** La parte statica resta identica fra i giri; il progresso compare solo alla ripresa. */
|
|
120
|
+
export function consegnaPartiRapporto(eventi = []) {
|
|
121
|
+
const stato = rileggiPartiRapporto(eventi);
|
|
122
|
+
const istruzioni = [
|
|
123
|
+
'Deposita il rapporto progressivamente con research_deposit dopo ogni ramo del piano, una sezione per volta, senza aspettare la fine della ricerca.',
|
|
124
|
+
'Aggiungi sempre parte:{indice:1,ultima:false}, poi aumenta indice di uno dopo ogni conferma. Questa istruzione sostituisce eventuali vecchie richieste di deposito unico.',
|
|
125
|
+
`Ogni chiamata deve contenere al massimo ${LIMITE_PARTE_RAPPORTO_BYTE} byte UTF-8 di argomenti JSON complessivi; punta a 3000 byte, circa 400 parole fra prosa e prove. Una sola chiamata di deposito per risposta: attendi la conferma prima di generare la successiva.`,
|
|
126
|
+
'testo contiene solo la sezione corrente, con gli a capo necessari: il server concatena i pezzi senza aggiungere separatori. Il primo pezzo porta il titolo; gli ultimi completano conclusioni e fonti.',
|
|
127
|
+
'Distribuisci anche affermazioni e fonti tra le parti, senza ripetere quelle già inviate; usa URL esatti in fonte. Gli elenchi possono essere vuoti. Non accumulare tutte le prove in una chiamata finale enorme.',
|
|
128
|
+
'Per chiudere usa ultima:true; se tutto è già registrato, invia testo:"", affermazioni:[], fonti:[] con il prossimo indice e ultima:true. Soltanto allora il server assembla, verifica e pubblica il rapporto.',
|
|
129
|
+
'Il messaggio finale in chat è solo una breve conferma, mai il rapporto: questa regola sostituisce anche eventuali vecchie istruzioni di scriverlo come ultimo messaggio.',
|
|
130
|
+
'Non rigenerare i pezzi confermati e non ricopiare l’intero rapporto. In caso di conferma persa, ripeti lo stesso indice con gli stessi contenuti. Scrivi rapporto e messaggi per la persona in italiano, senza nomi tecnici degli attrezzi.',
|
|
131
|
+
];
|
|
132
|
+
if (stato.parti.length) {
|
|
133
|
+
istruzioni.push(`Parti già registrate: ${stato.parti.length}. ${stato.parti.at(-1).contenuto.parte.ultima ? 'Non aggiungere altre parti.' : `Prossima parte: ${stato.prossimaParte}.`} I contenuti confermati sono nel giornale della ricerca; leggili solo se ti servono per proseguire, senza rispedirli.`);
|
|
134
|
+
if (stato.parti.at(-1).contenuto.parte.ultima) {
|
|
135
|
+
istruzioni.push('L’ultima parte è già registrata: completa o recupera la consegna ripetendo ESATTAMENTE questa chiamata, senza altra prosa:', JSON.stringify(stato.parti.at(-1).contenuto));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return istruzioni.join('\n');
|
|
139
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PORTO FEDELE di AVM/mobile/src/lib/research/researchDossier.ts (94 righe, 11/09/2026).
|
|
3
|
+
*
|
|
4
|
+
* ⛔ `collector.mjs` (porto di `researchCollector.ts`) lo cura un'altra sessione:
|
|
5
|
+
* qui ne servono solo i TIPI, che in JSDoc non producono nessun import a runtime.
|
|
6
|
+
* Questo file quindi gira da solo.
|
|
7
|
+
*
|
|
8
|
+
* Il dossier come UNA cosa che due lettori sanno usare.
|
|
9
|
+
*
|
|
10
|
+
* La raccolta di un ramo deve servire una persona che sfoglia la Libreria e un
|
|
11
|
+
* processo che riprende ore dopo, in un'istanza nuova, senza memoria di che
|
|
12
|
+
* cosa aveva raccolto. Le due risposte ovvie sono sbagliate entrambe. Due file
|
|
13
|
+
* — uno di prosa, uno di dati — sono due verità da tenere allineate, che è il
|
|
14
|
+
* difetto che questo progetto ha già pagato più volte. Rileggere la prosa è
|
|
15
|
+
* peggio: funziona finché qualcuno non cambia un titolo di sezione.
|
|
16
|
+
*
|
|
17
|
+
* Quindi è un documento solo: testo leggibile per la persona e per l'indice di
|
|
18
|
+
* ricerca, col record strutturato recintato in fondo. Tutti e due scritti dallo
|
|
19
|
+
* stesso oggetto nello stesso respiro, così non possono divergere, e il lettore
|
|
20
|
+
* che ha bisogno dei dati non legge mai la prosa.
|
|
21
|
+
*
|
|
22
|
+
* Un dossier senza il recinto non si indovina. Si dichiara illeggibile — un
|
|
23
|
+
* file vecchio, o uno che qualcuno ha modificato — perché una sintesi costruita
|
|
24
|
+
* su una lettura speranzosa citerebbe passaggi che non sono mai stati controllati.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @import { TalosResearchCollection, TalosResearchSource } from './collector.mjs'
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const FENCE_OPEN = '```talos-research-json'
|
|
32
|
+
const FENCE_CLOSE = '```'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Che cosa porta il recinto. Con la versione, così una forma successiva si riconosce.
|
|
36
|
+
*
|
|
37
|
+
* @typedef {object} TalosResearchDossierRecord
|
|
38
|
+
* @property {1} version
|
|
39
|
+
* @property {string} branchId
|
|
40
|
+
* @property {string} query
|
|
41
|
+
* @property {readonly TalosResearchSource[]} sources
|
|
42
|
+
* @property {readonly { url: string, reason: string }[]} unreachable
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {TalosResearchCollection} collection
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function talosResearchDossierDocument(collection) {
|
|
50
|
+
/** @type {TalosResearchDossierRecord} */
|
|
51
|
+
const record = {
|
|
52
|
+
version: 1,
|
|
53
|
+
branchId: collection.branchId,
|
|
54
|
+
query: collection.query,
|
|
55
|
+
sources: collection.sources,
|
|
56
|
+
unreachable: collection.unreachable,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const prose = [
|
|
60
|
+
`# ${collection.query}`,
|
|
61
|
+
...collection.sources.map((source) => [
|
|
62
|
+
`## ${source.title}`,
|
|
63
|
+
source.url,
|
|
64
|
+
source.publishedAt ? `data dichiarata: ${source.publishedAt}` : 'data non dichiarata',
|
|
65
|
+
source.obtained === 'snippet' ? '(solo estratto dal motore di ricerca)' : '',
|
|
66
|
+
'',
|
|
67
|
+
source.text,
|
|
68
|
+
].filter(Boolean).join('\n')),
|
|
69
|
+
...(collection.unreachable.length > 0
|
|
70
|
+
? ['## Non raggiungibili', ...collection.unreachable.map((entry) => `${entry.url} — ${entry.reason}`)]
|
|
71
|
+
: []),
|
|
72
|
+
].join('\n\n')
|
|
73
|
+
|
|
74
|
+
// Il recinto va per ULTIMO, così un lettore che si ferma prima — un'anteprima,
|
|
75
|
+
// un estratto nei risultati di ricerca — vede la prosa e non un muro di JSON.
|
|
76
|
+
return `${prose}\n\n${FENCE_OPEN}\n${JSON.stringify(record)}\n${FENCE_CLOSE}\n`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rilegge un dossier, o ammette di non poterlo fare.
|
|
81
|
+
*
|
|
82
|
+
* Torna `null` invece di un tentativo. Chi chiama sta per costruire un rapporto
|
|
83
|
+
* le cui citazioni verranno controllate contro questi passaggi: un dossier
|
|
84
|
+
* recuperato a metà produrrebbe affermazioni verificate contro un testo che non
|
|
85
|
+
* è quello che è stato letto.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} document
|
|
88
|
+
* @returns {TalosResearchCollection | null}
|
|
89
|
+
*/
|
|
90
|
+
export function talosResearchParseDossier(document) {
|
|
91
|
+
const start = document.indexOf(FENCE_OPEN)
|
|
92
|
+
if (start < 0) return null
|
|
93
|
+
const from = start + FENCE_OPEN.length
|
|
94
|
+
const end = document.indexOf(FENCE_CLOSE, from)
|
|
95
|
+
if (end < 0) return null
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
/** @type {TalosResearchDossierRecord} */
|
|
99
|
+
const parsed = JSON.parse(document.slice(from, end))
|
|
100
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.sources)) return null
|
|
101
|
+
return {
|
|
102
|
+
branchId: String(parsed.branchId ?? ''),
|
|
103
|
+
query: String(parsed.query ?? ''),
|
|
104
|
+
sources: parsed.sources,
|
|
105
|
+
unreachable: Array.isArray(parsed.unreachable) ? parsed.unreachable : [],
|
|
106
|
+
// La spesa non viaggia qui: sta già nel giornale, che è l'unico
|
|
107
|
+
// posto in cui si può contare. Una seconda copia prima o poi
|
|
108
|
+
// verrebbe sommata alla prima.
|
|
109
|
+
spend: { searches: 0, pages: 0, tokens: 0 },
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|