agent-nuvira 3.3.13 → 3.3.14
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/README.md +67 -1
- package/dist/agents/agents/planner.d.ts.map +1 -1
- package/dist/agents/agents/planner.js +9 -0
- package/dist/agents/agents/planner.js.map +1 -1
- package/dist/agents/orchestrator.d.ts +31 -0
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +74 -126
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/cli/chat.d.ts +89 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +765 -33
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/cli-program.d.ts.map +1 -1
- package/dist/cli/cli-program.js +12 -0
- package/dist/cli/cli-program.js.map +1 -1
- package/dist/cli/config.d.ts +12 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +86 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/decisions.d.ts +28 -0
- package/dist/cli/decisions.d.ts.map +1 -0
- package/dist/cli/decisions.js +140 -0
- package/dist/cli/decisions.js.map +1 -0
- package/dist/cli/doctor.d.ts +6 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +46 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/failover-runner.d.ts.map +1 -1
- package/dist/cli/failover-runner.js +19 -1
- package/dist/cli/failover-runner.js.map +1 -1
- package/dist/cli/feedback.d.ts.map +1 -1
- package/dist/cli/feedback.js +5 -1
- package/dist/cli/feedback.js.map +1 -1
- package/dist/cli/hooks.d.ts +30 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +224 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/knowledge.d.ts +9 -1
- package/dist/cli/knowledge.d.ts.map +1 -1
- package/dist/cli/knowledge.js +93 -2
- package/dist/cli/knowledge.js.map +1 -1
- package/dist/cli/loop-executor.d.ts.map +1 -1
- package/dist/cli/loop-executor.js +47 -12
- package/dist/cli/loop-executor.js.map +1 -1
- package/dist/cli/model.d.ts +20 -0
- package/dist/cli/model.d.ts.map +1 -1
- package/dist/cli/model.js +294 -14
- package/dist/cli/model.js.map +1 -1
- package/dist/cli/rate.d.ts +78 -0
- package/dist/cli/rate.d.ts.map +1 -0
- package/dist/cli/rate.js +294 -0
- package/dist/cli/rate.js.map +1 -0
- package/dist/config/process-env.d.ts.map +1 -1
- package/dist/config/process-env.js +10 -0
- package/dist/config/process-env.js.map +1 -1
- package/dist/enterprise/vault.d.ts.map +1 -1
- package/dist/enterprise/vault.js +5 -21
- package/dist/enterprise/vault.js.map +1 -1
- package/dist/gateway/hook-contract.d.ts +33 -0
- package/dist/gateway/hook-contract.d.ts.map +1 -1
- package/dist/gateway/hook-contract.js +97 -0
- package/dist/gateway/hook-contract.js.map +1 -1
- package/dist/inference/gemini-adapter.d.ts.map +1 -1
- package/dist/inference/gemini-adapter.js +4 -0
- package/dist/inference/gemini-adapter.js.map +1 -1
- package/dist/inference/interface.d.ts +37 -0
- package/dist/inference/interface.d.ts.map +1 -1
- package/dist/inference/native-tools.d.ts +16 -1
- package/dist/inference/native-tools.d.ts.map +1 -1
- package/dist/inference/native-tools.js +34 -10
- package/dist/inference/native-tools.js.map +1 -1
- package/dist/inference/route-resolver.d.ts +30 -0
- package/dist/inference/route-resolver.d.ts.map +1 -1
- package/dist/inference/route-resolver.js +158 -1
- package/dist/inference/route-resolver.js.map +1 -1
- package/dist/inference/tools.d.ts +16 -1
- package/dist/inference/tools.d.ts.map +1 -1
- package/dist/inference/tools.js +51 -18
- package/dist/inference/tools.js.map +1 -1
- package/dist/learning/acceptance-model.d.ts +247 -0
- package/dist/learning/acceptance-model.d.ts.map +1 -0
- package/dist/learning/acceptance-model.js +708 -0
- package/dist/learning/acceptance-model.js.map +1 -0
- package/dist/learning/auto-router.d.ts +68 -24
- package/dist/learning/auto-router.d.ts.map +1 -1
- package/dist/learning/auto-router.js +301 -96
- package/dist/learning/auto-router.js.map +1 -1
- package/dist/learning/autonomy-policy.d.ts +12 -16
- package/dist/learning/autonomy-policy.d.ts.map +1 -1
- package/dist/learning/autonomy-policy.js +50 -48
- package/dist/learning/autonomy-policy.js.map +1 -1
- package/dist/learning/behavioural-labels.d.ts +86 -0
- package/dist/learning/behavioural-labels.d.ts.map +1 -0
- package/dist/learning/behavioural-labels.js +166 -0
- package/dist/learning/behavioural-labels.js.map +1 -0
- package/dist/learning/capability-evidence.d.ts +167 -0
- package/dist/learning/capability-evidence.d.ts.map +1 -0
- package/dist/learning/capability-evidence.js +210 -0
- package/dist/learning/capability-evidence.js.map +1 -0
- package/dist/learning/capability-readiness.d.ts +64 -0
- package/dist/learning/capability-readiness.d.ts.map +1 -0
- package/dist/learning/capability-readiness.js +74 -0
- package/dist/learning/capability-readiness.js.map +1 -0
- package/dist/learning/capability-types.d.ts +169 -0
- package/dist/learning/capability-types.d.ts.map +1 -0
- package/dist/learning/capability-types.js +20 -0
- package/dist/learning/capability-types.js.map +1 -0
- package/dist/learning/catalog-feed.d.ts +89 -0
- package/dist/learning/catalog-feed.d.ts.map +1 -0
- package/dist/learning/catalog-feed.js +211 -0
- package/dist/learning/catalog-feed.js.map +1 -0
- package/dist/learning/cli-intent-effects.d.ts +60 -0
- package/dist/learning/cli-intent-effects.d.ts.map +1 -0
- package/dist/learning/cli-intent-effects.js +153 -0
- package/dist/learning/cli-intent-effects.js.map +1 -0
- package/dist/learning/command-adaptation.d.ts +41 -0
- package/dist/learning/command-adaptation.d.ts.map +1 -0
- package/dist/learning/command-adaptation.js +122 -0
- package/dist/learning/command-adaptation.js.map +1 -0
- package/dist/learning/consent-picture.d.ts +42 -0
- package/dist/learning/consent-picture.d.ts.map +1 -0
- package/dist/learning/consent-picture.js +53 -0
- package/dist/learning/consent-picture.js.map +1 -0
- package/dist/learning/context-budget.d.ts +37 -6
- package/dist/learning/context-budget.d.ts.map +1 -1
- package/dist/learning/context-budget.js +38 -9
- package/dist/learning/context-budget.js.map +1 -1
- package/dist/learning/decision-log.d.ts +114 -0
- package/dist/learning/decision-log.d.ts.map +1 -0
- package/dist/learning/decision-log.js +296 -0
- package/dist/learning/decision-log.js.map +1 -0
- package/dist/learning/deliverable-class.d.ts.map +1 -1
- package/dist/learning/deliverable-class.js +19 -0
- package/dist/learning/deliverable-class.js.map +1 -1
- package/dist/learning/deliverable-corpus.d.ts +117 -0
- package/dist/learning/deliverable-corpus.d.ts.map +1 -0
- package/dist/learning/deliverable-corpus.js +176 -0
- package/dist/learning/deliverable-corpus.js.map +1 -0
- package/dist/learning/error-repair.d.ts +22 -1
- package/dist/learning/error-repair.d.ts.map +1 -1
- package/dist/learning/error-repair.js +37 -0
- package/dist/learning/error-repair.js.map +1 -1
- package/dist/learning/exhaustion-report.d.ts +136 -0
- package/dist/learning/exhaustion-report.d.ts.map +1 -0
- package/dist/learning/exhaustion-report.js +239 -0
- package/dist/learning/exhaustion-report.js.map +1 -0
- package/dist/learning/failure-bookkeeping.d.ts +16 -0
- package/dist/learning/failure-bookkeeping.d.ts.map +1 -1
- package/dist/learning/failure-bookkeeping.js +51 -5
- package/dist/learning/failure-bookkeeping.js.map +1 -1
- package/dist/learning/gate-friction.d.ts +39 -0
- package/dist/learning/gate-friction.d.ts.map +1 -0
- package/dist/learning/gate-friction.js +91 -0
- package/dist/learning/gate-friction.js.map +1 -0
- package/dist/learning/harness-narration.d.ts +34 -0
- package/dist/learning/harness-narration.d.ts.map +1 -0
- package/dist/learning/harness-narration.js +58 -0
- package/dist/learning/harness-narration.js.map +1 -0
- package/dist/learning/hub-skill-catalog.d.ts.map +1 -1
- package/dist/learning/hub-skill-catalog.js +1 -2
- package/dist/learning/hub-skill-catalog.js.map +1 -1
- package/dist/learning/hybrid-router.d.ts +65 -0
- package/dist/learning/hybrid-router.d.ts.map +1 -1
- package/dist/learning/hybrid-router.js +186 -0
- package/dist/learning/hybrid-router.js.map +1 -1
- package/dist/learning/knowledge-base.d.ts +229 -3
- package/dist/learning/knowledge-base.d.ts.map +1 -1
- package/dist/learning/knowledge-base.js +593 -24
- package/dist/learning/knowledge-base.js.map +1 -1
- package/dist/learning/knowledge-eval.d.ts +171 -0
- package/dist/learning/knowledge-eval.d.ts.map +1 -0
- package/dist/learning/knowledge-eval.js +196 -0
- package/dist/learning/knowledge-eval.js.map +1 -0
- package/dist/learning/knowledge-turn.d.ts +92 -0
- package/dist/learning/knowledge-turn.d.ts.map +1 -0
- package/dist/learning/knowledge-turn.js +190 -0
- package/dist/learning/knowledge-turn.js.map +1 -0
- package/dist/learning/learned-commands.d.ts +64 -0
- package/dist/learning/learned-commands.d.ts.map +1 -0
- package/dist/learning/learned-commands.js +108 -0
- package/dist/learning/learned-commands.js.map +1 -0
- package/dist/learning/lexical-search.d.ts +50 -0
- package/dist/learning/lexical-search.d.ts.map +1 -0
- package/dist/learning/lexical-search.js +111 -0
- package/dist/learning/lexical-search.js.map +1 -0
- package/dist/learning/machine-facts.d.ts +71 -0
- package/dist/learning/machine-facts.d.ts.map +1 -0
- package/dist/learning/machine-facts.js +164 -0
- package/dist/learning/machine-facts.js.map +1 -0
- package/dist/learning/markdown-outline.d.ts +73 -0
- package/dist/learning/markdown-outline.d.ts.map +1 -0
- package/dist/learning/markdown-outline.js +136 -0
- package/dist/learning/markdown-outline.js.map +1 -0
- package/dist/learning/model-first-router.d.ts +33 -1
- package/dist/learning/model-first-router.d.ts.map +1 -1
- package/dist/learning/model-first-router.js +123 -9
- package/dist/learning/model-first-router.js.map +1 -1
- package/dist/learning/model-identity.d.ts +80 -0
- package/dist/learning/model-identity.d.ts.map +1 -0
- package/dist/learning/model-identity.js +115 -0
- package/dist/learning/model-identity.js.map +1 -0
- package/dist/learning/model-registry.d.ts +141 -2
- package/dist/learning/model-registry.d.ts.map +1 -1
- package/dist/learning/model-registry.js +324 -38
- package/dist/learning/model-registry.js.map +1 -1
- package/dist/learning/model-scoring.d.ts.map +1 -1
- package/dist/learning/model-scoring.js +8 -1
- package/dist/learning/model-scoring.js.map +1 -1
- package/dist/learning/model-selection.d.ts.map +1 -1
- package/dist/learning/model-selection.js +41 -0
- package/dist/learning/model-selection.js.map +1 -1
- package/dist/learning/outcome-observation.d.ts +49 -0
- package/dist/learning/outcome-observation.d.ts.map +1 -0
- package/dist/learning/outcome-observation.js +95 -0
- package/dist/learning/outcome-observation.js.map +1 -0
- package/dist/learning/pair-entitlement.d.ts +184 -0
- package/dist/learning/pair-entitlement.d.ts.map +1 -0
- package/dist/learning/pair-entitlement.js +214 -0
- package/dist/learning/pair-entitlement.js.map +1 -0
- package/dist/learning/provider-fallback.d.ts +23 -1
- package/dist/learning/provider-fallback.d.ts.map +1 -1
- package/dist/learning/provider-fallback.js +24 -0
- package/dist/learning/provider-fallback.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +65 -1
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +123 -1
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/learning/requirement-probe.d.ts +55 -0
- package/dist/learning/requirement-probe.d.ts.map +1 -0
- package/dist/learning/requirement-probe.js +77 -0
- package/dist/learning/requirement-probe.js.map +1 -0
- package/dist/learning/resilient-call.d.ts +20 -1
- package/dist/learning/resilient-call.d.ts.map +1 -1
- package/dist/learning/resilient-call.js +42 -4
- package/dist/learning/resilient-call.js.map +1 -1
- package/dist/learning/response-usability.d.ts +85 -0
- package/dist/learning/response-usability.d.ts.map +1 -0
- package/dist/learning/response-usability.js +135 -0
- package/dist/learning/response-usability.js.map +1 -0
- package/dist/learning/router-bandit.d.ts +132 -0
- package/dist/learning/router-bandit.d.ts.map +1 -1
- package/dist/learning/router-bandit.js +253 -0
- package/dist/learning/router-bandit.js.map +1 -1
- package/dist/learning/routing-history.d.ts +41 -2
- package/dist/learning/routing-history.d.ts.map +1 -1
- package/dist/learning/routing-history.js.map +1 -1
- package/dist/learning/run-trace.d.ts +21 -0
- package/dist/learning/run-trace.d.ts.map +1 -1
- package/dist/learning/run-trace.js +38 -0
- package/dist/learning/run-trace.js.map +1 -1
- package/dist/learning/session-grant.d.ts +75 -0
- package/dist/learning/session-grant.d.ts.map +1 -0
- package/dist/learning/session-grant.js +112 -0
- package/dist/learning/session-grant.js.map +1 -0
- package/dist/learning/task-model-continuity.d.ts +73 -0
- package/dist/learning/task-model-continuity.d.ts.map +1 -0
- package/dist/learning/task-model-continuity.js +87 -0
- package/dist/learning/task-model-continuity.js.map +1 -0
- package/dist/learning/turn-feedback.d.ts +93 -0
- package/dist/learning/turn-feedback.d.ts.map +1 -0
- package/dist/learning/turn-feedback.js +127 -0
- package/dist/learning/turn-feedback.js.map +1 -0
- package/dist/learning/turn-report.d.ts +104 -2
- package/dist/learning/turn-report.d.ts.map +1 -1
- package/dist/learning/turn-report.js +161 -6
- package/dist/learning/turn-report.js.map +1 -1
- package/dist/learning/verdict-routing.d.ts +51 -0
- package/dist/learning/verdict-routing.d.ts.map +1 -0
- package/dist/learning/verdict-routing.js +122 -0
- package/dist/learning/verdict-routing.js.map +1 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +5 -0
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/manager.d.ts +10 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +20 -0
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/mcp-discovery.d.ts +34 -0
- package/dist/mcp/mcp-discovery.d.ts.map +1 -0
- package/dist/mcp/mcp-discovery.js +71 -0
- package/dist/mcp/mcp-discovery.js.map +1 -0
- package/dist/mcp/mcp-schema-cache.d.ts +17 -0
- package/dist/mcp/mcp-schema-cache.d.ts.map +1 -1
- package/dist/mcp/mcp-schema-cache.js +47 -3
- package/dist/mcp/mcp-schema-cache.js.map +1 -1
- package/dist/mcp/types.d.ts +28 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/mcp/types.js.map +1 -1
- package/dist/tools/ask-user.d.ts.map +1 -1
- package/dist/tools/ask-user.js +4 -0
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/capability-registry.d.ts +150 -0
- package/dist/tools/capability-registry.d.ts.map +1 -0
- package/dist/tools/capability-registry.js +400 -0
- package/dist/tools/capability-registry.js.map +1 -0
- package/dist/tools/child-agent-runtime.d.ts.map +1 -1
- package/dist/tools/child-agent-runtime.js +19 -5
- package/dist/tools/child-agent-runtime.js.map +1 -1
- package/dist/tools/coding-tools.d.ts +17 -5
- package/dist/tools/coding-tools.d.ts.map +1 -1
- package/dist/tools/coding-tools.js +80 -11
- package/dist/tools/coding-tools.js.map +1 -1
- package/dist/tools/command-memo.d.ts +116 -0
- package/dist/tools/command-memo.d.ts.map +1 -0
- package/dist/tools/command-memo.js +202 -0
- package/dist/tools/command-memo.js.map +1 -0
- package/dist/tools/git-tool.d.ts.map +1 -1
- package/dist/tools/git-tool.js +13 -3
- package/dist/tools/git-tool.js.map +1 -1
- package/dist/tools/harness-directive.d.ts +42 -0
- package/dist/tools/harness-directive.d.ts.map +1 -0
- package/dist/tools/harness-directive.js +45 -0
- package/dist/tools/harness-directive.js.map +1 -0
- package/dist/tools/loop-skill-hint.d.ts +74 -174
- package/dist/tools/loop-skill-hint.d.ts.map +1 -1
- package/dist/tools/loop-skill-hint.js +110 -536
- package/dist/tools/loop-skill-hint.js.map +1 -1
- package/dist/tools/mcp-client-tool.d.ts +2 -0
- package/dist/tools/mcp-client-tool.d.ts.map +1 -1
- package/dist/tools/mcp-client-tool.js +3 -0
- package/dist/tools/mcp-client-tool.js.map +1 -1
- package/dist/tools/modality/shared.d.ts +1 -4
- package/dist/tools/modality/shared.d.ts.map +1 -1
- package/dist/tools/modality/shared.js +7 -22
- package/dist/tools/modality/shared.js.map +1 -1
- package/dist/tools/plan-store.d.ts +20 -2
- package/dist/tools/plan-store.d.ts.map +1 -1
- package/dist/tools/plan-store.js +51 -7
- package/dist/tools/plan-store.js.map +1 -1
- package/dist/tools/registry.d.ts +76 -3
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +486 -26
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/remediation-ladder.d.ts.map +1 -1
- package/dist/tools/remediation-ladder.js +8 -0
- package/dist/tools/remediation-ladder.js.map +1 -1
- package/dist/tools/run-cli.d.ts.map +1 -1
- package/dist/tools/run-cli.js +29 -4
- package/dist/tools/run-cli.js.map +1 -1
- package/dist/tools/run-terminal.d.ts +11 -0
- package/dist/tools/run-terminal.d.ts.map +1 -1
- package/dist/tools/run-terminal.js +265 -10
- package/dist/tools/run-terminal.js.map +1 -1
- package/dist/tools/tool-loop.d.ts +321 -13
- package/dist/tools/tool-loop.d.ts.map +1 -1
- package/dist/tools/tool-loop.js +867 -66
- package/dist/tools/tool-loop.js.map +1 -1
- package/dist/utils/atomic-store.d.ts +53 -0
- package/dist/utils/atomic-store.d.ts.map +1 -0
- package/dist/utils/atomic-store.js +178 -0
- package/dist/utils/atomic-store.js.map +1 -0
- package/dist/utils/binary-probe.d.ts +25 -0
- package/dist/utils/binary-probe.d.ts.map +1 -0
- package/dist/utils/binary-probe.js +48 -0
- package/dist/utils/binary-probe.js.map +1 -0
- package/dist/utils/workspace-path.d.ts +50 -0
- package/dist/utils/workspace-path.d.ts.map +1 -0
- package/dist/utils/workspace-path.js +86 -0
- package/dist/utils/workspace-path.js.map +1 -0
- package/dist/web-dashboard/chat-console.d.ts +24 -0
- package/dist/web-dashboard/chat-console.d.ts.map +1 -1
- package/dist/web-dashboard/chat-console.js +38 -0
- package/dist/web-dashboard/chat-console.js.map +1 -1
- package/dist/web-dashboard/project-context.d.ts +27 -2
- package/dist/web-dashboard/project-context.d.ts.map +1 -1
- package/dist/web-dashboard/project-context.js +32 -2
- package/dist/web-dashboard/project-context.js.map +1 -1
- package/dist/web-dashboard/server.d.ts +11 -0
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +362 -11
- package/dist/web-dashboard/server.js.map +1 -1
- package/dist/web-dashboard/src/types.d.ts +170 -1
- package/dist/web-dashboard/src/types.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.d.ts +11 -2
- package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.js +24 -12
- package/dist/web-dashboard/workspace-guard.js.map +1 -1
- package/dist/web-dashboard/workspace-resolution.d.ts +87 -0
- package/dist/web-dashboard/workspace-resolution.d.ts.map +1 -0
- package/dist/web-dashboard/workspace-resolution.js +105 -0
- package/dist/web-dashboard/workspace-resolution.js.map +1 -0
- package/package.json +1 -1
- package/src/web-dashboard/public/assets/index-zjbdJnO-.js +210 -0
- package/src/web-dashboard/public/assets/index-zjbdJnO-.js.map +1 -0
- package/src/web-dashboard/public/index.html +1 -1
- package/src/web-dashboard/public/assets/index-DhiuR4S2.js +0 -210
- package/src/web-dashboard/public/assets/index-DhiuR4S2.js.map +0 -1
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance model — fit `P(accepted | features)` to the labels the harness now
|
|
3
|
+
* collects, and report it READ-ONLY.
|
|
4
|
+
*
|
|
5
|
+
* THE POINT OF THE WHOLE `userVerdict` WORK. Bundles 27–30 made it possible for a
|
|
6
|
+
* turn to carry a LABEL: the user accepted it, rejected it, or said nothing. This
|
|
7
|
+
* module is the first consumer — it fits a probability of acceptance to the
|
|
8
|
+
* harness's OWN features (did the turn verify, was it flagged, did it deliver),
|
|
9
|
+
* which is the thing a "quality score" was always supposed to be and could never
|
|
10
|
+
* be built from derived signals alone.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IT REFUSES TO DO.
|
|
13
|
+
* - It never routes on an UNMEASURED or too-thin sample. `cachedAcceptanceFit`
|
|
14
|
+
* returns the same named refusal below the floor as `trainAcceptanceModel`, and
|
|
15
|
+
* the one routing consumer (`outcome-observation.ts` → the bandit's
|
|
16
|
+
* `qualityScore`) omits the field entirely unless the fit is `ok`. So a router
|
|
17
|
+
* cannot be moved by a probability fitted to three rows — the exact defect the
|
|
18
|
+
* programme removes. (NOTE: this file previously READ-ONLY; the user's decision
|
|
19
|
+
* of 2026-10-08 wired the fitted probability into the bandit's measured
|
|
20
|
+
* `qualityScore`, which is why the router now reads it. Below the floor, nothing
|
|
21
|
+
* changes.)
|
|
22
|
+
* - It never fits below the sample floor. With too few labelled turns, or with
|
|
23
|
+
* only one class, it returns a NAMED reason and no model — an underfit
|
|
24
|
+
* probability printed as if it were evidence is worse than printing nothing.
|
|
25
|
+
* - It never invents an acceptance. Only rows with a real verdict are used;
|
|
26
|
+
* `verdict: null` rows are excluded, because silence is not a positive.
|
|
27
|
+
*
|
|
28
|
+
* THE FEATURES are the ones the harness already records per turn, each of them
|
|
29
|
+
* evidence the run produced rather than the model's account of itself:
|
|
30
|
+
* verified — the turn's own check passed (`verified` / `delivered-and-read-back`)
|
|
31
|
+
* unverified — the turn ran but nothing verified it
|
|
32
|
+
* flag — an honesty flag fired (a defect the turn disclosed)
|
|
33
|
+
* delivered — the turn produced an authored artifact
|
|
34
|
+
*
|
|
35
|
+
* Training is deterministic (zero initial weights, fixed iterations), so the same
|
|
36
|
+
* corpus always yields the same coefficients and a report cannot drift run to run.
|
|
37
|
+
*/
|
|
38
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
import { listTraces, recordTraceVerdict, getTrace } from './reasoning-trace.js';
|
|
41
|
+
import { readDeliverableCandidates, labelDeliverableByTrace } from './deliverable-corpus.js';
|
|
42
|
+
import { envBuff, resolveNuviraHome } from '../config/paths.js';
|
|
43
|
+
/** The features a fit sees. Binary by construction — measured flags, not magnitudes. */
|
|
44
|
+
export const ACCEPTANCE_FEATURES = ['verified', 'unverified', 'flag', 'delivered'];
|
|
45
|
+
/** Below this many labelled turns a fit is noise. */
|
|
46
|
+
export const MIN_LABELS_FOR_FIT = 20;
|
|
47
|
+
/** …and below this many of EITHER class it can only learn the base rate. */
|
|
48
|
+
export const MIN_PER_CLASS = 5;
|
|
49
|
+
const LEARNING_RATE = 0.3;
|
|
50
|
+
const ITERATIONS = 500;
|
|
51
|
+
const L2 = 0.01;
|
|
52
|
+
const emptyFeatures = () => ({ verified: 0, unverified: 0, flag: 0, delivered: 0 });
|
|
53
|
+
function anyFlag(report) {
|
|
54
|
+
const f = report?.flags;
|
|
55
|
+
if (!f)
|
|
56
|
+
return false;
|
|
57
|
+
return Boolean(f.unverifiedActionClaim ||
|
|
58
|
+
f.unverifiedEdit ||
|
|
59
|
+
f.unverifiedEditClaim ||
|
|
60
|
+
f.unverifiedBuildClaim ||
|
|
61
|
+
f.undeliveredArtifact ||
|
|
62
|
+
f.unfulfilledPromise ||
|
|
63
|
+
f.noActionTaken ||
|
|
64
|
+
f.incompleteArtifactClaim ||
|
|
65
|
+
f.unverifiedFileClaim ||
|
|
66
|
+
f.artifactShortfall);
|
|
67
|
+
}
|
|
68
|
+
/** Imported labels kept on disk, oldest drop first — a dataset, not an unbounded log. */
|
|
69
|
+
export const MAX_IMPORTED_LABELS = 5000;
|
|
70
|
+
function memoryDir() {
|
|
71
|
+
return envBuff('MEMORY_DIR') || join(resolveNuviraHome(), 'memory');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Where an imported corpus's labels live. Kept separate from the traces because an
|
|
75
|
+
* imported row has no local trace to attach a `userVerdict` to — a fit simply READS
|
|
76
|
+
* these alongside the local labels, so a corpus collected elsewhere joins without
|
|
77
|
+
* the harness inventing a trace or rewriting one it does not own.
|
|
78
|
+
*/
|
|
79
|
+
export function importedLabelsPath() {
|
|
80
|
+
return join(memoryDir(), 'acceptance-labels.jsonl');
|
|
81
|
+
}
|
|
82
|
+
function isTruthy(v) {
|
|
83
|
+
return v === true || v === 1 || v === '1' || v === 'true';
|
|
84
|
+
}
|
|
85
|
+
/** One row from a shipped corpus → a `LabelledTurn`, or null when it is malformed. */
|
|
86
|
+
function coerceTurn(raw) {
|
|
87
|
+
if (!raw || typeof raw !== 'object')
|
|
88
|
+
return null;
|
|
89
|
+
const r = raw;
|
|
90
|
+
if (!('accepted' in r) || !('at' in r))
|
|
91
|
+
return null;
|
|
92
|
+
const at = Number(r.at);
|
|
93
|
+
if (!Number.isFinite(at))
|
|
94
|
+
return null;
|
|
95
|
+
const features = emptyFeatures();
|
|
96
|
+
const featSrc = r.features && typeof r.features === 'object'
|
|
97
|
+
? r.features
|
|
98
|
+
: r;
|
|
99
|
+
for (const f of ACCEPTANCE_FEATURES)
|
|
100
|
+
features[f] = isTruthy(featSrc[f]) ? 1 : 0;
|
|
101
|
+
return {
|
|
102
|
+
...(typeof r.traceId === 'string' && r.traceId ? { traceId: r.traceId } : {}),
|
|
103
|
+
...(typeof r.provider === 'string' && r.provider ? { provider: r.provider } : {}),
|
|
104
|
+
...(typeof r.model === 'string' && r.model ? { model: r.model } : {}),
|
|
105
|
+
at,
|
|
106
|
+
accepted: isTruthy(r.accepted),
|
|
107
|
+
source: typeof r.source === 'string' && r.source ? r.source : 'imported',
|
|
108
|
+
features,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Quote-aware split of one CSV line (RFC-4180: doubled quotes, embedded commas). */
|
|
112
|
+
function splitCsvLine(line) {
|
|
113
|
+
const out = [];
|
|
114
|
+
let cur = '';
|
|
115
|
+
let inQuotes = false;
|
|
116
|
+
for (let i = 0; i < line.length; i++) {
|
|
117
|
+
const ch = line[i];
|
|
118
|
+
if (inQuotes) {
|
|
119
|
+
if (ch === '"') {
|
|
120
|
+
if (line[i + 1] === '"') {
|
|
121
|
+
cur += '"';
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
else
|
|
125
|
+
inQuotes = false;
|
|
126
|
+
}
|
|
127
|
+
else
|
|
128
|
+
cur += ch;
|
|
129
|
+
}
|
|
130
|
+
else if (ch === '"')
|
|
131
|
+
inQuotes = true;
|
|
132
|
+
else if (ch === ',') {
|
|
133
|
+
out.push(cur);
|
|
134
|
+
cur = '';
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
cur += ch;
|
|
138
|
+
}
|
|
139
|
+
out.push(cur);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
function parseCorpusCsv(text) {
|
|
143
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
144
|
+
if (lines.length < 2)
|
|
145
|
+
return [];
|
|
146
|
+
const header = splitCsvLine(lines[0]).map((h) => h.trim());
|
|
147
|
+
const out = [];
|
|
148
|
+
for (let i = 1; i < lines.length; i++) {
|
|
149
|
+
const cells = splitCsvLine(lines[i]);
|
|
150
|
+
const obj = {};
|
|
151
|
+
header.forEach((h, j) => { obj[h] = cells[j]; });
|
|
152
|
+
const t = coerceTurn(obj);
|
|
153
|
+
if (t)
|
|
154
|
+
out.push(t);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function parseCorpusJson(text) {
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(text);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const rows = Array.isArray(parsed)
|
|
167
|
+
? parsed
|
|
168
|
+
: parsed && typeof parsed === 'object' && Array.isArray(parsed.turns)
|
|
169
|
+
? parsed.turns
|
|
170
|
+
: [];
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const raw of rows) {
|
|
173
|
+
const t = coerceTurn(raw);
|
|
174
|
+
if (t)
|
|
175
|
+
out.push(t);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/** Best-effort format detection: by extension, else by the first non-space character. */
|
|
180
|
+
export function detectCorpusFormat(path, text) {
|
|
181
|
+
if (path && path.toLowerCase().endsWith('.csv'))
|
|
182
|
+
return 'csv';
|
|
183
|
+
if (path && path.toLowerCase().endsWith('.json'))
|
|
184
|
+
return 'json';
|
|
185
|
+
const first = text.trimStart()[0];
|
|
186
|
+
return first === '{' || first === '[' ? 'json' : 'csv';
|
|
187
|
+
}
|
|
188
|
+
/** Parse a shipped corpus in either format into labelled turns (malformed rows dropped). */
|
|
189
|
+
export function parseCorpusText(text, format) {
|
|
190
|
+
return format === 'csv' ? parseCorpusCsv(text) : parseCorpusJson(text);
|
|
191
|
+
}
|
|
192
|
+
/** Read the imported labels store (best-effort; a corrupt line is skipped). */
|
|
193
|
+
export function readImportedLabels() {
|
|
194
|
+
try {
|
|
195
|
+
if (!existsSync(importedLabelsPath()))
|
|
196
|
+
return [];
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const line of readFileSync(importedLabelsPath(), 'utf-8').split('\n')) {
|
|
199
|
+
const trimmed = line.trim();
|
|
200
|
+
if (!trimmed)
|
|
201
|
+
continue;
|
|
202
|
+
try {
|
|
203
|
+
const row = coerceTurn(JSON.parse(trimmed));
|
|
204
|
+
if (row)
|
|
205
|
+
out.push(row);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// A corrupt line is skipped — a dataset store must not be a landmine.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Dedupe key for an imported row: its trace when it has one, else its identity. */
|
|
218
|
+
function labelKey(t) {
|
|
219
|
+
return t.traceId ?? `p:${t.provider ?? ''}|m:${t.model ?? ''}|at:${t.at}|a:${t.accepted ? 1 : 0}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Merge an imported corpus into the local store, deduped by trace (or identity).
|
|
223
|
+
* Re-importing the same file is idempotent: rows already present are updated, not
|
|
224
|
+
* duplicated. Returns the counts a caller reports, and never throws.
|
|
225
|
+
*/
|
|
226
|
+
export function mergeImportedLabels(incoming) {
|
|
227
|
+
const byKey = new Map();
|
|
228
|
+
for (const t of readImportedLabels())
|
|
229
|
+
byKey.set(labelKey(t), t);
|
|
230
|
+
let added = 0;
|
|
231
|
+
let updated = 0;
|
|
232
|
+
for (const t of incoming) {
|
|
233
|
+
const k = labelKey(t);
|
|
234
|
+
if (byKey.has(k))
|
|
235
|
+
updated++;
|
|
236
|
+
else
|
|
237
|
+
added++;
|
|
238
|
+
byKey.set(k, t);
|
|
239
|
+
}
|
|
240
|
+
const merged = [...byKey.values()].sort((a, b) => a.at - b.at).slice(-MAX_IMPORTED_LABELS);
|
|
241
|
+
try {
|
|
242
|
+
if (!existsSync(memoryDir()))
|
|
243
|
+
mkdirSync(memoryDir(), { recursive: true });
|
|
244
|
+
writeFileSync(importedLabelsPath(), merged.map((r) => JSON.stringify(r)).join('\n') + '\n', 'utf-8');
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Best-effort — an import must never break the caller.
|
|
248
|
+
}
|
|
249
|
+
return { added, updated, total: merged.length };
|
|
250
|
+
}
|
|
251
|
+
/** Names of the honesty flags a report set, for an audit view. */
|
|
252
|
+
export function flagNames(report) {
|
|
253
|
+
const f = report?.flags;
|
|
254
|
+
if (!f)
|
|
255
|
+
return [];
|
|
256
|
+
return Object.entries(f)
|
|
257
|
+
.filter(([, v]) => Boolean(v))
|
|
258
|
+
.map(([k]) => k);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The features a fit sees for ONE trace — the SAME derivation `collectLocalTurns`
|
|
262
|
+
* uses, extracted so an audit (`nuvira rate --explain`) cannot disagree with the fit.
|
|
263
|
+
*/
|
|
264
|
+
export function featuresFromTrace(trace, delivered) {
|
|
265
|
+
const features = emptyFeatures();
|
|
266
|
+
const verification = trace.turnReport?.verification;
|
|
267
|
+
if (verification === 'verified' || verification === 'delivered-and-read-back')
|
|
268
|
+
features.verified = 1;
|
|
269
|
+
else if (verification === 'unverified')
|
|
270
|
+
features.unverified = 1;
|
|
271
|
+
if (anyFlag(trace.turnReport))
|
|
272
|
+
features.flag = 1;
|
|
273
|
+
if (delivered)
|
|
274
|
+
features.delivered = 1;
|
|
275
|
+
return features;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Features for a LIVE turn, derived from its `TurnReport` the SAME way
|
|
279
|
+
* {@link featuresFromTrace} derives them for a stored one — so the score a turn
|
|
280
|
+
* gets while it runs cannot disagree with the rows the fit trained on.
|
|
281
|
+
*/
|
|
282
|
+
export function featuresFromReport(report) {
|
|
283
|
+
const features = emptyFeatures();
|
|
284
|
+
const verification = report?.verification;
|
|
285
|
+
if (verification === 'verified' || verification === 'delivered-and-read-back')
|
|
286
|
+
features.verified = 1;
|
|
287
|
+
else if (verification === 'unverified')
|
|
288
|
+
features.unverified = 1;
|
|
289
|
+
if (anyFlag(report))
|
|
290
|
+
features.flag = 1;
|
|
291
|
+
if (verification === 'delivered-and-read-back' || (report?.changedPaths?.length ?? 0) > 0)
|
|
292
|
+
features.delivered = 1;
|
|
293
|
+
return features;
|
|
294
|
+
}
|
|
295
|
+
/** How long a fitted model is reused before the corpus is re-read. */
|
|
296
|
+
export const FIT_CACHE_TTL_MS = 60_000;
|
|
297
|
+
let _fitCache = null;
|
|
298
|
+
/**
|
|
299
|
+
* A MEMOIZED {@link trainAcceptanceModel}.
|
|
300
|
+
*
|
|
301
|
+
* WHY IT MUST BE CACHED: this is read on the live turn-outcome path, and fitting
|
|
302
|
+
* re-reads up to 1000 traces plus the corpus and runs 500 iterations. Doing that
|
|
303
|
+
* once per turn would put a synchronous fs read on every answer. The corpus only
|
|
304
|
+
* grows when a turn is rated, so a short TTL costs at most one refit per minute and
|
|
305
|
+
* cannot hide a label for longer than that.
|
|
306
|
+
*/
|
|
307
|
+
export function cachedAcceptanceFit(now = Date.now()) {
|
|
308
|
+
if (_fitCache && now - _fitCache.at < FIT_CACHE_TTL_MS)
|
|
309
|
+
return _fitCache.fit;
|
|
310
|
+
const fit = trainAcceptanceModel(now);
|
|
311
|
+
_fitCache = { at: now, fit };
|
|
312
|
+
return fit;
|
|
313
|
+
}
|
|
314
|
+
/** Drop the memoized fit, so the next read refits (a CLI refresh, and tests). */
|
|
315
|
+
export function invalidateAcceptanceFit() {
|
|
316
|
+
_fitCache = null;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The LOCAL labels: the trace verdicts (the reliable source, carrying the
|
|
320
|
+
* features) plus any corpus rows whose verdict has no trace counterpart. Excludes
|
|
321
|
+
* imported rows — a conflict merge must compare against what THIS machine recorded.
|
|
322
|
+
*/
|
|
323
|
+
function collectLocalTurns() {
|
|
324
|
+
const out = [];
|
|
325
|
+
const seen = new Set();
|
|
326
|
+
let traces = [];
|
|
327
|
+
try {
|
|
328
|
+
traces = listTraces(1000);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
traces = [];
|
|
332
|
+
}
|
|
333
|
+
const corpus = (() => {
|
|
334
|
+
try {
|
|
335
|
+
return readDeliverableCandidates();
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
})();
|
|
341
|
+
for (const t of traces) {
|
|
342
|
+
const v = t.userVerdict;
|
|
343
|
+
if (!v)
|
|
344
|
+
continue;
|
|
345
|
+
const verification = t.turnReport?.verification;
|
|
346
|
+
const row = corpus.find((r) => r.traceId === t.id);
|
|
347
|
+
const features = featuresFromTrace(t, Boolean(row) || verification === 'delivered-and-read-back');
|
|
348
|
+
out.push({
|
|
349
|
+
traceId: t.id,
|
|
350
|
+
...(t.provider ? { provider: t.provider } : {}),
|
|
351
|
+
...(t.model ? { model: t.model } : {}),
|
|
352
|
+
at: v.at,
|
|
353
|
+
accepted: v.verdict === 'accepted',
|
|
354
|
+
source: v.source,
|
|
355
|
+
features,
|
|
356
|
+
});
|
|
357
|
+
seen.add(t.id);
|
|
358
|
+
}
|
|
359
|
+
for (const row of corpus) {
|
|
360
|
+
if (row.verdict === null)
|
|
361
|
+
continue;
|
|
362
|
+
if (row.traceId && seen.has(row.traceId))
|
|
363
|
+
continue;
|
|
364
|
+
const features = emptyFeatures();
|
|
365
|
+
if (row.verification === 'verified' || row.verification === 'delivered-and-read-back')
|
|
366
|
+
features.verified = 1;
|
|
367
|
+
else if (row.verification === 'unverified')
|
|
368
|
+
features.unverified = 1;
|
|
369
|
+
features.delivered = 1;
|
|
370
|
+
out.push({
|
|
371
|
+
...(row.traceId ? { traceId: row.traceId } : {}),
|
|
372
|
+
at: row.verdictAt ?? row.ts,
|
|
373
|
+
accepted: row.verdict === 'accepted',
|
|
374
|
+
source: 'derived',
|
|
375
|
+
features,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return out.sort((a, b) => a.at - b.at);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Collect every LABELLED turn the harness has: the local labels plus the rows
|
|
382
|
+
* imported from another corpus. A LOCAL label for the same trace wins — the local
|
|
383
|
+
* record carries the features we measured ourselves.
|
|
384
|
+
*/
|
|
385
|
+
export function collectLabelledTurns() {
|
|
386
|
+
const out = collectLocalTurns();
|
|
387
|
+
const seen = new Set(out.map((t) => t.traceId).filter((id) => Boolean(id)));
|
|
388
|
+
for (const imp of readImportedLabels()) {
|
|
389
|
+
if (imp.traceId && seen.has(imp.traceId))
|
|
390
|
+
continue;
|
|
391
|
+
out.push(imp);
|
|
392
|
+
if (imp.traceId)
|
|
393
|
+
seen.add(imp.traceId);
|
|
394
|
+
}
|
|
395
|
+
return out.sort((a, b) => a.at - b.at);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Explain ONE trace: its features, its label and — when the fit has trained — the
|
|
399
|
+
* probability the model assigns to it. The features come from the SAME
|
|
400
|
+
* `featuresFromTrace` the fit uses, so an audit cannot disagree with the model.
|
|
401
|
+
*/
|
|
402
|
+
export function explainTraceLabel(traceId, now = Date.now()) {
|
|
403
|
+
let trace;
|
|
404
|
+
try {
|
|
405
|
+
trace = getTrace(traceId);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
trace = null;
|
|
409
|
+
}
|
|
410
|
+
if (!trace)
|
|
411
|
+
return null;
|
|
412
|
+
const row = (() => {
|
|
413
|
+
try {
|
|
414
|
+
return readDeliverableCandidates().find((r) => r.traceId === traceId);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
})();
|
|
420
|
+
const verification = trace.turnReport?.verification;
|
|
421
|
+
const delivered = Boolean(row) || verification === 'delivered-and-read-back';
|
|
422
|
+
const features = featuresFromTrace(trace, delivered);
|
|
423
|
+
const fit = trainAcceptanceModel(now);
|
|
424
|
+
return {
|
|
425
|
+
traceId,
|
|
426
|
+
goal: trace.goal,
|
|
427
|
+
...(trace.provider ? { provider: trace.provider } : {}),
|
|
428
|
+
...(trace.model ? { model: trace.model } : {}),
|
|
429
|
+
...(verification ? { verification } : {}),
|
|
430
|
+
flags: flagNames(trace.turnReport),
|
|
431
|
+
delivered,
|
|
432
|
+
features,
|
|
433
|
+
label: trace.userVerdict ? trace.userVerdict.verdict === 'accepted' : null,
|
|
434
|
+
source: trace.userVerdict?.source ?? null,
|
|
435
|
+
probability: fit.ok ? predictAcceptance(fit.model, features) : null,
|
|
436
|
+
fitReady: fit.ok,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Overwrite one LOCAL label with an imported verdict (corpus row and trace). Used
|
|
441
|
+
* only by an EXPLICIT `--replace` — the default keeps the local label, because a
|
|
442
|
+
* label this machine recorded is one a person or the behaviour here produced.
|
|
443
|
+
*/
|
|
444
|
+
export function replaceLocalLabel(traceId, accepted, source, now = Date.now()) {
|
|
445
|
+
const verdict = accepted ? 'accepted' : 'rejected';
|
|
446
|
+
const src = source === 'cli' || source === 'dashboard' || source === 'derived' ? source : 'derived';
|
|
447
|
+
let did = false;
|
|
448
|
+
try {
|
|
449
|
+
did = recordTraceVerdict(traceId, verdict, src, now) || did;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// best-effort
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
did = labelDeliverableByTrace(traceId, verdict, now) || did;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// best-effort
|
|
459
|
+
}
|
|
460
|
+
return did;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Reconcile an incoming corpus against the LOCAL labels, explicitly.
|
|
464
|
+
*
|
|
465
|
+
* A conflict is a trace the local record already labels differently. The default
|
|
466
|
+
* KEEPS the local label (it is this machine's own measurement) and reports the
|
|
467
|
+
* count; `replace: true` lets the incoming label win. Rows with no local label are
|
|
468
|
+
* simply stored. Deduped and idempotent like {@link mergeImportedLabels}.
|
|
469
|
+
*/
|
|
470
|
+
export function mergeLabelledCorpus(incoming, opts = {}) {
|
|
471
|
+
const now = opts.now ?? Date.now();
|
|
472
|
+
const localByTrace = new Map();
|
|
473
|
+
for (const t of collectLocalTurns()) {
|
|
474
|
+
if (t.traceId)
|
|
475
|
+
localByTrace.set(t.traceId, t.accepted);
|
|
476
|
+
}
|
|
477
|
+
const toStore = [];
|
|
478
|
+
let conflicts = 0;
|
|
479
|
+
let replaced = 0;
|
|
480
|
+
let keptLocal = 0;
|
|
481
|
+
let matching = 0;
|
|
482
|
+
for (const t of incoming) {
|
|
483
|
+
if (!t.traceId || !localByTrace.has(t.traceId)) {
|
|
484
|
+
toStore.push(t);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (localByTrace.get(t.traceId) === t.accepted) {
|
|
488
|
+
matching++;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
conflicts++;
|
|
492
|
+
if (opts.replace) {
|
|
493
|
+
replaceLocalLabel(t.traceId, t.accepted, t.source, now);
|
|
494
|
+
replaced++;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
keptLocal++;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const merged = mergeImportedLabels(toStore);
|
|
501
|
+
return {
|
|
502
|
+
incoming: incoming.length,
|
|
503
|
+
stored: toStore.length,
|
|
504
|
+
conflicts,
|
|
505
|
+
replaced,
|
|
506
|
+
keptLocal,
|
|
507
|
+
matching,
|
|
508
|
+
total: merged.total,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Fit the logistic model. Deterministic; returns a NAMED reason rather than a
|
|
513
|
+
* model whenever the sample is too thin to mean anything. Trains on an arbitrary
|
|
514
|
+
* set of rows, so the offline fitter can reuse the identical maths on an EXPORTED
|
|
515
|
+
* corpus — a shipped corpus and this machine cannot disagree.
|
|
516
|
+
*/
|
|
517
|
+
export function fitLabelledTurns(rows, now = Date.now()) {
|
|
518
|
+
const positives = rows.filter((r) => r.accepted).length;
|
|
519
|
+
const negatives = rows.length - positives;
|
|
520
|
+
if (rows.length < MIN_LABELS_FOR_FIT) {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
reason: `only ${rows.length} labelled turn(s) — need ${MIN_LABELS_FOR_FIT}`,
|
|
524
|
+
n: rows.length,
|
|
525
|
+
positives,
|
|
526
|
+
negatives,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (positives < MIN_PER_CLASS || negatives < MIN_PER_CLASS) {
|
|
530
|
+
return {
|
|
531
|
+
ok: false,
|
|
532
|
+
reason: `needs ≥${MIN_PER_CLASS} of each class (have ${positives}👍/${negatives}👎)`,
|
|
533
|
+
n: rows.length,
|
|
534
|
+
positives,
|
|
535
|
+
negatives,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const X = rows.map((r) => ACCEPTANCE_FEATURES.map((f) => r.features[f]));
|
|
539
|
+
const y = rows.map((r) => (r.accepted ? 1 : 0));
|
|
540
|
+
const dim = ACCEPTANCE_FEATURES.length;
|
|
541
|
+
const weights = new Array(dim).fill(0);
|
|
542
|
+
let bias = 0;
|
|
543
|
+
for (let iter = 0; iter < ITERATIONS; iter++) {
|
|
544
|
+
const gradW = new Array(dim).fill(0);
|
|
545
|
+
let gradB = 0;
|
|
546
|
+
for (let i = 0; i < X.length; i++) {
|
|
547
|
+
const z = bias + X[i].reduce((s, xi, j) => s + xi * weights[j], 0);
|
|
548
|
+
const p = 1 / (1 + Math.exp(-z));
|
|
549
|
+
const err = p - y[i];
|
|
550
|
+
for (let j = 0; j < dim; j++)
|
|
551
|
+
gradW[j] += err * X[i][j];
|
|
552
|
+
gradB += err;
|
|
553
|
+
}
|
|
554
|
+
for (let j = 0; j < dim; j++) {
|
|
555
|
+
weights[j] -= LEARNING_RATE * (gradW[j] / X.length + L2 * weights[j]);
|
|
556
|
+
}
|
|
557
|
+
bias -= LEARNING_RATE * (gradB / X.length);
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
model: {
|
|
562
|
+
weights,
|
|
563
|
+
bias,
|
|
564
|
+
featureNames: [...ACCEPTANCE_FEATURES],
|
|
565
|
+
n: rows.length,
|
|
566
|
+
positives,
|
|
567
|
+
negatives,
|
|
568
|
+
trainedAt: now,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/** Fit to the LIVE corpus the harness has collected (local traces + imports). */
|
|
573
|
+
export function trainAcceptanceModel(now = Date.now()) {
|
|
574
|
+
return fitLabelledTurns(collectLabelledTurns(), now);
|
|
575
|
+
}
|
|
576
|
+
/** `P(accepted | features)` under a fitted model. */
|
|
577
|
+
export function predictAcceptance(model, features) {
|
|
578
|
+
const z = model.bias + model.featureNames.reduce((s, f, j) => s + (features[f] ?? 0) * model.weights[j], 0);
|
|
579
|
+
return 1 / (1 + Math.exp(-z));
|
|
580
|
+
}
|
|
581
|
+
/** The whole acceptance picture in one call — the ONE source the surfaces render. */
|
|
582
|
+
export function acceptanceSummary(now = Date.now()) {
|
|
583
|
+
const turns = collectLabelledTurns();
|
|
584
|
+
const bySource = {};
|
|
585
|
+
for (const t of turns)
|
|
586
|
+
bySource[t.source] = (bySource[t.source] ?? 0) + 1;
|
|
587
|
+
const accepted = turns.filter((t) => t.accepted).length;
|
|
588
|
+
return {
|
|
589
|
+
labelled: turns.length,
|
|
590
|
+
accepted,
|
|
591
|
+
rejected: turns.length - accepted,
|
|
592
|
+
bySource,
|
|
593
|
+
byPair: acceptanceByPair(),
|
|
594
|
+
fit: trainAcceptanceModel(now),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Render a summary as human lines — the SINGLE renderer every surface uses.
|
|
599
|
+
*
|
|
600
|
+
* `nuvira rate --stats`, `model explain` and any future surface call THIS, so they
|
|
601
|
+
* cannot describe the same corpus two different ways. `focusPair` (optional) lists
|
|
602
|
+
* that `provider/model` first, which is how `model explain` keeps the decision's own
|
|
603
|
+
* pair at the top without a second formatter.
|
|
604
|
+
*/
|
|
605
|
+
export function formatAcceptanceSummary(s, focusPair) {
|
|
606
|
+
const lines = [];
|
|
607
|
+
lines.push(`labelled turns: ${s.labelled} (👍 ${s.accepted} / 👎 ${s.rejected})`);
|
|
608
|
+
const sources = Object.entries(s.bySource);
|
|
609
|
+
lines.push(sources.length > 0
|
|
610
|
+
? `by source: ${sources.map(([k, v]) => `${k} ${v}`).join(', ')}`
|
|
611
|
+
: 'by source: (none yet)');
|
|
612
|
+
const pairLine = (key, v) => {
|
|
613
|
+
const n = v.accepted + v.rejected;
|
|
614
|
+
return `${key}: 👍 ${v.accepted} / 👎 ${v.rejected} (${Math.round((100 * v.accepted) / n)}%, n=${n})`;
|
|
615
|
+
};
|
|
616
|
+
const pairs = Object.entries(s.byPair);
|
|
617
|
+
if (pairs.length === 0 && !focusPair) {
|
|
618
|
+
lines.push('by pair: (no rated turns yet)');
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
lines.push('by pair:');
|
|
622
|
+
if (focusPair) {
|
|
623
|
+
const v = s.byPair[focusPair];
|
|
624
|
+
lines.push(v ? ` ${pairLine(focusPair, v)} ←` : ` ${focusPair}: n/a (no rated turns yet) ←`);
|
|
625
|
+
}
|
|
626
|
+
for (const [key, v] of pairs) {
|
|
627
|
+
if (key === focusPair)
|
|
628
|
+
continue;
|
|
629
|
+
lines.push(` ${pairLine(key, v)}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (s.fit.ok) {
|
|
633
|
+
lines.push(`fit: TRAINED — P(accepted | features) n=${s.fit.model.n} (${s.fit.model.positives}👍/${s.fit.model.negatives}👎)`);
|
|
634
|
+
for (let j = 0; j < s.fit.model.featureNames.length; j++) {
|
|
635
|
+
const w = s.fit.model.weights[j];
|
|
636
|
+
lines.push(` ${s.fit.model.featureNames[j].padEnd(10)} ${w >= 0 ? '+' : ''}${w.toFixed(2)}`);
|
|
637
|
+
}
|
|
638
|
+
lines.push(` ${'bias'.padEnd(10)} ${s.fit.model.bias >= 0 ? '+' : ''}${s.fit.model.bias.toFixed(2)}`);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
lines.push(`fit: NOT trained — ${s.fit.reason}`);
|
|
642
|
+
lines.push(` (needs ${MIN_LABELS_FOR_FIT} labelled turns with ≥${MIN_PER_CLASS} of each class)`);
|
|
643
|
+
}
|
|
644
|
+
lines.push(s.fit.ok
|
|
645
|
+
? 'this fit feeds the router bandit\'s qualityScore on live turns (never below the sample floor)'
|
|
646
|
+
: 'nothing routes on this until the fit clears the sample floor above');
|
|
647
|
+
return lines;
|
|
648
|
+
}
|
|
649
|
+
/** CSV column order (stable, so an offline reader can rely on it). */
|
|
650
|
+
export const CORPUS_CSV_COLUMNS = [
|
|
651
|
+
'traceId',
|
|
652
|
+
'provider',
|
|
653
|
+
'model',
|
|
654
|
+
'at',
|
|
655
|
+
'accepted',
|
|
656
|
+
'source',
|
|
657
|
+
...ACCEPTANCE_FEATURES,
|
|
658
|
+
];
|
|
659
|
+
function csvCell(v) {
|
|
660
|
+
const s = v === undefined || v === null ? '' : String(v);
|
|
661
|
+
// Quote only when needed, so a plain id stays readable and RFC-4180 holds.
|
|
662
|
+
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Serialize the labelled corpus for shipping and offline fitting — the SAME rows
|
|
666
|
+
* a fit here would read, so nothing is lost in the hand-off.
|
|
667
|
+
*
|
|
668
|
+
* JSON carries the full structure (features nested, plus provenance); CSV is the
|
|
669
|
+
* flat form for a spreadsheet, with one column per feature and `accepted` as 0/1.
|
|
670
|
+
*/
|
|
671
|
+
export function serializeLabelledTurns(rows, format, now = Date.now()) {
|
|
672
|
+
if (format === 'csv') {
|
|
673
|
+
const header = CORPUS_CSV_COLUMNS.join(',');
|
|
674
|
+
const lines = rows.map((r) => CORPUS_CSV_COLUMNS.map((c) => {
|
|
675
|
+
if (c === 'accepted')
|
|
676
|
+
return r.accepted ? '1' : '0';
|
|
677
|
+
if (c === 'traceId')
|
|
678
|
+
return csvCell(r.traceId);
|
|
679
|
+
if (c === 'provider')
|
|
680
|
+
return csvCell(r.provider);
|
|
681
|
+
if (c === 'model')
|
|
682
|
+
return csvCell(r.model);
|
|
683
|
+
if (c === 'at')
|
|
684
|
+
return csvCell(r.at);
|
|
685
|
+
if (c === 'source')
|
|
686
|
+
return csvCell(r.source);
|
|
687
|
+
return csvCell(r.features[c]);
|
|
688
|
+
}).join(','));
|
|
689
|
+
return `${header}\n${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`;
|
|
690
|
+
}
|
|
691
|
+
return `${JSON.stringify({ version: 1, exportedAt: now, count: rows.length, turns: rows }, null, 2)}\n`;
|
|
692
|
+
}
|
|
693
|
+
/** Rated turns per `provider/model`, for the per-pair acceptance line. */
|
|
694
|
+
export function acceptanceByPair() {
|
|
695
|
+
const out = {};
|
|
696
|
+
for (const t of collectLabelledTurns()) {
|
|
697
|
+
if (!t.provider)
|
|
698
|
+
continue;
|
|
699
|
+
const key = `${t.provider}/${t.model ?? ''}`;
|
|
700
|
+
out[key] = out[key] ?? { accepted: 0, rejected: 0 };
|
|
701
|
+
if (t.accepted)
|
|
702
|
+
out[key].accepted++;
|
|
703
|
+
else
|
|
704
|
+
out[key].rejected++;
|
|
705
|
+
}
|
|
706
|
+
return out;
|
|
707
|
+
}
|
|
708
|
+
//# sourceMappingURL=acceptance-model.js.map
|