@mutagent/helix 0.1.0-alpha.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/.claude/skills/mutagent-helix/SKILL.md +113 -0
- package/.claude/skills/mutagent-helix/scripts/cli/codex-transcode.ts +143 -0
- package/.claude/skills/mutagent-helix/scripts/cli/doctor.ts +111 -0
- package/.claude/skills/mutagent-helix/scripts/cli/init.ts +382 -0
- package/.claude/skills/mutagent-helix/scripts/cli/run.sh +61 -0
- package/CLAUDE.md +193 -0
- package/LICENSE +201 -0
- package/NOTICE +22 -0
- package/README.md +209 -0
- package/assets/adl-loop.svg +162 -0
- package/assets/system-status.svg +422 -0
- package/bin/mutagent-cli.mjs +9950 -0
- package/bin/mutagent-helix.mjs +78 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/CLAUDE.md +28 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/SKILL.md +138 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/assets/templates/agentspec.yaml.tpl +247 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/references/frameworks/doc-pins.md +53 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/references/principles.md +91 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/references/requirements.yaml +295 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/references/workflows/orchestrator-protocol.md +302 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/scripts/cli/doctor.ts +116 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/scripts/cli/init.ts +230 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/scripts/cli/run.sh +60 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/scripts/config/resolve-spec-dir.ts +199 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/scripts/contract/agentspec.schema.ts +455 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/scripts/setup/detect.ts +67 -0
- package/mutagent-agentspec/.claude/skills/mutagent-agentspec/scripts/validate/validate-spec.ts +93 -0
- package/mutagent-agentspec/LICENSE +201 -0
- package/mutagent-agentspec/NOTICE +19 -0
- package/mutagent-agentspec/README.md +44 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/SKILL.md +100 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/assets/agents/ai-architect.md +312 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/assets/agents/ai-engineer.md +357 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/assets/templates/build-report.md.tpl +39 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/references/principles.md +31 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/references/workflows/build-protocol.md +61 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/references/workflows/improve-handoff.md +23 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/scripts/cli/run.sh +60 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/scripts/handoff/validate-build-input.ts +72 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/scripts/sync-spec/check-sync-spec.ts +499 -0
- package/mutagent-builder/.claude/skills/mutagent-builder/scripts/verify/spec-impl-coverage.ts +182 -0
- package/mutagent-builder/CLAUDE.md +25 -0
- package/mutagent-builder/README.md +26 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/README.md +192 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/SKILL.md +352 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/agents/ai-architect.md +256 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/agents/ai-engineer.md +344 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/agents/diagnostics-analyzer.md +576 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/agents/discovery.md +425 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/audit.json.tpl +24 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/audit.md.tpl +49 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/config.yaml.tpl +100 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/pr-body.md.tpl +56 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/report.html.tpl +327 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/shared/README.md +27 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/shared/iter-N-handover.md.tpl +125 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/shared/self-diagnosis-contract.v0.1.0.yaml.tpl +234 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/shared/spec.yaml.tpl +152 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/shared/team.yaml.tpl +127 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/templates/shared/wave-N-dashboard.html.tpl +1712 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/diagnostics/WF-2.1-nl-query.md +59 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/diagnostics/WF-2.2-tier0-progress.md +58 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/diagnostics/WF-2.3-analysis-progress.md +44 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/diagnostics/WF-2.4-html-report.md +61 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/diagnostics/WF-2.5-rca-detail.md +69 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/diagnostics/WF-2.6-stale-warning.md +53 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.1-welcome.md +60 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.2-source-platform-picker.md +41 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.3-source-auth.md +75 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.4-target-picker.md +48 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.5-ask-tool.md +43 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.6-config-review.md +57 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.7-filter-defaults.md +52 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/assets/wireframes/onboarding/WF-1.8-first-run.md +59 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/examples/sample-findings.json +92 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/examples/sample-meta.json +47 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/adapter-strategy.md +75 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/config-migration.md +84 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/config.md +159 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/filter-search-matrix.md +66 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/harness-knowledge.md +22 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/memory-format.md +78 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/onboarding-decisions.yaml +187 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/operation-inventory.md +59 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/overview.md +167 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/reference.md +119 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/script-index.md +101 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-adapter-contract.md +93 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/claude-code-transcripts.md +66 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/codex-transcripts.md +78 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/codex.md +145 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/install-paths.md +57 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/langfuse.md +178 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/local-jsonl.md +55 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/openobserve.md +102 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/source-platforms/otel.md +63 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/target-platforms/cloud-rest.md +84 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/target-platforms/local-claude.md +65 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/target-platforms/local-cloud-agent-sdk.md +92 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/target-platforms/local-codex.md +72 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/target-platforms/local-cursor.md +108 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/target-platforms/local-mastra.md +77 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/target-platforms/local-opencode.md +70 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/apply-pr-comment-format.md +82 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/diagnostics.md +35 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/handover-contract.md +247 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/onboarding.md +300 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/orchestrator-protocol.md +1861 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/rca.md +331 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/rendering-anatomy.md +314 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/schedule-prep.md +100 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/scope-model.md +128 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/references/workflows/verification-methodology.md +174 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/aggregate/sort-findings.ts +0 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/awareness/blind-spots.ts +167 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/awareness/llm-sample.ts +681 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/cli/doctor.ts +215 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/cli/init.ts +1013 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/cli/install-agents.ts +350 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/cli/run.sh +61 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/cli/shared-templates.ts +79 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/config/load.ts +392 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/config/resolve-credential.ts +120 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/config/schema.ts +641 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/config/validate.ts +134 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/context/build-diagnosis-context.ts +417 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/context/load-context.ts +105 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/contract/types.ts +166 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/enrich/build-render-input.ts +1964 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/enrich/corpus-severity.ts +172 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/enrich/dismissal-match.ts +270 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/enrich/rank-remedies.ts +75 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/hooks/install-hooks.sh +46 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/hooks/pre-push.sh +78 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/invocation/parse-brief.ts +188 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/library/match.ts +114 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/library/paths.ts +95 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/library/store.ts +535 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/library/types.ts +181 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/lint/template-inline-js.ts +267 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/memory/append.ts +188 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/memory/read.ts +218 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/normalize/platforms/entity-context.ts +686 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/normalize/read-unitf.ts +267 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/normalize/trace.ts +1078 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/normalize/unitf-adapter.ts +0 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/normalize/unitf-types.ts +213 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/report/persist-selections.ts +174 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/report/render.ts +3254 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/run/diagnose.ts +64 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/run/session.ts +167 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/sample/caps.ts +345 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/sample/deep-read-gate.ts +424 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/sample/representative.ts +584 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/scan/objection.ts +224 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/scan/trajectory.ts +489 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/self-diagnostics/dispatch.ts +135 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/self-diagnostics/probe.ts +170 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/setup/detect.ts +307 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/setup/ensure-cli.ts +315 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/setup/reconfigure.ts +77 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/setup/verify-agents.ts +207 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/slicer.ts +166 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/adapters/claude-code.ts +17 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/adapters/codex.ts +11 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/adapters/langfuse.ts +17 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/adapters/local-jsonl.ts +12 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/adapters/otel.ts +12 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/index.ts +54 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/registry.ts +45 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/source/types.ts +86 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/stale-detector.ts +74 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/test/render-playwright.cli.ts +477 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/tier0/claude-code.ts +133 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/tier0/langfuse.ts +143 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/tier0-scan.ts +423 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/completeness-check.ts +436 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/doc-refs.ts +190 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/finalize-gate.ts +417 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/findings-contract.ts +628 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/render-js-syntax.ts +188 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/report-checklist.yaml +217 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/trace-shape.ts +262 -0
- package/mutagent-diagnostics/.claude/skills/mutagent-diagnostics/scripts/validate/wave6-checklist.ts +326 -0
- package/mutagent-diagnostics/CLAUDE.md +41 -0
- package/mutagent-diagnostics/LICENSE +201 -0
- package/mutagent-diagnostics/NOTICE +18 -0
- package/mutagent-diagnostics/README.md +343 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/SKILL.md +548 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/assets/agents/audit-executor.md +169 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +160 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/assets/agents/discovery.md +425 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +1108 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/assets/brand/theme.css +213 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/assets/brand/wordmark.html +9 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/lenses/context-flow-lens.md +85 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/lenses/data-lens.md +47 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/lenses/decision-lens.md +48 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +53 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/lenses/trajectory-lens.md +44 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/build-review-interface.md +83 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/edd-loop.md +134 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/error-analysis.md +113 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/eval-audit.md +154 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/eval-stage.md +168 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/generate-synthetic-data.md +81 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/grounded-adjudication.md +221 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/memory-format.md +65 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/methodology.md +201 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/operation-inventory.md +197 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/validate-evaluator.md +125 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/workflows/orchestrator-protocol.md +300 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/references/write-judge-prompt.md +123 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/behavior-tree.schema.yaml +73 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/dataset.schema.yaml +66 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/edd-change-request.schema.yaml +114 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/eval-matrix.schema.yaml +74 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/flow-graph.schema.yaml +69 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/flow-profile.schema.yaml +49 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +40 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/schemas/scorecard.schema.yaml +85 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +543 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/artifact-paths.ts +115 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/assemble-scorecard.ts +172 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/build-dataset.ts +186 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/build-evals.ts +93 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +393 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/check-method-router.ts +170 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/aggregate.ts +112 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +182 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/doctor.ts +211 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/dogfood.ts +133 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/init.ts +601 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/methodology-review.ts +122 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +295 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +165 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/run.sh +56 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cli/variance-check.ts +105 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/code-eval.ts +248 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +285 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/codegen-evals.ts +173 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cold-start-project.ts +151 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/cold-start-sampler.ts +267 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/config/load.ts +343 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +380 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +85 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/dataset.ts +149 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/eval-engine.ts +118 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +577 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +1074 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/flow-graph.ts +194 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/types.ts +303 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/contracts/validation.ts +193 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/derive-dataset.ts +152 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +219 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/diff-discriminate.ts +160 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/discover-criteria.ts +348 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +232 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +210 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/edd/variance-gate.ts +186 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/emit-completeness.ts +111 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/eval-engine.ts +160 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/evaluate.ts +333 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/flow-graph.ts +230 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +253 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/judge-provider.ts +135 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/lint-grounding.ts +229 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/lint-uniformity.ts +168 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/living-suite.ts +109 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/load-bundle.ts +203 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/load-profile-vocab.ts +64 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/load-profile.ts +106 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/mask.ts +138 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +113 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +750 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +215 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/memory/read.ts +168 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +125 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/profile-subject.ts +310 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/publish-report.ts +131 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +81 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +195 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/render-discover-report.ts +1640 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +3838 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/render-report.ts +212 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/resolve-credential.ts +110 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/resolve-ref.ts +98 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/result-verify.ts +129 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +330 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +367 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +719 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/run-judge.ts +155 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +175 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/sample-traces.ts +210 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/self-audit.ts +387 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/source-map.ts +106 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +134 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/substrate.ts +162 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/ui-slots.ts +119 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +284 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/validate-judge.ts +358 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/scripts/variance-compare.ts +177 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/behavior-tree.yaml +140 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/eval-matrix.yaml +1270 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/methodology-review.yaml +105 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/workflows/audit.workflow.js +82 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/workflows/data-leak.workflow.js +236 -0
- package/mutagent-evaluator/.claude/skills/mutagent-evaluator/workflows/variance.workflow.js +163 -0
- package/mutagent-evaluator/CLAUDE.md +45 -0
- package/mutagent-evaluator/LICENSE +201 -0
- package/mutagent-evaluator/NOTICE +23 -0
- package/mutagent-evaluator/README.md +88 -0
- package/mutagent-optimize/.claude/skills/mutagent-optimize/SKILL.md +287 -0
- package/mutagent-optimize/.claude/skills/mutagent-optimize/scripts/contracts/amend-request.ts +503 -0
- package/mutagent-optimize/.claude/skills/mutagent-optimize/scripts/improve-loop-run.ts +304 -0
- package/mutagent-optimize/.claude/skills/mutagent-optimize/scripts/loop-state-cli.ts +300 -0
- package/mutagent-optimize/.claude/skills/mutagent-optimize/scripts/loop-state.ts +440 -0
- package/mutagent-optimize/CLAUDE.md +49 -0
- package/mutagent-optimize/README.md +26 -0
- package/mutagent-orchestrator/CLAUDE.md +102 -0
- package/mutagent-orchestrator/README.md +20 -0
- package/mutagent-orchestrator/assets/agents/discovery.md +425 -0
- package/mutagent-orchestrator/assets/agents/dogfood-monitor.md +266 -0
- package/mutagent-orchestrator/assets/agents/monitor.md +258 -0
- package/mutagent-orchestrator/orchestrator.md +782 -0
- package/mutagent-orchestrator/references/MONITORING.md +209 -0
- package/mutagent-orchestrator/references/config-fields.md +132 -0
- package/mutagent-orchestrator/references/config-migration.md +132 -0
- package/mutagent-orchestrator/references/index-registry.md +92 -0
- package/mutagent-orchestrator/routing.yaml +391 -0
- package/mutagent-orchestrator/scripts/config-schema.ts +1010 -0
- package/mutagent-orchestrator/scripts/dispatch.ts +439 -0
- package/mutagent-orchestrator/scripts/dogfood/extract-feedback.ts +203 -0
- package/mutagent-orchestrator/scripts/dogfood/reconstruct-trajectory.ts +327 -0
- package/mutagent-orchestrator/scripts/dogfood/render-dogfood-report.ts +347 -0
- package/mutagent-orchestrator/scripts/dogfood/resolve-subagents.ts +160 -0
- package/mutagent-orchestrator/scripts/dogfood/run-report.ts +264 -0
- package/mutagent-orchestrator/scripts/dogfood/slack-thread.ts +106 -0
- package/mutagent-orchestrator/scripts/dogfood/types.ts +257 -0
- package/mutagent-orchestrator/scripts/eval-routing.ts +753 -0
- package/mutagent-orchestrator/scripts/gate.ts +219 -0
- package/mutagent-orchestrator/scripts/handover-contract.ts +362 -0
- package/mutagent-orchestrator/scripts/index/build-index.ts +351 -0
- package/mutagent-orchestrator/scripts/index/render-index.ts +110 -0
- package/mutagent-orchestrator/scripts/monitor/slack-notify.ts +111 -0
- package/mutagent-orchestrator/scripts/monitor/triggers.ts +162 -0
- package/mutagent-orchestrator/scripts/onboarding-check.ts +369 -0
- package/mutagent-orchestrator/scripts/render-roster.ts +308 -0
- package/mutagent-orchestrator/scripts/resolve-credential.ts +185 -0
- package/mutagent-orchestrator/scripts/resolve-paths.ts +114 -0
- package/mutagent-orchestrator/scripts/slack/format.ts +63 -0
- package/mutagent-orchestrator/scripts/slack/post.ts +115 -0
- package/mutagent-orchestrator/scripts/sync-index.ts +311 -0
- package/package.json +64 -0
|
@@ -0,0 +1,1964 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/enrich/build-render-input.ts
|
|
3
|
+
* Wave-5 R1.4: deterministic enricher — (tier0-out, slice-plan, findings,
|
|
4
|
+
* trace-metadata) → a fully-populated RenderInput for the gold-standard renderer.
|
|
5
|
+
* Type A — Pure Script (DETERMINISTIC + IDEMPOTENT — no network, no LLM, no random,
|
|
6
|
+
* no clock beyond an injected `generatedAt`).
|
|
7
|
+
*
|
|
8
|
+
* Ported from wave-5-refs/build-render-input.ts (the hand-authored
|
|
9
|
+
* gold-standard body builder). The reference hardcoded its numbers; this enricher
|
|
10
|
+
* DERIVES them from the inputs so the pipeline never hand-authors a report again.
|
|
11
|
+
*
|
|
12
|
+
* Fail-loud (R1 §9.3): if ≥3 of the 4 internal render shapes (diagnosedEntity /
|
|
13
|
+
* bigStat / hourlyHeatmap / signalCensus) cannot be built, THROW — never emit a
|
|
14
|
+
* silently-degraded RenderInput (the regression's root cause).
|
|
15
|
+
*
|
|
16
|
+
* R1.7 (APPENDIX-A): `diagnosedEntity` is PULLED FROM the normalizer's
|
|
17
|
+
* EntityContext (passed in via opts.entityContext), NOT re-synthesized here.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
Finding,
|
|
22
|
+
TraceMetadata,
|
|
23
|
+
EntityContext,
|
|
24
|
+
Assumption,
|
|
25
|
+
} from "../normalize/trace.ts";
|
|
26
|
+
import type {
|
|
27
|
+
RenderInput,
|
|
28
|
+
Entity,
|
|
29
|
+
BigStat,
|
|
30
|
+
SignalCensusRow,
|
|
31
|
+
ScanFunnel,
|
|
32
|
+
HourlyHeatmap,
|
|
33
|
+
HourlyHeatCell,
|
|
34
|
+
DecisionLogRow,
|
|
35
|
+
RunMeta,
|
|
36
|
+
} from "../report/render.ts";
|
|
37
|
+
import { entityFromContext } from "../report/render.ts";
|
|
38
|
+
import { rankRemedies } from "./rank-remedies.ts";
|
|
39
|
+
// ② G-2 over-alarming fix (SL-8 / DS-02): score numeric-anomaly severity against the
|
|
40
|
+
// corpus's OWN distribution (FI-LAT recipe) instead of absolute cutoffs. See
|
|
41
|
+
// .meta/prd.yaml KP-023 (proposed PR-056, operator-locked).
|
|
42
|
+
import {
|
|
43
|
+
computeCorpusBaseline,
|
|
44
|
+
calibrateFromSpans,
|
|
45
|
+
type CorpusBaseline,
|
|
46
|
+
type NameValue,
|
|
47
|
+
} from "./corpus-severity.ts";
|
|
48
|
+
import type {
|
|
49
|
+
TrajectoryCorroboration,
|
|
50
|
+
TrajectorySignal,
|
|
51
|
+
} from "../scan/trajectory.ts";
|
|
52
|
+
import type { DeepReadLedgerEntry, VerdictLedgerEntry } from "../library/types.ts";
|
|
53
|
+
// DISMISSAL FINAL-CHECK: the PURE partition (severity-escalation guard → semantic match).
|
|
54
|
+
// The reasoning that produces `verdicts` runs UPSTREAM (host-runtime, pinned); this module
|
|
55
|
+
// only applies the deterministic split. Keeps the enricher's no-LLM/no-network contract.
|
|
56
|
+
import { partitionByDismissal } from "./dismissal-match.ts";
|
|
57
|
+
import type { VerdictLookup } from "./dismissal-match.ts";
|
|
58
|
+
|
|
59
|
+
// ── Input shapes ──────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** Subset of tier0-scan.ts output the enricher reads. */
|
|
62
|
+
export interface Tier0Input {
|
|
63
|
+
totalTraces: number;
|
|
64
|
+
withError?: number;
|
|
65
|
+
withFeedback?: number;
|
|
66
|
+
withLowScore?: number;
|
|
67
|
+
withHighLatency?: number;
|
|
68
|
+
hasPrimarySignal?: boolean;
|
|
69
|
+
recommendedSlicing?: string;
|
|
70
|
+
estimatedSlots?: number;
|
|
71
|
+
patterns?: Array<{ patternId: string; name: string; matchCount: number }>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* W11-01: PrimarySignal — the reconciled primary signal for a diagnostic run.
|
|
76
|
+
* Emitted by buildSignalCensus and threaded to RunMeta.primarySignal for
|
|
77
|
+
* census badge · heatmap · funnel to read from a single authoritative source.
|
|
78
|
+
*/
|
|
79
|
+
export interface PrimarySignal {
|
|
80
|
+
/** The selected primary signal name (e.g. "latency-spike"). */
|
|
81
|
+
name: string;
|
|
82
|
+
/** One-sentence rationale for selection (impact×prevalence + deep-read corroboration). */
|
|
83
|
+
why: string;
|
|
84
|
+
/** Signals ruled out BEFORE scoring (e.g. benign observability artifacts). */
|
|
85
|
+
ruledOut: string[];
|
|
86
|
+
/** Confidence level based on deep-read corroboration. */
|
|
87
|
+
confidence: "high" | "medium" | "low";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Subset of slicer.ts output the enricher reads. */
|
|
91
|
+
export interface SlicePlanInput {
|
|
92
|
+
totalSlices?: number;
|
|
93
|
+
slices?: Array<{ sliceId?: string; traceIds?: string[] }>;
|
|
94
|
+
rationale?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The findings document (already RenderInput-shaped from the RCA layer): carries
|
|
99
|
+
* sessionId, findings, runMeta, audience, isMetaReport, plus story-led copy.
|
|
100
|
+
*/
|
|
101
|
+
export interface FindingsInput {
|
|
102
|
+
sessionId: string;
|
|
103
|
+
diagnosedAt: string;
|
|
104
|
+
sourcePlatform: string;
|
|
105
|
+
targetPlatform: string;
|
|
106
|
+
totalTraces: number;
|
|
107
|
+
findings: Finding[];
|
|
108
|
+
audience?: "client" | "internal";
|
|
109
|
+
isMetaReport?: boolean;
|
|
110
|
+
runMeta?: RunMeta;
|
|
111
|
+
entities?: Entity[];
|
|
112
|
+
// Story-led overview copy (optional — passed through verbatim).
|
|
113
|
+
headerTitle?: string;
|
|
114
|
+
overviewTitle?: string;
|
|
115
|
+
overviewSub?: string;
|
|
116
|
+
overviewHeadline?: string;
|
|
117
|
+
overviewLeverage?: string;
|
|
118
|
+
decisionsBundle?: string;
|
|
119
|
+
mermaidSequence?: string;
|
|
120
|
+
decisionLog?: DecisionLogRow[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Trace metadata as emitted by the normalizers (+ optional cost). */
|
|
124
|
+
export interface MetadataInput extends TraceMetadata {
|
|
125
|
+
totalCostUsd?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface EnricherInputs {
|
|
129
|
+
tier0: Tier0Input;
|
|
130
|
+
slicePlan: SlicePlanInput;
|
|
131
|
+
findings: FindingsInput;
|
|
132
|
+
metadata: MetadataInput[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface EnricherOptions {
|
|
136
|
+
/** Injected ISO8601 timestamp — keeps the enricher deterministic in tests. */
|
|
137
|
+
generatedAt: string;
|
|
138
|
+
/** R1.7: EntityContext from the normalizer (preferred over findings.entities). */
|
|
139
|
+
entityContext?: EntityContext;
|
|
140
|
+
/**
|
|
141
|
+
* SD self-diag: set when `config.self_diagnostics.enabled`. Forces isMetaReport,
|
|
142
|
+
* audience=internal, a `[INTERNAL]` sessionId prefix, and a skill-typed entity.
|
|
143
|
+
* When supplied, `selfDiag.skillEntity` is the EntityContext built from the
|
|
144
|
+
* skill's own SKILL.md + scripts/ (via buildSkillSelfEntityContext).
|
|
145
|
+
*/
|
|
146
|
+
selfDiag?: {
|
|
147
|
+
enabled: boolean;
|
|
148
|
+
/** Skill-typed EntityContext (entityType:"skill", codeAccess:true). */
|
|
149
|
+
skillEntity?: EntityContext;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* W17-C (SELECTION HUB integration seam): the discovered-signal context threaded into
|
|
153
|
+
* buildSignalCensus — trajectory corroborations (R1 floor), folded ledger digests (R2)
|
|
154
|
+
* + the sampled-trace count (R6 honest prevalence). OPTIONAL: Block B (ledger fold) and
|
|
155
|
+
* Block D1 (trajectory scan) populate it upstream; until then the enricher runs the
|
|
156
|
+
* legacy Tier-0-only census (discovered signals, if any, surface as suspected-unconfirmed
|
|
157
|
+
* — never silently crowned, because no corroboration = no PRIMARY eligibility).
|
|
158
|
+
*/
|
|
159
|
+
signalCtx?: SignalCensusContext;
|
|
160
|
+
/**
|
|
161
|
+
* DISMISSAL FINAL-CHECK (injected). When present, findings that semantically match a
|
|
162
|
+
* previously-dismissed verdict for this entity are REMOVED from renderInput.findings
|
|
163
|
+
* (hidden — there is NO visible "Suppressed" section; the verdict ledger is the audit
|
|
164
|
+
* trail). The reasoning that produced `verdicts` runs UPSTREAM (host-runtime, pinned
|
|
165
|
+
* temp-0 — see enrich/dismissal-match.ts + orchestrator-protocol.md); the enricher only
|
|
166
|
+
* applies the PURE deterministic partition. Absent ⇒ no suppression (fail-safe no-op,
|
|
167
|
+
* behaviour byte-unchanged vs pre-dismissal-loop).
|
|
168
|
+
*/
|
|
169
|
+
dismissalContext?: {
|
|
170
|
+
/** The diagnosed entity's slug — EXACT-match gate for the final-check. */
|
|
171
|
+
entitySlug: string;
|
|
172
|
+
/** The folded VALID dismissed verdicts (fingerprint-invalidation already applied). */
|
|
173
|
+
dismissedEntries: VerdictLedgerEntry[];
|
|
174
|
+
/** Injected reasoning verdicts, keyed by verdictLookupKey(). */
|
|
175
|
+
verdicts: VerdictLookup;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Deterministic numeric helpers ─────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/** Nearest-rank percentile over a numeric array (deterministic). */
|
|
182
|
+
function percentile(values: number[], p: number): number {
|
|
183
|
+
if (values.length === 0) return 0;
|
|
184
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
185
|
+
const rank = Math.max(0, Math.ceil((p / 100) * sorted.length) - 1);
|
|
186
|
+
return sorted[rank];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sum(values: number[]): number {
|
|
190
|
+
return values.reduce((s, v) => s + v, 0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Round to N decimals deterministically. */
|
|
194
|
+
function round(n: number, dp = 0): number {
|
|
195
|
+
const f = 10 ** dp;
|
|
196
|
+
return Math.round(n * f) / f;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Format a millisecond latency as a compact seconds string ("39s"). */
|
|
200
|
+
function ms2s(ms: number): string {
|
|
201
|
+
return `${Math.round(ms / 1000)}s`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Heatmap aggregation (the core R1.4 deterministic computation) ──────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* F3 (UR-2): the heatmap metric MODE, keyed to the primary signal. The cell metric
|
|
208
|
+
* + colour FOLLOWS the primary failure mode rather than being latency-locked.
|
|
209
|
+
*
|
|
210
|
+
* latency-spike → avg latency (absolute bands, the historic default)
|
|
211
|
+
* cost-overshoot → avg $/trace (relative bands)
|
|
212
|
+
* error-WHAT → error rate (relative bands)
|
|
213
|
+
* else → volume (relative bands)
|
|
214
|
+
*
|
|
215
|
+
* Default (no primarySignal / unrecognized) = "latency" so a latency-primary run is
|
|
216
|
+
* byte-identical to the pre-F3 behaviour.
|
|
217
|
+
*/
|
|
218
|
+
type HeatMetricMode = "latency" | "cost" | "error" | "volume";
|
|
219
|
+
|
|
220
|
+
/** WHAT-categories that map to the error-rate heatmap mode. */
|
|
221
|
+
const ERROR_WHATS = new Set([
|
|
222
|
+
"wrong-output",
|
|
223
|
+
"hallucination",
|
|
224
|
+
"missing-output",
|
|
225
|
+
"format-violation",
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* W17-C (R6): discovered WHY-style signals (from the trajectory map — tool-misuse ·
|
|
230
|
+
* handoff-loss · prompt-underspec) have NO direct per-trace numeric metric (unlike
|
|
231
|
+
* latency/cost/error). When one of these is the discovery-only primary, the heatmap
|
|
232
|
+
* falls back to the MECHANICAL metric the enricher can ALWAYS compute from metadata:
|
|
233
|
+
* trace VOLUME. This keeps the heatmap honest (a real per-hour mechanical metric) rather
|
|
234
|
+
* than fabricating a per-hour "tool-misuse rate" the metadata cannot support.
|
|
235
|
+
*/
|
|
236
|
+
const DISCOVERY_ONLY_VOLUME_SIGNALS = new Set([
|
|
237
|
+
"tool-misuse",
|
|
238
|
+
"handoff-loss",
|
|
239
|
+
"prompt-underspec",
|
|
240
|
+
"loop",
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
/** Resolve the heatmap metric mode from a primary-signal name (deterministic). */
|
|
244
|
+
export function resolveHeatMetricMode(primarySignalName?: string): HeatMetricMode {
|
|
245
|
+
if (primarySignalName === "cost-overshoot") return "cost";
|
|
246
|
+
if (primarySignalName && ERROR_WHATS.has(primarySignalName)) return "error";
|
|
247
|
+
if (primarySignalName === "latency-spike" || !primarySignalName) return "latency";
|
|
248
|
+
// W17-C (R6): discovery-only WHY-style primaries have no per-trace numeric metric →
|
|
249
|
+
// mechanical-metric fallback = volume (the metric metadata can always supply).
|
|
250
|
+
if (primarySignalName && DISCOVERY_ONLY_VOLUME_SIGNALS.has(primarySignalName)) return "volume";
|
|
251
|
+
// volume / no-signal / any other admissible signal → volume view.
|
|
252
|
+
return "volume";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* F3: classify a value into an l0..l4 bucket using RELATIVE bands (quintiles of the
|
|
257
|
+
* observed max). Used for cost / error / volume modes where there are no absolute
|
|
258
|
+
* gold-standard boundaries (latency keeps its absolute bands via classifyHeatLevel).
|
|
259
|
+
* Deterministic. maxValue 0 → all cells l0.
|
|
260
|
+
*/
|
|
261
|
+
function classifyRelativeLevel(value: number, maxValue: number): number {
|
|
262
|
+
if (maxValue <= 0 || value <= 0) return 0;
|
|
263
|
+
const frac = value / maxValue;
|
|
264
|
+
if (frac >= 0.8) return 4;
|
|
265
|
+
if (frac >= 0.6) return 3;
|
|
266
|
+
if (frac >= 0.4) return 2;
|
|
267
|
+
if (frac >= 0.2) return 1;
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Absolute latency→l0..l4 bands (mirrors render.ts classifyHeatLevel for parity). */
|
|
272
|
+
function classifyLatencyLevel(avgS: number): number {
|
|
273
|
+
if (avgS < 50) return 0;
|
|
274
|
+
if (avgS < 65) return 1;
|
|
275
|
+
if (avgS < 85) return 2;
|
|
276
|
+
if (avgS < 100) return 3;
|
|
277
|
+
return 4;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Aggregate a 24h heatmap from per-trace startTime + latencyMs (+ cost + error).
|
|
282
|
+
* Each cell = one hour bucket: trace count, avg latency (s), max latency (s), plus
|
|
283
|
+
* F3 dynamic-metric fields (level / metricValue / metricLabel) keyed to the primary
|
|
284
|
+
* signal. Cells with zero traces are still emitted (count 0) so the 24-cell grid is
|
|
285
|
+
* complete. DETERMINISTIC — pure function of the metadata + the (data-derived)
|
|
286
|
+
* primary-signal name.
|
|
287
|
+
*
|
|
288
|
+
* F3 default = latency: when primarySignalName is latency/absent the cell colour is
|
|
289
|
+
* the historic absolute-band latency colour, so a latency-primary run is unchanged.
|
|
290
|
+
*/
|
|
291
|
+
export function aggregateHourlyHeatmap(
|
|
292
|
+
metadata: MetadataInput[],
|
|
293
|
+
primarySignalName?: string
|
|
294
|
+
): HourlyHeatmap {
|
|
295
|
+
const buckets: Array<{ latencies: number[]; costs: number[]; errors: number }> =
|
|
296
|
+
Array.from({ length: 24 }, () => ({ latencies: [], costs: [], errors: 0 }));
|
|
297
|
+
|
|
298
|
+
for (const t of metadata) {
|
|
299
|
+
if (!t.startTime || t.latencyMs === undefined) continue;
|
|
300
|
+
const hour = new Date(t.startTime).getUTCHours();
|
|
301
|
+
if (Number.isNaN(hour)) continue;
|
|
302
|
+
buckets[hour].latencies.push(t.latencyMs);
|
|
303
|
+
buckets[hour].costs.push(t.totalCostUsd ?? 0);
|
|
304
|
+
if (t.hasError) buckets[hour].errors += 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const mode = resolveHeatMetricMode(primarySignalName);
|
|
308
|
+
|
|
309
|
+
// First pass: latency stats + the raw active-metric value per cell.
|
|
310
|
+
const interim = buckets.map((b, hour) => {
|
|
311
|
+
const count = b.latencies.length;
|
|
312
|
+
const avgS = count > 0 ? round(sum(b.latencies) / count / 1000) : 0;
|
|
313
|
+
const maxS = count > 0 ? round(Math.max(...b.latencies) / 1000) : 0;
|
|
314
|
+
const avgCost = count > 0 ? round(sum(b.costs) / count, 4) : 0;
|
|
315
|
+
const errorRate = count > 0 ? round((b.errors / count) * 100) : 0;
|
|
316
|
+
const metricValue =
|
|
317
|
+
mode === "cost" ? avgCost : mode === "error" ? errorRate : mode === "volume" ? count : avgS;
|
|
318
|
+
return { hour, count, avgS, maxS, avgCost, errorRate, metricValue };
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Relative-band modes need the observed max across cells.
|
|
322
|
+
const maxMetric = Math.max(0, ...interim.map((c) => c.metricValue));
|
|
323
|
+
|
|
324
|
+
const cells: HourlyHeatCell[] = interim.map((c) => {
|
|
325
|
+
const level =
|
|
326
|
+
mode === "latency"
|
|
327
|
+
? classifyLatencyLevel(c.avgS)
|
|
328
|
+
: classifyRelativeLevel(c.metricValue, maxMetric);
|
|
329
|
+
const metricLabel =
|
|
330
|
+
mode === "cost"
|
|
331
|
+
? `avg $${c.avgCost.toFixed(2)}`
|
|
332
|
+
: mode === "error"
|
|
333
|
+
? `${c.errorRate}% errors`
|
|
334
|
+
: mode === "volume"
|
|
335
|
+
? `${c.count} traces`
|
|
336
|
+
: `avg ${c.avgS}s`;
|
|
337
|
+
return {
|
|
338
|
+
hour: c.hour,
|
|
339
|
+
count: c.count,
|
|
340
|
+
avgS: c.avgS,
|
|
341
|
+
maxS: c.maxS,
|
|
342
|
+
level,
|
|
343
|
+
metricValue: c.metricValue,
|
|
344
|
+
metricLabel,
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Narrative: top volume + top active-metric hours.
|
|
349
|
+
const byVol = cells.filter((c) => c.count > 0).slice().sort((a, b) => b.count - a.count);
|
|
350
|
+
const byMetric = cells
|
|
351
|
+
.filter((c) => c.count > 0)
|
|
352
|
+
.slice()
|
|
353
|
+
.sort((a, b) => (b.metricValue ?? 0) - (a.metricValue ?? 0));
|
|
354
|
+
const fmt = (c: HourlyHeatCell) => `${c.hour.toString().padStart(2, "0")}h`;
|
|
355
|
+
const metricWord =
|
|
356
|
+
mode === "cost" ? "cost" : mode === "error" ? "error rate" : mode === "volume" ? "volume" : "latency";
|
|
357
|
+
const narrative =
|
|
358
|
+
byVol.length > 0 && byMetric.length > 0
|
|
359
|
+
? `Volume peaks at ${byVol.slice(0, 3).map(fmt).join(", ")}; ${metricWord} peaks at ${byMetric.slice(0, 3).map(fmt).join(", ")}.`
|
|
360
|
+
: undefined;
|
|
361
|
+
|
|
362
|
+
const metric: HourlyHeatmap["metric"] = {
|
|
363
|
+
signal: primarySignalName ?? "latency-spike",
|
|
364
|
+
label:
|
|
365
|
+
mode === "cost" ? "avg cost" : mode === "error" ? "error rate" : mode === "volume" ? "volume" : "avg latency",
|
|
366
|
+
unit: mode === "cost" ? "$" : mode === "error" ? "%" : mode === "volume" ? "" : "s",
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const heatmap: HourlyHeatmap = { cells, metric };
|
|
370
|
+
if (narrative) heatmap.narrative = narrative;
|
|
371
|
+
return heatmap;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Big-stat row ───────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
/** Build the 6-tile big-stat row from latency percentiles + cost + counts. */
|
|
377
|
+
export function buildBigStat(
|
|
378
|
+
metadata: MetadataInput[],
|
|
379
|
+
tier0: Tier0Input
|
|
380
|
+
): BigStat[] {
|
|
381
|
+
// Truly starved: no traces at all → no big-stat row to build (fail-loud signal).
|
|
382
|
+
if (metadata.length === 0) return [];
|
|
383
|
+
|
|
384
|
+
const latencies = metadata
|
|
385
|
+
.map((t) => t.latencyMs)
|
|
386
|
+
.filter((v): v is number => v !== undefined);
|
|
387
|
+
const cost = round(sum(metadata.map((t) => t.totalCostUsd ?? 0)), 2);
|
|
388
|
+
const errors = tier0.withError ?? metadata.filter((t) => t.hasError).length;
|
|
389
|
+
|
|
390
|
+
// PRD-SO-07: cost tile color-coded: red > $50, yellow > $10, muted <= $10.
|
|
391
|
+
const costColor = cost > 50 ? "var(--r)" : cost > 10 ? "var(--y)" : "var(--muted)";
|
|
392
|
+
|
|
393
|
+
return [
|
|
394
|
+
{ value: latencies.length ? ms2s(percentile(latencies, 50)) : "—", label: "latency p50", color: "var(--y)" },
|
|
395
|
+
{ value: latencies.length ? ms2s(percentile(latencies, 95)) : "—", label: "latency p95", color: "var(--y)" },
|
|
396
|
+
{ value: latencies.length ? ms2s(Math.max(...latencies)) : "—", label: "latency max", color: "var(--r)" },
|
|
397
|
+
{ value: cost > 0 ? `$${cost.toFixed(2)}` : "—", label: "cost / window", color: costColor },
|
|
398
|
+
{ value: metadata.length.toLocaleString("en-US"), label: "traces", color: "var(--muted)" },
|
|
399
|
+
{ value: String(errors), label: "errors", color: errors > 0 ? "var(--r)" : "var(--g)" },
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Signal census ──────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* W11-01: Impact weight per WHAT category.
|
|
407
|
+
* Signals that map to user-facing failure WHATs (correctness / cost / latency)
|
|
408
|
+
* score higher than observability artifacts (missing-metadata, etc.).
|
|
409
|
+
* Scale: 3 = direct user harm, 2 = indirect / degraded UX, 1 = mild / uncertain.
|
|
410
|
+
*
|
|
411
|
+
* W17-C (R3 — default impact ≥ 2): the DISCOVERED taxonomy (deep-read-found WHATs
|
|
412
|
+
* + ledger-folded digests + trajectory-corroborated signals) is keyed here so a
|
|
413
|
+
* discovered signal NEVER falls back to the impact-1 default — at impact 1 a fresh,
|
|
414
|
+
* genuine discovery would silently lose to latency-spike (impact 2) on prevalence
|
|
415
|
+
* tiebreak. The discovered WHY-style signals come from the trajectory map
|
|
416
|
+
* (loop/latency · tool-misuse · handoff-loss · prompt-underspec); `correctness` and
|
|
417
|
+
* `loop` carry impact 3. `hallucination` is already 3 (a correctness WHAT).
|
|
418
|
+
*/
|
|
419
|
+
const SIGNAL_IMPACT: Record<string, number> = {
|
|
420
|
+
"wrong-output": 3,
|
|
421
|
+
"hallucination": 3,
|
|
422
|
+
"missing-output": 3,
|
|
423
|
+
"loop": 3,
|
|
424
|
+
"cost-overshoot": 3,
|
|
425
|
+
"latency-spike": 2,
|
|
426
|
+
"format-violation": 2,
|
|
427
|
+
"user-complaint": 2,
|
|
428
|
+
"low-score": 2,
|
|
429
|
+
"missing-context": 1,
|
|
430
|
+
|
|
431
|
+
// ── W17-C (R3): discovered taxonomy — impact ≥ 2 floor (never the default 1) ──
|
|
432
|
+
"correctness": 3, // umbrella correctness WHAT (≥ latency on a tie)
|
|
433
|
+
"handoff-loss": 2, // abandoned-call → degraded multi-step UX
|
|
434
|
+
"prompt-underspec": 2, // oscillation → wasted turns / wrong path
|
|
435
|
+
"tool-misuse": 2, // tool-error → broken action, degraded UX
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* W17-C (R3): DISCOVERED-signal default impact. Any discovered signal NOT explicitly
|
|
440
|
+
* keyed in SIGNAL_IMPACT falls back to 2 (NOT the cheap-signal default of 1), so an
|
|
441
|
+
* unknown but genuine discovered category cannot silently lose to latency-spike on the
|
|
442
|
+
* prevalence tiebreak. Tier-0 cheap signals keep the impact-1 default (DEFAULT_IMPACT).
|
|
443
|
+
*/
|
|
444
|
+
const DISCOVERED_DEFAULT_IMPACT = 2;
|
|
445
|
+
|
|
446
|
+
/** Tier-0 cheap-signal default impact (unchanged — mild / uncertain). */
|
|
447
|
+
const DEFAULT_IMPACT = 1;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* W11-01: Benign observability artifacts that NEVER qualify as a primary signal
|
|
451
|
+
* (failure-validity gate). These patterns may appear in the Tier-0 census but are
|
|
452
|
+
* ruled out before impact×prevalence scoring because they do not map to a user-
|
|
453
|
+
* visible WHAT failure category.
|
|
454
|
+
*
|
|
455
|
+
* W12-05 (PR-051 propose): `low-tagging-rate` was REMOVED — its Tier-0 emitter
|
|
456
|
+
* (LF-002) is deleted, so it is now dead. The BENIGN_SIGNALS mechanism is RETAINED
|
|
457
|
+
* as a forward-looking guard for genuine future observability artifacts.
|
|
458
|
+
*/
|
|
459
|
+
const BENIGN_SIGNALS = new Set([
|
|
460
|
+
"missing-metadata",
|
|
461
|
+
"missing-score",
|
|
462
|
+
]);
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* W12-05 (PR-051 propose): signal source-allowlist. The census admits only
|
|
466
|
+
* MECHANICAL signals that map to a user-visible failure WHAT. Any Tier-0 pattern
|
|
467
|
+
* whose name is neither a known mechanical signal NOR a deep-read failure-WHAT
|
|
468
|
+
* category is observability hygiene by construction and is ruled out BEFORE
|
|
469
|
+
* scoring — observability hygiene can never leak in as a signal/census row even
|
|
470
|
+
* if a future Tier-0 module emits one. Deep-read `failureOrigin.what` categories
|
|
471
|
+
* are admitted via SIGNAL_IMPACT (which keys them) at the corroboration step.
|
|
472
|
+
*/
|
|
473
|
+
const MECHANICAL_SIGNALS = new Set([
|
|
474
|
+
"error-spike",
|
|
475
|
+
"latency-spike",
|
|
476
|
+
"feedback-cluster",
|
|
477
|
+
"low-score-concentration",
|
|
478
|
+
"api-exhaustion",
|
|
479
|
+
"context-compaction-cluster",
|
|
480
|
+
"high-teammate-ratio",
|
|
481
|
+
]);
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* True when a Tier-0 pattern name is an admissible census signal — i.e. a
|
|
485
|
+
* mechanical signal, a deep-read failure-WHAT category, OR a recognized benign
|
|
486
|
+
* artifact (which earns a visible "ruled out" row rather than a silent drop).
|
|
487
|
+
* Anything else is observability hygiene and is dropped silently (W12-05).
|
|
488
|
+
*/
|
|
489
|
+
function isAdmissibleSignal(name: string): boolean {
|
|
490
|
+
return MECHANICAL_SIGNALS.has(name) || name in SIGNAL_IMPACT || BENIGN_SIGNALS.has(name);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── W17-C SELECTION HUB — discovered-signal pool + the R1 evidence floor ──────────
|
|
494
|
+
//
|
|
495
|
+
// Wave-17 makes deep-read-DISCOVERED signals FIRST-CLASS census candidates (the old
|
|
496
|
+
// `validPatterns.some(p => p.name === llmWhat)` gate is deleted). A discovered signal
|
|
497
|
+
// is any failure-WHAT surfaced by the LLM analyzer (findings[].failureOrigin.what) OR
|
|
498
|
+
// folded from a still-valid cross-run ledger digest (Block G). But first-class ≠ free:
|
|
499
|
+
// R1 — a discovered signal may be PRIMARY only if MECHANICALLY corroborated by an
|
|
500
|
+
// analyzeTrajectory() corroboration whose signal (via PATTERN_SIGNAL_MAP) maps
|
|
501
|
+
// to the discovered WHAT, with a RESOLVABLE evidenceRef. LLM-asserted citation
|
|
502
|
+
// alone is NOT enough. Unevidenced discovered → capped at SECONDARY.
|
|
503
|
+
// R2 — a ledger-folded digest must RE-PASS the SAME floor (its evidenceRef must still
|
|
504
|
+
// resolve + corroborate). A digest is never trusted blindly.
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* W17-C (R1/R4): does a trajectory corroboration `signal` corroborate a discovered
|
|
508
|
+
* WHAT? The trajectory layer emits coarse WHY-style signal labels (loop/latency ·
|
|
509
|
+
* tool-misuse · handoff-loss · prompt-underspec — see PATTERN_SIGNAL_MAP); the
|
|
510
|
+
* discovered WHAT may be a WhatCategory (loop · latency-spike · hallucination …) OR a
|
|
511
|
+
* WhyCategory-style label (tool-misuse · handoff-loss · prompt-underspec). The compound
|
|
512
|
+
* `loop/latency` corroborates EITHER `loop` OR `latency-spike`. Exact-label matches
|
|
513
|
+
* (tool-misuse / handoff-loss / prompt-underspec) pass through. `hallucination` has no
|
|
514
|
+
* mechanical trajectory signal → never corroborated here (it is content-only; R1 caps
|
|
515
|
+
* it at SECONDARY unless an exact-named trajectory signal ever maps to it).
|
|
516
|
+
*/
|
|
517
|
+
function trajectorySignalMatchesWhat(signal: TrajectorySignal, what: string): boolean {
|
|
518
|
+
if (signal === "loop/latency") return what === "loop" || what === "latency-spike";
|
|
519
|
+
// The remaining trajectory signals are exact WHY-style labels.
|
|
520
|
+
return signal === what;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* W17-C (R1): a single resolved mechanical corroboration for a discovered WHAT.
|
|
525
|
+
* Carries the concrete evidenceRef so the census/selection card can CITE it (not just
|
|
526
|
+
* assert it). `undefined` from resolveCorroboration() means "unevidenced → cap at
|
|
527
|
+
* SECONDARY".
|
|
528
|
+
*/
|
|
529
|
+
interface ResolvedCorroboration {
|
|
530
|
+
signal: TrajectorySignal;
|
|
531
|
+
evidenceRef: string;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* W17-C (R1 — evidence-cited floor): find a trajectory corroboration that mechanically
|
|
536
|
+
* evidences `what`. A corroboration counts ONLY when:
|
|
537
|
+
* (a) its signal maps (via PATTERN_SIGNAL_MAP semantics) to `what`, AND
|
|
538
|
+
* (b) its evidenceRef RESOLVES — i.e. is a non-empty, well-formed trace span pointer
|
|
539
|
+
* (`trace:<id>#msg[...]`, the makeEvidenceRef shape). An empty/garbled ref is NOT
|
|
540
|
+
* resolvable, so a corroboration carrying one does not satisfy the floor.
|
|
541
|
+
* Returns the first matching resolvable corroboration (deterministic — input order),
|
|
542
|
+
* or undefined when none corroborates `what` (→ discovered signal capped at SECONDARY).
|
|
543
|
+
*/
|
|
544
|
+
function resolveCorroboration(
|
|
545
|
+
what: string,
|
|
546
|
+
corroborations: ReadonlyArray<TrajectoryCorroboration>
|
|
547
|
+
): ResolvedCorroboration | undefined {
|
|
548
|
+
for (const c of corroborations) {
|
|
549
|
+
if (!trajectorySignalMatchesWhat(c.signal, what)) continue;
|
|
550
|
+
if (!isResolvableEvidenceRef(c.evidenceRef)) continue;
|
|
551
|
+
return { signal: c.signal, evidenceRef: c.evidenceRef };
|
|
552
|
+
}
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* W17-C (R1/R2): an evidenceRef is RESOLVABLE when it matches the deterministic
|
|
558
|
+
* trajectory-evidence shape `trace:<traceId>#msg[<indices>]` (makeEvidenceRef in
|
|
559
|
+
* scan/trajectory.ts) with at least one span index. This is the mechanical "does the
|
|
560
|
+
* citation point at a real span" check the floor demands — an LLM that merely asserts
|
|
561
|
+
* a signal without a span pointer, or a stale ledger digest whose evidenceRef no longer
|
|
562
|
+
* conforms, fails here and cannot reach PRIMARY. Pure + deterministic.
|
|
563
|
+
*/
|
|
564
|
+
export function isResolvableEvidenceRef(ref: string | undefined): boolean {
|
|
565
|
+
return typeof ref === "string" && /^trace:[^#]+#msg\[\d+(?:,\d+)*\]$/.test(ref);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* W17-C (selection hub): one merged candidate in the discovered-signal pool. `origin`
|
|
570
|
+
* records WHERE the candidate came from (deep-read finding vs folded ledger digest) so
|
|
571
|
+
* the selection card + residual-surfacing copy can be honest. `seenCount` is the count
|
|
572
|
+
* of distinct traces the signal was observed on (deep-read finding sourceTraceIds OR
|
|
573
|
+
* 1-per-folded-digest) — the basis for the HONEST prevalence measure (R6: "seen in k/n
|
|
574
|
+
* sampled", never a fabricated corpus rate).
|
|
575
|
+
*/
|
|
576
|
+
interface DiscoveredCandidate {
|
|
577
|
+
what: string;
|
|
578
|
+
origin: "deep-read" | "ledger";
|
|
579
|
+
/** Distinct traces this discovered signal was observed on (honest prevalence numerator). */
|
|
580
|
+
seenCount: number;
|
|
581
|
+
/** R1: the resolved mechanical corroboration (undefined → cap at SECONDARY). */
|
|
582
|
+
corroboration?: ResolvedCorroboration;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* W12-F1 (UI-2op): the auditable signal-selection metadata the enricher derives
|
|
587
|
+
* from the SAME census scoring data. Threaded into runMeta so the Methodology tab
|
|
588
|
+
* can SHOW THE WORK (per-signal score cards + a mermaid decision-path trace)
|
|
589
|
+
* instead of falling back to the generic 5-step blurb. DETERMINISTIC — no LLM,
|
|
590
|
+
* no clock, no random; purely a function of the impact×prevalence scoring already
|
|
591
|
+
* computed for the census + the deep-read corroboration outcome (PR-049 / PR-038).
|
|
592
|
+
*/
|
|
593
|
+
export interface SignalSelectionMeta {
|
|
594
|
+
/** Per-signal selection-rule cards (RunMeta.selectionRules shape). */
|
|
595
|
+
selectionRules: NonNullable<RunMeta["selectionRules"]>;
|
|
596
|
+
/** Mermaid decision-path trace (RunMeta.signalSelectionTrace shape). */
|
|
597
|
+
signalSelectionTrace: string;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* W17-C (R7 — residual surfacing): emitted when the top DISCOVERED signal is capped at
|
|
602
|
+
* SECONDARY for lack of mechanical evidence (R1 floor) AND primary falls back to a cheap
|
|
603
|
+
* signal. Block E (the renderer) reads this to show "suspected primary — unconfirmed"
|
|
604
|
+
* instead of silently crowning the cheap signal (e.g. latency-spike). When the discovered
|
|
605
|
+
* signal IS evidence-floored (passes R1) this is absent — there is nothing unconfirmed.
|
|
606
|
+
*/
|
|
607
|
+
export interface SuspectedPrimary {
|
|
608
|
+
/** The discovered WHAT that was suspected-but-uncorroborated. */
|
|
609
|
+
signal: string;
|
|
610
|
+
/** Distinct sampled traces it was seen on (honest prevalence numerator). */
|
|
611
|
+
seenCount: number;
|
|
612
|
+
/** Sampled-trace denominator (R6 — "seen in k/n sampled"). */
|
|
613
|
+
sampledCount: number;
|
|
614
|
+
/** Why it was capped at SECONDARY (the R1-floor reason). */
|
|
615
|
+
reason: string;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* W17-C (R7): RunMeta + the residual-surfacing field. The canonical `RunMeta` lives in
|
|
620
|
+
* report/render.ts (Block E's scope) and does not yet declare `suspectedPrimaryUnconfirmed`;
|
|
621
|
+
* the enricher (Block C) is the PRODUCER of the field. We model it locally as a superset
|
|
622
|
+
* so the enricher stays type-safe WITHOUT editing render.ts (out of this block's scope).
|
|
623
|
+
* Block E adds the canonical slot to RunMeta when it lands the renderer consumption; the
|
|
624
|
+
* shape here is the contract Block E reads. Structurally a RunMetaW17 IS a RunMeta, so it
|
|
625
|
+
* threads transparently into RenderInput.runMeta.
|
|
626
|
+
*/
|
|
627
|
+
export type RunMetaW17 = RunMeta & {
|
|
628
|
+
/** R7: present only when a discovered top signal was capped at SECONDARY (unevidenced). */
|
|
629
|
+
suspectedPrimaryUnconfirmed?: SuspectedPrimary;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* W17-C (SELECTION HUB): the discovered-signal wiring the census reads to make
|
|
634
|
+
* deep-read findings + folded ledger digests FIRST-CLASS candidates under the R1
|
|
635
|
+
* evidence floor. All OPTIONAL — a caller that passes only `tier0`/`total`/`findings`
|
|
636
|
+
* gets the legacy Tier-0-only behaviour (backward-compat: existing 3-arg callers and
|
|
637
|
+
* tests are unaffected). Block C (this file) owns this object; Block B threads the
|
|
638
|
+
* folded ledger in, Block D1 threads the trajectory corroborations in.
|
|
639
|
+
*/
|
|
640
|
+
export interface SignalCensusContext {
|
|
641
|
+
/**
|
|
642
|
+
* R1 evidence floor surface: mechanical corroborations from analyzeTrajectory()
|
|
643
|
+
* across the sampled traces. A discovered signal reaches PRIMARY only if one of
|
|
644
|
+
* these corroborates it (signal maps via PATTERN_SIGNAL_MAP + resolvable evidenceRef).
|
|
645
|
+
*/
|
|
646
|
+
corroborations?: ReadonlyArray<TrajectoryCorroboration>;
|
|
647
|
+
/**
|
|
648
|
+
* R2 surface: ledger digests folded down to the still-VALID set by
|
|
649
|
+
* store.foldValidDigests(). Each is re-admitted as a discovered candidate and MUST
|
|
650
|
+
* re-pass the SAME evidence floor (its evidenceRef must still resolve + corroborate)
|
|
651
|
+
* — never trusted blindly.
|
|
652
|
+
*/
|
|
653
|
+
foldedDigests?: ReadonlyArray<DeepReadLedgerEntry>;
|
|
654
|
+
/**
|
|
655
|
+
* R6 honest-prevalence denominator: how many traces were actually SAMPLED (deep-read).
|
|
656
|
+
* A discovered primary's measure reads "seen in k/n sampled" against THIS n — never a
|
|
657
|
+
* fabricated corpus rate over `total`. Defaults to `total` when absent (best-effort).
|
|
658
|
+
*/
|
|
659
|
+
sampledCount?: number;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Finding subset the census reads (widened W17-C: + sourceTraceIds for honest
|
|
664
|
+
* prevalence; widened D3: + the RCA-narrative fields the dataset-candidate cards
|
|
665
|
+
* derive from — failureOrigin.{why,where,whatHappened} · severity · problem ·
|
|
666
|
+
* title · subDesc · whyChain origin). All optional — the real Finding is a strict
|
|
667
|
+
* superset, so passing normalizedFindings (full Finding[]) satisfies this type.
|
|
668
|
+
*/
|
|
669
|
+
type CensusFinding = {
|
|
670
|
+
findingId?: string;
|
|
671
|
+
failureOrigin?: { what?: string; why?: string; where?: string; whatHappened?: string };
|
|
672
|
+
sourceTraceIds?: string[];
|
|
673
|
+
severity?: "crit" | "high" | "med" | "info";
|
|
674
|
+
problem?: string;
|
|
675
|
+
title?: string;
|
|
676
|
+
subDesc?: string;
|
|
677
|
+
whyChain?: Array<{ why?: string; isOrigin?: boolean }>;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* W11-01 + W17-C: Build the signal census as the SELECTION HUB. Pipeline:
|
|
682
|
+
* 1. Failure-validity gate — drop non-admissible signals; benign → ruled-out row.
|
|
683
|
+
* 2. Tier-0 cheap-signal scoring: impact × (matchCount/total), default impact 1.
|
|
684
|
+
* 3. DISCOVERED pool (W17-C, first-class): findings[].failureOrigin.what ∪ folded
|
|
685
|
+
* ledger digests. Impact-dominant ranking (impact first, prevalence tiebreak),
|
|
686
|
+
* default impact 2 (R3). The OLD gate `validPatterns.some(p => p.name === llmWhat)`
|
|
687
|
+
* is DELETED — a discovered signal no longer needs a matching Tier-0 pattern.
|
|
688
|
+
* 4. R1 evidence floor: a discovered signal is PRIMARY-eligible ONLY when mechanically
|
|
689
|
+
* corroborated (trajectory corroboration maps to its WHAT + resolvable evidenceRef).
|
|
690
|
+
* R2: folded digests re-pass the SAME floor. Unevidenced discovered → SECONDARY.
|
|
691
|
+
* 5. PRIMARY selection: an evidence-floored discovered signal beats the top cheap
|
|
692
|
+
* signal (deep-read mechanical > static frequency). Else primary falls back to the
|
|
693
|
+
* top cheap signal; if a discovered top signal was capped for lack of evidence,
|
|
694
|
+
* `suspectedPrimaryUnconfirmed` is emitted (R7) so Block E shows "suspected primary
|
|
695
|
+
* — unconfirmed" instead of silently crowning latency.
|
|
696
|
+
*
|
|
697
|
+
* Returns rows AND attaches primarySignal + selectionRules + signalSelectionTrace +
|
|
698
|
+
* (W17-C) suspectedPrimaryUnconfirmed to the array so callers thread them into RunMeta.
|
|
699
|
+
*
|
|
700
|
+
* @param tier0 - Tier-0 scan output
|
|
701
|
+
* @param total - Denominator for prevalence (total traces in window)
|
|
702
|
+
* @param findings - Optional findings (deep-read discovered WHATs + sourceTraceIds)
|
|
703
|
+
* @param ctx - W17-C selection-hub context (corroborations · foldedDigests · sampledCount)
|
|
704
|
+
*/
|
|
705
|
+
export function buildSignalCensus(
|
|
706
|
+
tier0: Tier0Input,
|
|
707
|
+
total: number,
|
|
708
|
+
findings?: ReadonlyArray<CensusFinding>,
|
|
709
|
+
ctx?: SignalCensusContext
|
|
710
|
+
): SignalCensusRow[] &
|
|
711
|
+
{ primarySignal?: PrimarySignal } &
|
|
712
|
+
Partial<SignalSelectionMeta> &
|
|
713
|
+
{ suspectedPrimaryUnconfirmed?: SuspectedPrimary } {
|
|
714
|
+
const rawPatterns = (tier0.patterns ?? []).slice();
|
|
715
|
+
const corroborations = ctx?.corroborations ?? [];
|
|
716
|
+
const foldedDigests = ctx?.foldedDigests ?? [];
|
|
717
|
+
// R6: honest prevalence denominator = sampled count (falls back to total).
|
|
718
|
+
const sampledN = ctx?.sampledCount ?? total;
|
|
719
|
+
|
|
720
|
+
// ── W17-C: assemble the DISCOVERED candidate pool (first-class) ───────────────
|
|
721
|
+
// Source A: deep-read findings — every failureOrigin.what is a discovered signal.
|
|
722
|
+
// Source B (R2): folded ledger digests — each digest.signal re-admits as discovered.
|
|
723
|
+
// Both are merged + deduped by WHAT; seenCount sums distinct evidence (honest prev).
|
|
724
|
+
const discovered = buildDiscoveredPool(findings ?? [], foldedDigests, corroborations);
|
|
725
|
+
|
|
726
|
+
// Truly starved: no detected patterns AND no signal counts AND no discovered pool →
|
|
727
|
+
// no census to build (fail-loud signal). A census with only rule-out rows requires
|
|
728
|
+
// that tier0 at least measured the signals (or a discovery exists).
|
|
729
|
+
const hasSignalData =
|
|
730
|
+
rawPatterns.length > 0 ||
|
|
731
|
+
discovered.length > 0 ||
|
|
732
|
+
tier0.withError !== undefined ||
|
|
733
|
+
tier0.withLowScore !== undefined ||
|
|
734
|
+
tier0.withFeedback !== undefined;
|
|
735
|
+
if (!hasSignalData)
|
|
736
|
+
return Object.assign([], {
|
|
737
|
+
primarySignal: undefined,
|
|
738
|
+
selectionRules: undefined,
|
|
739
|
+
signalSelectionTrace: undefined,
|
|
740
|
+
suspectedPrimaryUnconfirmed: undefined,
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// ── Step 1: failure-validity gate (Tier-0 cheap signals) ─────────────────────
|
|
744
|
+
// 1a. Signal source-allowlist (W12-05 / PR-051 propose): drop non-admissible
|
|
745
|
+
// signals SILENTLY (no census row at all — not even ruled-out). Observability
|
|
746
|
+
// hygiene must never surface anywhere in the census.
|
|
747
|
+
// 1b. BENIGN_SIGNALS: admissible-but-benign artifacts get a visible "ruled out"
|
|
748
|
+
// row (forward-looking guard; currently {missing-metadata, missing-score}).
|
|
749
|
+
const ruledOut: string[] = [];
|
|
750
|
+
const validPatternsAll = rawPatterns.filter((p) => {
|
|
751
|
+
if (!isAdmissibleSignal(p.name)) {
|
|
752
|
+
// Not a mechanical signal and not a deep-read failure WHAT → observability
|
|
753
|
+
// hygiene. Dropped silently; never rendered (W12-05).
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
if (BENIGN_SIGNALS.has(p.name)) {
|
|
757
|
+
ruledOut.push(p.name);
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
return true;
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// ── W17-C: dedupe overlapping signals across the cheap + discovered pools ──────
|
|
764
|
+
// A signal name may appear in BOTH Tier-0 (cheap pattern) AND the discovered pool
|
|
765
|
+
// (same failure surfaced by deep-read). It is ONE signal — render ONE row. Ownership:
|
|
766
|
+
// - CORROBORATED discovered name → owned by the discovered pool (it can be PRIMARY
|
|
767
|
+
// via the evidence floor). Suppress the cheap row so there is no duplicate.
|
|
768
|
+
// - UNCORROBORATED discovered name that ALSO has a cheap Tier-0 pattern → owned by
|
|
769
|
+
// the cheap pool (normal prevalence scoring; the discovered duplicate is dropped —
|
|
770
|
+
// no extra "suspected-unconfirmed" row for a signal Tier-0 already measures).
|
|
771
|
+
// A discovered name with NO cheap pattern always stays in the discovered pool.
|
|
772
|
+
const corroboratedDiscoveredNames = new Set(
|
|
773
|
+
discovered.filter((d) => d.corroboration !== undefined).map((d) => d.what)
|
|
774
|
+
);
|
|
775
|
+
const cheapPatternNames = new Set(validPatternsAll.map((p) => p.name));
|
|
776
|
+
|
|
777
|
+
// Cheap pool: drop any pattern whose name is owned by a CORROBORATED discovered signal.
|
|
778
|
+
const validPatterns = validPatternsAll.filter((p) => !corroboratedDiscoveredNames.has(p.name));
|
|
779
|
+
|
|
780
|
+
// Discovered pool: drop an UNCORROBORATED discovered signal that a cheap pattern already
|
|
781
|
+
// covers (avoid a duplicate suspected-unconfirmed row for a Tier-0-measured signal).
|
|
782
|
+
const discoveredDeduped = discovered.filter(
|
|
783
|
+
(d) => d.corroboration !== undefined || !cheapPatternNames.has(d.what)
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// ── Step 2: Tier-0 cheap-signal scoring (UNCHANGED real invariant) ────────────
|
|
787
|
+
// score = impact * (matchCount / total). Ties broken by matchCount, then name.
|
|
788
|
+
// Cheap signals keep the impact-1 DEFAULT_IMPACT fallback.
|
|
789
|
+
const scored = validPatterns
|
|
790
|
+
.map((p) => {
|
|
791
|
+
const impact = SIGNAL_IMPACT[p.name] ?? DEFAULT_IMPACT;
|
|
792
|
+
const prevalence = p.matchCount / Math.max(1, total);
|
|
793
|
+
return { ...p, score: impact * prevalence };
|
|
794
|
+
})
|
|
795
|
+
.sort((a, b) => {
|
|
796
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
797
|
+
if (b.matchCount !== a.matchCount) return b.matchCount - a.matchCount;
|
|
798
|
+
return a.name.localeCompare(b.name);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// ── Step 3: DISCOVERED impact-dominant ranking (R3 default impact ≥ 2) ────────
|
|
802
|
+
// Rank discovered signals by IMPACT first (impact-dominant), prevalence (seenCount)
|
|
803
|
+
// tiebreak, then name. Unknown discovered categories get DISCOVERED_DEFAULT_IMPACT
|
|
804
|
+
// (2) — never the cheap-signal default of 1 (else a fresh discovery silently loses
|
|
805
|
+
// to latency-spike). This is intentionally NOT impact×prevalence: a low-prevalence
|
|
806
|
+
// but high-impact freshly-discovered failure should not be buried.
|
|
807
|
+
const discoveredScored = discoveredDeduped
|
|
808
|
+
.map((d) => ({
|
|
809
|
+
...d,
|
|
810
|
+
impact: SIGNAL_IMPACT[d.what] ?? DISCOVERED_DEFAULT_IMPACT,
|
|
811
|
+
}))
|
|
812
|
+
.sort((a, b) => {
|
|
813
|
+
if (b.impact !== a.impact) return b.impact - a.impact; // impact-dominant
|
|
814
|
+
if (b.seenCount !== a.seenCount) return b.seenCount - a.seenCount; // prevalence tiebreak
|
|
815
|
+
return a.what.localeCompare(b.what);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// ── Step 4 + 5: R1 evidence floor → PRIMARY selection ────────────────────────
|
|
819
|
+
// The top discovered signal that PASSES the evidence floor (has a resolved mechanical
|
|
820
|
+
// corroboration) is PRIMARY-eligible and beats the top cheap signal. If the top
|
|
821
|
+
// discovered signal exists but is UNEVIDENCED, it is capped at SECONDARY and the
|
|
822
|
+
// residual flag is raised (R7).
|
|
823
|
+
const topDiscovered = discoveredScored[0];
|
|
824
|
+
const evidencedDiscovered = discoveredScored.find((d) => d.corroboration !== undefined);
|
|
825
|
+
|
|
826
|
+
const cheapPrimary = scored[0]?.name;
|
|
827
|
+
let primaryName: string | undefined;
|
|
828
|
+
let corroborationNote = "";
|
|
829
|
+
let confidence: PrimarySignal["confidence"] = "medium";
|
|
830
|
+
let primaryEvidenceRef: string | undefined;
|
|
831
|
+
let primaryIsDiscovered = false;
|
|
832
|
+
// R7: when the top discovered signal is capped at SECONDARY for lack of evidence and
|
|
833
|
+
// primary falls back to a cheap signal, surface it so Block E can render "suspected
|
|
834
|
+
// primary — unconfirmed" instead of silently crowning the cheap signal.
|
|
835
|
+
let suspected: SuspectedPrimary | undefined;
|
|
836
|
+
|
|
837
|
+
if (evidencedDiscovered) {
|
|
838
|
+
// R1 PASS — an evidence-floored discovered signal wins (deep-read mechanical >
|
|
839
|
+
// static frequency).
|
|
840
|
+
primaryName = evidencedDiscovered.what;
|
|
841
|
+
primaryIsDiscovered = true;
|
|
842
|
+
primaryEvidenceRef = evidencedDiscovered.corroboration!.evidenceRef;
|
|
843
|
+
confidence = "high";
|
|
844
|
+
corroborationNote =
|
|
845
|
+
evidencedDiscovered.origin === "ledger"
|
|
846
|
+
? `discovered (ledger-folded) — mechanically corroborated (${primaryEvidenceRef})`
|
|
847
|
+
: `discovered (deep-read) — mechanically corroborated (${primaryEvidenceRef})`;
|
|
848
|
+
} else {
|
|
849
|
+
// No evidence-floored discovered signal → primary is the top cheap signal.
|
|
850
|
+
primaryName = cheapPrimary;
|
|
851
|
+
if (cheapPrimary) {
|
|
852
|
+
corroborationNote = "impact×prevalence (no deep-read corroboration)";
|
|
853
|
+
confidence = scored[0].score > 0.5 ? "medium" : "low";
|
|
854
|
+
}
|
|
855
|
+
// R7: a discovered signal WAS surfaced but failed the floor → suspected-unconfirmed.
|
|
856
|
+
if (topDiscovered) {
|
|
857
|
+
suspected = {
|
|
858
|
+
signal: topDiscovered.what,
|
|
859
|
+
seenCount: topDiscovered.seenCount,
|
|
860
|
+
sampledCount: sampledN,
|
|
861
|
+
reason:
|
|
862
|
+
"discovered but UNCONFIRMED — no mechanical trajectory corroboration with a resolvable evidenceRef (R1 evidence floor); capped at SECONDARY",
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ── Build census rows ─────────────────────────────────────────────────────────
|
|
868
|
+
const rows: SignalCensusRow[] = [];
|
|
869
|
+
|
|
870
|
+
// Tier-0 cheap-signal rows. A cheap signal is PRIMARY only when it actually won.
|
|
871
|
+
for (const p of scored) {
|
|
872
|
+
const isPrimary = !primaryIsDiscovered && p.name === primaryName;
|
|
873
|
+
rows.push({
|
|
874
|
+
signal: p.name,
|
|
875
|
+
present: "YES",
|
|
876
|
+
presentColor: "var(--r)",
|
|
877
|
+
measure: `${p.matchCount}/${total} (${round((p.matchCount / total) * 100)}%)`,
|
|
878
|
+
decision: isPrimary
|
|
879
|
+
? `<span class="badge b-crit">★ PRIMARY</span>`
|
|
880
|
+
: `<span class="badge b-med">SECONDARY</span>`,
|
|
881
|
+
primary: isPrimary,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// W17-C discovered-signal rows. Honest prevalence (R6): "seen in k/n sampled" — NOT a
|
|
886
|
+
// corpus rate. The evidence-floored winner is ★ PRIMARY; the rest (incl. the capped
|
|
887
|
+
// suspected one) are SECONDARY with an explicit reason.
|
|
888
|
+
for (const d of discoveredScored) {
|
|
889
|
+
const isPrimary = primaryIsDiscovered && d.what === primaryName;
|
|
890
|
+
const evidenced = d.corroboration !== undefined;
|
|
891
|
+
rows.push({
|
|
892
|
+
signal: d.what,
|
|
893
|
+
present: "YES",
|
|
894
|
+
presentColor: evidenced ? "var(--r)" : "var(--y)",
|
|
895
|
+
// R6 honest prevalence — sampled denominator, never a fabricated corpus rate.
|
|
896
|
+
measure: `seen in ${d.seenCount}/${sampledN} sampled`,
|
|
897
|
+
decision: isPrimary
|
|
898
|
+
? `<span class="badge b-crit">★ PRIMARY</span>`
|
|
899
|
+
: evidenced
|
|
900
|
+
? `<span class="badge b-med">SECONDARY</span>`
|
|
901
|
+
: `<span class="badge b-med">SECONDARY — discovered, unconfirmed (no mechanical evidence)</span>`,
|
|
902
|
+
primary: isPrimary,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Ruled-out rows (benign observability artifacts).
|
|
907
|
+
for (const name of ruledOut) {
|
|
908
|
+
const p = rawPatterns.find((x) => x.name === name)!;
|
|
909
|
+
const rate = p.matchCount / Math.max(1, total);
|
|
910
|
+
rows.push({
|
|
911
|
+
signal: name,
|
|
912
|
+
present: "YES",
|
|
913
|
+
presentColor: rate >= 0.2 ? "var(--y)" : "var(--g)",
|
|
914
|
+
measure: `${p.matchCount}/${total} (${round(rate * 100)}%)`,
|
|
915
|
+
decision:
|
|
916
|
+
rate >= 0.2
|
|
917
|
+
? "ruled out — benign observability artifact (>20% rate)"
|
|
918
|
+
: "ruled out — benign observability artifact",
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// W11-04: Always-present rule-out rows for absent/low error+score signals.
|
|
923
|
+
const withErrorVal = tier0.withError ?? 0;
|
|
924
|
+
const withErrorRate = withErrorVal / Math.max(1, total);
|
|
925
|
+
rows.push({
|
|
926
|
+
signal: "wrong-output / hallucination",
|
|
927
|
+
present: String(withErrorVal),
|
|
928
|
+
presentColor: withErrorVal === 0
|
|
929
|
+
? "var(--g)"
|
|
930
|
+
: withErrorRate >= 0.2
|
|
931
|
+
? "var(--r)"
|
|
932
|
+
: "var(--y)",
|
|
933
|
+
measure: `${withErrorVal} errors`,
|
|
934
|
+
decision:
|
|
935
|
+
withErrorVal === 0
|
|
936
|
+
? "excluded — no signal"
|
|
937
|
+
: withErrorRate >= 0.2
|
|
938
|
+
? `<span class="badge b-crit">potential signal — check rate ${round(withErrorRate * 100)}%</span>`
|
|
939
|
+
: "potential signal — ruled out (benign success:false / <20% rate)",
|
|
940
|
+
});
|
|
941
|
+
rows.push({
|
|
942
|
+
signal: "low-score",
|
|
943
|
+
present: String(tier0.withLowScore ?? 0),
|
|
944
|
+
presentColor: "var(--g)",
|
|
945
|
+
measure: `${tier0.withFeedback ?? 0} feedback`,
|
|
946
|
+
decision: "excluded — no signal",
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// ── Attach primarySignal to result array ─────────────────────────────────────
|
|
950
|
+
const primarySignal: PrimarySignal | undefined = primaryName
|
|
951
|
+
? {
|
|
952
|
+
name: primaryName,
|
|
953
|
+
why: corroborationNote
|
|
954
|
+
? primaryIsDiscovered
|
|
955
|
+
? `${primaryName} selected as ${corroborationNote}`
|
|
956
|
+
: `${primaryName} selected by impact×prevalence; ${corroborationNote}`
|
|
957
|
+
: `${primaryName} selected by impact×prevalence`,
|
|
958
|
+
ruledOut,
|
|
959
|
+
confidence,
|
|
960
|
+
}
|
|
961
|
+
: undefined;
|
|
962
|
+
|
|
963
|
+
// ── W12-F1 (UI-2op) + W17-C: auditable selection metadata ─────────────────────
|
|
964
|
+
// selectionRules: one card per cheap-signal + discovered candidate — score + verdict.
|
|
965
|
+
// The primary card cites WHY it won (impact×prevalence corroboration OR mechanical
|
|
966
|
+
// discovered corroboration); discovered-unevidenced cards cite the floor cap.
|
|
967
|
+
// signalSelectionTrace: a deterministic mermaid of the decision path. Pure functions
|
|
968
|
+
// of the scoring + floor outcome — NO LLM, NO clock, NO random (PR-038 / PR-049).
|
|
969
|
+
// D3: index the deep-read findings by failureOrigin.what so each selection card can
|
|
970
|
+
// link back to its finding's RCA narrative (first finding per WHAT wins — deterministic
|
|
971
|
+
// input order). Used by buildSelectionMeta to DERIVE the dataset-candidate copy.
|
|
972
|
+
const findingsByWhat = new Map<string, CensusFinding>();
|
|
973
|
+
for (const f of findings ?? []) {
|
|
974
|
+
const what = f.failureOrigin?.what;
|
|
975
|
+
if (what && !findingsByWhat.has(what)) findingsByWhat.set(what, f);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const selection = primaryName
|
|
979
|
+
? buildSelectionMeta(scored, discoveredScored, {
|
|
980
|
+
primaryName,
|
|
981
|
+
primaryIsDiscovered,
|
|
982
|
+
corroborationNote,
|
|
983
|
+
ruledOut,
|
|
984
|
+
total,
|
|
985
|
+
sampledN,
|
|
986
|
+
suspected,
|
|
987
|
+
findingsByWhat,
|
|
988
|
+
})
|
|
989
|
+
: undefined;
|
|
990
|
+
|
|
991
|
+
return Object.assign(rows, {
|
|
992
|
+
primarySignal,
|
|
993
|
+
selectionRules: selection?.selectionRules,
|
|
994
|
+
signalSelectionTrace: selection?.signalSelectionTrace,
|
|
995
|
+
suspectedPrimaryUnconfirmed: suspected,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* W17-C: merge the discovered-signal pool from deep-read findings + folded ledger
|
|
1001
|
+
* digests, deduped by WHAT. For each distinct WHAT:
|
|
1002
|
+
* - seenCount = distinct traces it was observed on (findings.sourceTraceIds ∪ one per
|
|
1003
|
+
* folded digest) — the HONEST-prevalence numerator (R6).
|
|
1004
|
+
* - origin = "deep-read" when any finding contributed it, else "ledger".
|
|
1005
|
+
* - corroboration = the resolved mechanical trajectory corroboration (R1), if any.
|
|
1006
|
+
* R2: ledger-folded WHATs re-pass the SAME floor here — never trusted blindly.
|
|
1007
|
+
* Deterministic: output ordering is the first-seen order; callers re-sort.
|
|
1008
|
+
*/
|
|
1009
|
+
function buildDiscoveredPool(
|
|
1010
|
+
findings: ReadonlyArray<CensusFinding>,
|
|
1011
|
+
foldedDigests: ReadonlyArray<DeepReadLedgerEntry>,
|
|
1012
|
+
corroborations: ReadonlyArray<TrajectoryCorroboration>
|
|
1013
|
+
): DiscoveredCandidate[] {
|
|
1014
|
+
// WHAT → set of distinct trace ids (for honest prevalence) + origin tracking.
|
|
1015
|
+
const byWhat = new Map<string, { traces: Set<string>; fromDeepRead: boolean; idx: number }>();
|
|
1016
|
+
let order = 0;
|
|
1017
|
+
const note = (what: string, traceIds: string[], fromDeepRead: boolean): void => {
|
|
1018
|
+
if (!what) return;
|
|
1019
|
+
let rec = byWhat.get(what);
|
|
1020
|
+
if (!rec) {
|
|
1021
|
+
rec = { traces: new Set<string>(), fromDeepRead: false, idx: order++ };
|
|
1022
|
+
byWhat.set(what, rec);
|
|
1023
|
+
}
|
|
1024
|
+
for (const t of traceIds) if (t) rec.traces.add(t);
|
|
1025
|
+
rec.fromDeepRead = rec.fromDeepRead || fromDeepRead;
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Source A: deep-read findings.
|
|
1029
|
+
for (const f of findings) {
|
|
1030
|
+
const what = f.failureOrigin?.what;
|
|
1031
|
+
if (!what) continue;
|
|
1032
|
+
note(what, f.sourceTraceIds ?? [], true);
|
|
1033
|
+
}
|
|
1034
|
+
// Source B (R2): folded ledger digests. Each digest = one (entity, trace) observation.
|
|
1035
|
+
for (const d of foldedDigests) {
|
|
1036
|
+
note(d.signal, [d.traceId], false);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return Array.from(byWhat.entries())
|
|
1040
|
+
.sort((a, b) => a[1].idx - b[1].idx) // stable first-seen order
|
|
1041
|
+
.map(([what, rec]) => ({
|
|
1042
|
+
what,
|
|
1043
|
+
origin: rec.fromDeepRead ? ("deep-read" as const) : ("ledger" as const),
|
|
1044
|
+
// seenCount is the distinct-trace count; ≥1 even when sourceTraceIds were absent
|
|
1045
|
+
// (a finding with no listed traces still counts as one observation).
|
|
1046
|
+
seenCount: Math.max(1, rec.traces.size),
|
|
1047
|
+
// R1/R2 evidence floor: resolve a mechanical corroboration for this WHAT.
|
|
1048
|
+
corroboration: resolveCorroboration(what, corroborations),
|
|
1049
|
+
}));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** W17-C: the resolved-scoring inputs buildSelectionMeta reads (keeps the arg list sane). */
|
|
1053
|
+
interface SelectionMetaContext {
|
|
1054
|
+
primaryName: string;
|
|
1055
|
+
/** True when the winning primary is a DISCOVERED signal (evidence-floored). */
|
|
1056
|
+
primaryIsDiscovered: boolean;
|
|
1057
|
+
corroborationNote: string;
|
|
1058
|
+
ruledOut: string[];
|
|
1059
|
+
total: number;
|
|
1060
|
+
/** R6: sampled-trace denominator for discovered prevalence cards. */
|
|
1061
|
+
sampledN: number;
|
|
1062
|
+
/** R7: the suspected-but-uncorroborated discovered signal, if any. */
|
|
1063
|
+
suspected?: SuspectedPrimary;
|
|
1064
|
+
/** D3: findings indexed by failureOrigin.what — source for dataset-candidate card copy. */
|
|
1065
|
+
findingsByWhat?: Map<string, CensusFinding>;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** D3: severity → human label for the "why high-value" rationale. */
|
|
1069
|
+
const SEVERITY_LABEL: Record<string, string> = {
|
|
1070
|
+
crit: "critical",
|
|
1071
|
+
high: "high",
|
|
1072
|
+
med: "medium",
|
|
1073
|
+
info: "low",
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* D3: derive the dataset-candidate enrichment fields for ONE selection card from its
|
|
1078
|
+
* linked finding (matched by failureOrigin.what). Pure + deterministic. Each field is
|
|
1079
|
+
* DERIVED from an existing finding field; when the source is absent the field is left
|
|
1080
|
+
* undefined (omitted gracefully by the renderer — never fabricated).
|
|
1081
|
+
*
|
|
1082
|
+
* scenario ← failureOrigin.whatHappened (plain-words trace narration)
|
|
1083
|
+
* useCase ← finding.subDesc ?? title (the edge-case it represents)
|
|
1084
|
+
* whyFailed ← why-chain ORIGIN entry .why ?? failureOrigin.why (root cause)
|
|
1085
|
+
* whyHighValue ← severity · prevalence · novelty (composed from real signals)
|
|
1086
|
+
* prevents ← finding.problem ?? title (the regression it guards against)
|
|
1087
|
+
*/
|
|
1088
|
+
function deriveCandidateEnrichment(
|
|
1089
|
+
finding: CensusFinding | undefined,
|
|
1090
|
+
opts: {
|
|
1091
|
+
/** Honest prevalence phrase (e.g. "seen in 12/40 sampled"). */
|
|
1092
|
+
prevalence: string;
|
|
1093
|
+
/** Novelty phrase (deep-read fresh vs ledger-recurring); omitted for cheap signals. */
|
|
1094
|
+
novelty?: string;
|
|
1095
|
+
}
|
|
1096
|
+
): {
|
|
1097
|
+
linkedFindingId?: string;
|
|
1098
|
+
scenario?: string;
|
|
1099
|
+
useCase?: string;
|
|
1100
|
+
whyFailed?: string;
|
|
1101
|
+
whyHighValue?: string;
|
|
1102
|
+
prevents?: string;
|
|
1103
|
+
} {
|
|
1104
|
+
if (!finding) return {};
|
|
1105
|
+
const scenario = finding.failureOrigin?.whatHappened?.trim() || undefined;
|
|
1106
|
+
const useCaseRaw = (finding.subDesc ?? finding.title)?.trim() || undefined;
|
|
1107
|
+
// Avoid echoing the scenario verbatim in the edge-case slot.
|
|
1108
|
+
const useCase = useCaseRaw && useCaseRaw !== scenario ? useCaseRaw : undefined;
|
|
1109
|
+
const originEntry = finding.whyChain?.find((w) => w.isOrigin);
|
|
1110
|
+
const whyFailed =
|
|
1111
|
+
originEntry?.why?.trim() || finding.failureOrigin?.why?.trim() || undefined;
|
|
1112
|
+
const preventsRaw = (finding.problem ?? finding.title)?.trim() || undefined;
|
|
1113
|
+
const prevents = preventsRaw && preventsRaw !== scenario ? preventsRaw : undefined;
|
|
1114
|
+
|
|
1115
|
+
// whyHighValue — compose ONLY from signals we actually have (severity · prevalence ·
|
|
1116
|
+
// novelty). Empty when none are available.
|
|
1117
|
+
const valueParts: string[] = [];
|
|
1118
|
+
if (finding.severity) valueParts.push(`${SEVERITY_LABEL[finding.severity] ?? finding.severity} severity`);
|
|
1119
|
+
if (opts.prevalence) valueParts.push(opts.prevalence);
|
|
1120
|
+
if (opts.novelty) valueParts.push(opts.novelty);
|
|
1121
|
+
const whyHighValue = valueParts.length > 0 ? valueParts.join(" · ") : undefined;
|
|
1122
|
+
|
|
1123
|
+
return {
|
|
1124
|
+
linkedFindingId: finding.findingId,
|
|
1125
|
+
scenario,
|
|
1126
|
+
useCase,
|
|
1127
|
+
whyFailed,
|
|
1128
|
+
whyHighValue,
|
|
1129
|
+
prevents,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* W12-F1 (UI-2op / PR-038 / PR-049) + W17-C: deterministically derive the per-signal
|
|
1135
|
+
* selection-rule cards + the mermaid decision-path trace from the cheap-signal scoring
|
|
1136
|
+
* AND the discovered-pool ranking. PURE — same inputs → byte-identical output.
|
|
1137
|
+
*
|
|
1138
|
+
* Cheap signals: `score` = "impact × prevalence = score" (the auditable arithmetic).
|
|
1139
|
+
* Discovered signals: `score` = "impact (discovered) · seen k/n" (impact-dominant, R3).
|
|
1140
|
+
* Verdict cites the floor outcome: ★ PRIMARY (mechanically corroborated) for the
|
|
1141
|
+
* evidence-floored winner, "secondary — discovered, unconfirmed (no mechanical evidence)"
|
|
1142
|
+
* for capped discovered signals (R7), "secondary" otherwise, "ruled-out" for benign.
|
|
1143
|
+
*/
|
|
1144
|
+
export function buildSelectionMeta(
|
|
1145
|
+
scored: Array<{ name: string; matchCount: number; score: number }>,
|
|
1146
|
+
discoveredScored: Array<{ what: string; impact: number; seenCount: number; corroboration?: ResolvedCorroboration; origin?: "deep-read" | "ledger" }>,
|
|
1147
|
+
ctx: SelectionMetaContext
|
|
1148
|
+
): SignalSelectionMeta {
|
|
1149
|
+
const { primaryName, primaryIsDiscovered, corroborationNote, ruledOut, total, sampledN, findingsByWhat } = ctx;
|
|
1150
|
+
const safeTotal = Math.max(1, total);
|
|
1151
|
+
|
|
1152
|
+
// Cheap-signal cards (impact × prevalence = score).
|
|
1153
|
+
const selectionRules: NonNullable<RunMeta["selectionRules"]> = scored.map((p) => {
|
|
1154
|
+
const impact = SIGNAL_IMPACT[p.name] ?? DEFAULT_IMPACT;
|
|
1155
|
+
const prevalencePct = round((p.matchCount / safeTotal) * 100);
|
|
1156
|
+
const isPrimary = !primaryIsDiscovered && p.name === primaryName;
|
|
1157
|
+
const verdict = isPrimary
|
|
1158
|
+
? corroborationNote.includes("deep-read")
|
|
1159
|
+
? "★ PRIMARY (corroborated by deep-read)"
|
|
1160
|
+
: "★ PRIMARY"
|
|
1161
|
+
: "secondary";
|
|
1162
|
+
// D3: link the cheap signal to its finding (by WHAT) and derive the candidate copy.
|
|
1163
|
+
const enrich = deriveCandidateEnrichment(findingsByWhat?.get(p.name), {
|
|
1164
|
+
prevalence: `seen in ${p.matchCount}/${safeTotal} traces (${prevalencePct}%)`,
|
|
1165
|
+
});
|
|
1166
|
+
return {
|
|
1167
|
+
signal: p.name,
|
|
1168
|
+
// "impact × prevalence = score" — the auditable arithmetic.
|
|
1169
|
+
score: `${impact} × ${prevalencePct}% = ${round(p.score, 2)}`,
|
|
1170
|
+
verdict,
|
|
1171
|
+
...enrich,
|
|
1172
|
+
};
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// W17-C discovered-signal cards (impact-dominant, R3; honest prevalence, R6).
|
|
1176
|
+
for (const d of discoveredScored) {
|
|
1177
|
+
const isPrimary = primaryIsDiscovered && d.what === primaryName;
|
|
1178
|
+
const evidenced = d.corroboration !== undefined;
|
|
1179
|
+
const verdict = isPrimary
|
|
1180
|
+
? "★ PRIMARY (discovered, mechanically corroborated)"
|
|
1181
|
+
: evidenced
|
|
1182
|
+
? "secondary (discovered, corroborated)"
|
|
1183
|
+
: "secondary — discovered, unconfirmed (no mechanical evidence — R1 floor)";
|
|
1184
|
+
// D3: link the discovered signal to its finding (by WHAT) and derive the candidate
|
|
1185
|
+
// copy. Novelty reflects WHERE it surfaced (fresh deep-read vs recurring ledger fold).
|
|
1186
|
+
const enrich = deriveCandidateEnrichment(findingsByWhat?.get(d.what), {
|
|
1187
|
+
prevalence: `seen in ${d.seenCount}/${sampledN} sampled`,
|
|
1188
|
+
novelty:
|
|
1189
|
+
d.origin === "ledger"
|
|
1190
|
+
? "recurring across runs (ledger-folded)"
|
|
1191
|
+
: "newly surfaced via deep-read (not a Tier-0 pattern)",
|
|
1192
|
+
});
|
|
1193
|
+
selectionRules.push({
|
|
1194
|
+
signal: d.what,
|
|
1195
|
+
// impact-dominant card: impact (discovered default ≥2) · honest sampled prevalence.
|
|
1196
|
+
score: `impact ${d.impact} (discovered) · seen ${d.seenCount}/${sampledN}`,
|
|
1197
|
+
verdict,
|
|
1198
|
+
discoveredByAwareness: true,
|
|
1199
|
+
...enrich,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Ruled-out signals get an explicit "ruled-out" card so the gate is visible.
|
|
1204
|
+
for (const name of ruledOut) {
|
|
1205
|
+
selectionRules.push({
|
|
1206
|
+
signal: name,
|
|
1207
|
+
score: "n/a (gated)",
|
|
1208
|
+
verdict: "ruled-out: benign observability artifact (failure-validity gate)",
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Mermaid decision path. W17-C extends the historic chain with the discovered pool +
|
|
1213
|
+
// the R1 evidence floor + the R7 residual branch. Deterministic string.
|
|
1214
|
+
const candidateList = scored.map((p) => p.name).join(", ") || "(none)";
|
|
1215
|
+
const discoveredList = discoveredScored.map((d) => d.what).join(", ") || "(none)";
|
|
1216
|
+
const gateNote = ruledOut.length > 0 ? `ruled out: ${ruledOut.join(", ")}` : "none ruled out";
|
|
1217
|
+
const floorNote = primaryIsDiscovered
|
|
1218
|
+
? `discovered PASSED floor: ${corroborationNote}`
|
|
1219
|
+
: ctx.suspected
|
|
1220
|
+
? `discovered CAPPED (suspected-unconfirmed: ${ctx.suspected.signal})`
|
|
1221
|
+
: "no discovered candidate";
|
|
1222
|
+
const signalSelectionTrace = [
|
|
1223
|
+
"graph TD",
|
|
1224
|
+
` A["Tier-0 candidates: ${candidateList}"] --> B["failure-validity gate (${gateNote})"]`,
|
|
1225
|
+
" B --> C[\"impact × prevalence (cheap signals)\"]",
|
|
1226
|
+
` C --> D["discovered pool: ${discoveredList}"]`,
|
|
1227
|
+
` D --> F["R1 evidence floor (${floorNote})"]`,
|
|
1228
|
+
` F --> E["★ PRIMARY: ${primaryName}"]`,
|
|
1229
|
+
].join("\n");
|
|
1230
|
+
|
|
1231
|
+
return { selectionRules, signalSelectionTrace };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// ── Scan funnel ────────────────────────────────────────────────────────────────
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* W11-03: Build the 4-stage scan-coverage funnel from tier0 + slice-plan.
|
|
1238
|
+
* Stages: total → Tier-0 scan → representative sample N → deep-read 6/N.
|
|
1239
|
+
*
|
|
1240
|
+
* The deep-read denominator is the SAMPLE size (honest: "6 of N sampled"),
|
|
1241
|
+
* not the population ("6 of total"), which was misleadingly low before.
|
|
1242
|
+
* When no representative sample metadata is available, the sample segment is
|
|
1243
|
+
* omitted and we fall back to the 3-stage display.
|
|
1244
|
+
*/
|
|
1245
|
+
export function buildScanFunnel(
|
|
1246
|
+
tier0: Tier0Input,
|
|
1247
|
+
slicePlan: SlicePlanInput,
|
|
1248
|
+
runMeta?: RunMeta
|
|
1249
|
+
): ScanFunnel {
|
|
1250
|
+
const total = tier0.totalTraces;
|
|
1251
|
+
const scanned = runMeta?.tier0ScannedCount ?? total;
|
|
1252
|
+
const llm = runMeta?.llmReadCount ?? 0;
|
|
1253
|
+
const sliceNote = slicePlan.totalSlices
|
|
1254
|
+
? `${slicePlan.totalSlices} slices · ${tier0.recommendedSlicing ?? "window-based"}`
|
|
1255
|
+
: tier0.recommendedSlicing ?? "window-based";
|
|
1256
|
+
|
|
1257
|
+
// W11-03: representative sample N (from deepRead metadata when available).
|
|
1258
|
+
// deepRead.tierReached is the LLM read ceiling; population is total scope.
|
|
1259
|
+
const sampleN = runMeta?.deepRead?.tierReached ?? 0;
|
|
1260
|
+
const hasSample = sampleN > 0;
|
|
1261
|
+
|
|
1262
|
+
// Coverage confidence annotation for the deep-read segment.
|
|
1263
|
+
const coverage = runMeta?.deepRead?.coverageConfidence;
|
|
1264
|
+
const coverageNote = coverage ? ` · coverage: ${coverage}` : "";
|
|
1265
|
+
|
|
1266
|
+
// Deep-read detail: "6 of N sampled" when sample present, else "X% of population".
|
|
1267
|
+
const llmDetail = hasSample
|
|
1268
|
+
? `${llm} of ${sampleN} sampled (100% of sample)${coverageNote}`
|
|
1269
|
+
: `${round((llm / Math.max(1, total)) * 100)}%${coverageNote}`;
|
|
1270
|
+
|
|
1271
|
+
const funnel: ScanFunnel = {
|
|
1272
|
+
total: { value: total.toLocaleString("en-US"), label: "traces in window", detail: sliceNote },
|
|
1273
|
+
code: {
|
|
1274
|
+
value: scanned.toLocaleString("en-US"),
|
|
1275
|
+
label: "code-level scan (Tier 0)",
|
|
1276
|
+
detail: `${round((scanned / Math.max(1, total)) * 100)}% · no LLM`,
|
|
1277
|
+
},
|
|
1278
|
+
llm: {
|
|
1279
|
+
value: llm.toLocaleString("en-US"),
|
|
1280
|
+
label: "LLM deep-read (analyzers)",
|
|
1281
|
+
detail: llmDetail,
|
|
1282
|
+
},
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// W11-03: attach sample segment when deepRead metadata is present.
|
|
1286
|
+
if (hasSample) {
|
|
1287
|
+
funnel.sample = {
|
|
1288
|
+
value: sampleN.toLocaleString("en-US"),
|
|
1289
|
+
label: "representative sample",
|
|
1290
|
+
detail: `${round((sampleN / Math.max(1, total)) * 100)}% of window · worst/med/best/rand`,
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return funnel;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// ── Diagnosed entity ───────────────────────────────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Resolve the diagnosed entity. R1.7: prefer the normalizer's EntityContext
|
|
1301
|
+
* (passed through transparently); fall back to findings.entities[0] only when no
|
|
1302
|
+
* EntityContext is supplied (legacy path).
|
|
1303
|
+
*
|
|
1304
|
+
* W9-08 portability: when neither EntityContext nor findings.entities is available
|
|
1305
|
+
* but findings ARE present, this is the "Path-B missing" case. The function returns
|
|
1306
|
+
* undefined and the caller's assertShapesBuildable will catch it. The silent jq
|
|
1307
|
+
* Path-B fallback (auto-construct entity from jq-extracted fields) is RETIRED —
|
|
1308
|
+
* callers MUST pass --entity-context or populate findings.entities explicitly.
|
|
1309
|
+
*/
|
|
1310
|
+
function resolveDiagnosedEntity(
|
|
1311
|
+
findings: FindingsInput,
|
|
1312
|
+
entityContext?: EntityContext
|
|
1313
|
+
): Entity | undefined {
|
|
1314
|
+
if (entityContext) return entityFromContext(entityContext);
|
|
1315
|
+
// Path-B (legacy): findings.entities[0]. Retained for backward-compat with pre-W9 inputs.
|
|
1316
|
+
// NOTE: silent jq Path-B fallback (auto-synthesize entity from jq field extraction) is
|
|
1317
|
+
// RETIRED (W9-08 portability). Provide --entity-context or populate findings.entities.
|
|
1318
|
+
return findings.entities?.[0];
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ── Assumption normalization (R1.3: legacy free-text → structured) ─────────────
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Parse a single inline VERIFIED/UNVERIFIED/HYPOTHESIS marker out of a free-text
|
|
1325
|
+
* assumption. Deterministic. When no marker is present, defaults to "unverified"
|
|
1326
|
+
* (the conservative status — an un-annotated assumption is not yet confirmed).
|
|
1327
|
+
*/
|
|
1328
|
+
function parseAssumptionStatus(text: string): Assumption["status"] {
|
|
1329
|
+
const upper = text.toUpperCase();
|
|
1330
|
+
if (/\bHYPOTHESIS\b/.test(upper)) return "hypothesis-pending";
|
|
1331
|
+
if (/\bUNVERIFIED\b|\bASSUMED\b/.test(upper)) return "unverified";
|
|
1332
|
+
if (/\bVERIFIED\b|\bCONFIRMED\b/.test(upper)) return "verified";
|
|
1333
|
+
return "unverified";
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Normalize a finding's assumptions into the structured Assumption[] shape.
|
|
1338
|
+
* Already-structured entries pass through unchanged; legacy free-text strings are
|
|
1339
|
+
* converted (status parsed from any inline marker, basis = the marker tail or a
|
|
1340
|
+
* default note). Idempotent. Strips inline HTML pills so the renderer re-adds them.
|
|
1341
|
+
*/
|
|
1342
|
+
export function normalizeAssumptions(
|
|
1343
|
+
raw: ReadonlyArray<string | Assumption> | undefined
|
|
1344
|
+
): Assumption[] | undefined {
|
|
1345
|
+
if (!raw || raw.length === 0) return undefined;
|
|
1346
|
+
return raw.map((a) => {
|
|
1347
|
+
if (typeof a !== "string") return a; // already structured
|
|
1348
|
+
const stripped = a.replace(/<[^>]*>/g, "").trim();
|
|
1349
|
+
const status = parseAssumptionStatus(stripped);
|
|
1350
|
+
// basis = text after an em-dash marker, else a generic note.
|
|
1351
|
+
const dash = stripped.indexOf("—");
|
|
1352
|
+
const basis =
|
|
1353
|
+
dash >= 0 && dash < stripped.length - 1
|
|
1354
|
+
? stripped.slice(dash + 1).trim()
|
|
1355
|
+
: "derived during RCA — not directly confirmed";
|
|
1356
|
+
const text = dash >= 0 ? stripped.slice(0, dash).trim() : stripped;
|
|
1357
|
+
return { text, status, basis };
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* PRD-CC-10: Inject a hypothesis-pending no-code-access disclaimer assumption
|
|
1363
|
+
* when entity.codeAccess === false AND finding.assumptions is null/empty.
|
|
1364
|
+
* Never overrides analyzer-emitted assumptions.
|
|
1365
|
+
*/
|
|
1366
|
+
export function synthesizeNoCodeAccessAssumption(
|
|
1367
|
+
finding: Finding,
|
|
1368
|
+
entity: EntityContext | undefined
|
|
1369
|
+
): Assumption[] | undefined {
|
|
1370
|
+
if (!entity || entity.codeAccess !== false) return finding.assumptions;
|
|
1371
|
+
const existing = finding.assumptions;
|
|
1372
|
+
if (existing && existing.length > 0) return existing;
|
|
1373
|
+
return [
|
|
1374
|
+
{
|
|
1375
|
+
text: `Source code for ${entity.name} was not provided; findings are evidence-only.`,
|
|
1376
|
+
status: "hypothesis-pending",
|
|
1377
|
+
basis: "entity.codeAccess === false — source code unavailable for direct verification",
|
|
1378
|
+
},
|
|
1379
|
+
];
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/** Apply assumption normalization + remedy-rank backfill across all findings (immutably). */
|
|
1383
|
+
/**
|
|
1384
|
+
* ② WHAT-categories whose severity is a NUMERIC anomaly and must be scored corpus-relative
|
|
1385
|
+
* rather than by absolute cutoffs (SL-8 / DS-02 G-2 fix). Scoped to latency today (the
|
|
1386
|
+
* proven DS-02 case); cost-overshoot can join once cost is a first-class corpus metric.
|
|
1387
|
+
*/
|
|
1388
|
+
const NUMERIC_ANOMALY_WHATS = new Set(["latency-spike"]);
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* ② Build the corpus latency baseline (per-agent p90) from the trace metadata. Groups by
|
|
1392
|
+
* `agentId` (the span/operation name the FI-LAT recipe compares WITHIN); traces with no
|
|
1393
|
+
* agentId share a single "__unnamed__" cohort so they still get a corpus-relative read.
|
|
1394
|
+
* Returns null when the corpus carries no latency samples → callers skip calibration.
|
|
1395
|
+
*/
|
|
1396
|
+
function buildLatencyBaseline(metadata: MetadataInput[]): CorpusBaseline | null {
|
|
1397
|
+
const samples: NameValue[] = [];
|
|
1398
|
+
for (const t of metadata) {
|
|
1399
|
+
if (t.latencyMs === undefined) continue;
|
|
1400
|
+
samples.push({ name: t.agentId ?? "__unnamed__", value: t.latencyMs });
|
|
1401
|
+
}
|
|
1402
|
+
if (samples.length === 0) return null;
|
|
1403
|
+
return computeCorpusBaseline(samples);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* ② Re-score a numeric-anomaly finding's severity against the corpus baseline. Gathers the
|
|
1408
|
+
* finding's source spans (agentId + latencyMs from metadata) and takes the MAX-severity
|
|
1409
|
+
* corpus-relative verdict. Leaves severity untouched when the finding is not a numeric
|
|
1410
|
+
* anomaly, has no scorable source spans, or there is no corpus baseline. This DOWNGRADES a
|
|
1411
|
+
* latency that IS the corpus baseline (the G-2 over-alarm) and still promotes a genuine
|
|
1412
|
+
* 3×-p90+floor outlier.
|
|
1413
|
+
*/
|
|
1414
|
+
function calibrateNumericSeverity(
|
|
1415
|
+
finding: Finding,
|
|
1416
|
+
baseline: CorpusBaseline | null,
|
|
1417
|
+
latencyByTraceId: Map<string, number>,
|
|
1418
|
+
agentByTraceId: Map<string, string>
|
|
1419
|
+
): Finding {
|
|
1420
|
+
if (!baseline) return finding;
|
|
1421
|
+
const what = finding.failureOrigin?.what;
|
|
1422
|
+
if (!what || !NUMERIC_ANOMALY_WHATS.has(what)) return finding;
|
|
1423
|
+
|
|
1424
|
+
const spans: NameValue[] = [];
|
|
1425
|
+
for (const id of finding.sourceTraceIds ?? []) {
|
|
1426
|
+
const value = latencyByTraceId.get(id);
|
|
1427
|
+
if (value === undefined) continue;
|
|
1428
|
+
spans.push({ name: agentByTraceId.get(id) ?? "__unnamed__", value });
|
|
1429
|
+
}
|
|
1430
|
+
const verdict = calibrateFromSpans(spans, baseline);
|
|
1431
|
+
if (!verdict) return finding;
|
|
1432
|
+
return { ...finding, severity: verdict.severity };
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function normalizeFindings(
|
|
1436
|
+
findings: Finding[],
|
|
1437
|
+
entityContext?: EntityContext,
|
|
1438
|
+
metadata: MetadataInput[] = []
|
|
1439
|
+
): Finding[] {
|
|
1440
|
+
// ② Precompute the corpus latency baseline + per-trace lookups once for the whole batch.
|
|
1441
|
+
const latencyBaseline = buildLatencyBaseline(metadata);
|
|
1442
|
+
const latencyByTraceId = new Map<string, number>();
|
|
1443
|
+
const agentByTraceId = new Map<string, string>();
|
|
1444
|
+
for (const t of metadata) {
|
|
1445
|
+
if (t.latencyMs !== undefined) latencyByTraceId.set(t.traceId, t.latencyMs);
|
|
1446
|
+
if (t.agentId !== undefined) agentByTraceId.set(t.traceId, t.agentId);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return findings.map((original) => {
|
|
1450
|
+
// ② Corpus-relative severity FIRST so downstream derivations read the calibrated grade.
|
|
1451
|
+
const f = calibrateNumericSeverity(
|
|
1452
|
+
original,
|
|
1453
|
+
latencyBaseline,
|
|
1454
|
+
latencyByTraceId,
|
|
1455
|
+
agentByTraceId
|
|
1456
|
+
);
|
|
1457
|
+
const assumptions = normalizeAssumptions(
|
|
1458
|
+
f.assumptions as ReadonlyArray<string | Assumption> | undefined
|
|
1459
|
+
);
|
|
1460
|
+
const withNormalized = assumptions ? { ...f, assumptions } : f;
|
|
1461
|
+
// PRD-CC-10: inject no-code-access disclaimer when needed.
|
|
1462
|
+
// W12-08: Finding.assumptions is now required (≥1). synthesizeNoCodeAccessAssumption
|
|
1463
|
+
// may still return undefined for the legacy/empty path; coerce to the original
|
|
1464
|
+
// (now-required) array so the enriched Finding keeps a non-undefined assumptions.
|
|
1465
|
+
const synthesized =
|
|
1466
|
+
synthesizeNoCodeAccessAssumption(withNormalized, entityContext) ??
|
|
1467
|
+
withNormalized.assumptions;
|
|
1468
|
+
const withAssumptions =
|
|
1469
|
+
synthesized !== withNormalized.assumptions
|
|
1470
|
+
? { ...withNormalized, assumptions: synthesized }
|
|
1471
|
+
: withNormalized;
|
|
1472
|
+
|
|
1473
|
+
// W13-C (D-1): deterministically derive remedy.rank from cost × correctness
|
|
1474
|
+
// (orchestrator-protocol §8 — finally implemented in code). This closes the
|
|
1475
|
+
// last leg of the D-1 contract triad: cost/correctness are analyzer-required
|
|
1476
|
+
// (gate-enforced at Step 7.1), rank is ALWAYS backfilled here. So a remedy can
|
|
1477
|
+
// never reach the renderer with rank/cost/correctness undefined. The backfill
|
|
1478
|
+
// is reproducible (no LLM judgment), removing an agent-discretion variance source.
|
|
1479
|
+
return Array.isArray(withAssumptions.remedies) && withAssumptions.remedies.length > 0
|
|
1480
|
+
? { ...withAssumptions, remedies: rankRemedies(withAssumptions.remedies) }
|
|
1481
|
+
: withAssumptions;
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// ── Fail-loud predicate ────────────────────────────────────────────────────────
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* R1 §9.3 + W12-06: fail-loud guard over the 4 internal render shapes.
|
|
1489
|
+
*
|
|
1490
|
+
* TWO-tier threshold — the shapes are NOT all equivalent:
|
|
1491
|
+
*
|
|
1492
|
+
* - `diagnosedEntity` is INDIVIDUALLY REQUIRED. A run WITH findings always has a
|
|
1493
|
+
* diagnosed subject; a missing entity yields an empty entity card despite the
|
|
1494
|
+
* "no silent placeholder" promise (W12-06 / OP-7). So a lone missing entity
|
|
1495
|
+
* (1-of-4) MUST refuse — it can never be a legitimate empty on a findings run.
|
|
1496
|
+
*
|
|
1497
|
+
* - `bigStat` / `hourlyHeatmap` / `signalCensus` can be LEGITIMATELY empty on a
|
|
1498
|
+
* clean no-signal run (no latencies → no big-stat; no startTimes → empty
|
|
1499
|
+
* heatmap; no patterns → empty census). These keep the COLLECTIVE ≥3-of-4
|
|
1500
|
+
* threshold: 3+ simultaneously empty signals a genuinely starved input, but any
|
|
1501
|
+
* one alone is tolerated.
|
|
1502
|
+
*
|
|
1503
|
+
* Mirrors the renderer's guard so the enricher fails BEFORE handing a starved
|
|
1504
|
+
* input downstream.
|
|
1505
|
+
*/
|
|
1506
|
+
function assertShapesBuildable(
|
|
1507
|
+
shapes: {
|
|
1508
|
+
diagnosedEntity?: Entity;
|
|
1509
|
+
bigStat: BigStat[];
|
|
1510
|
+
hourlyHeatmap: HourlyHeatmap;
|
|
1511
|
+
signalCensus: SignalCensusRow[];
|
|
1512
|
+
},
|
|
1513
|
+
hasFindings: boolean
|
|
1514
|
+
): void {
|
|
1515
|
+
if (!hasFindings) return;
|
|
1516
|
+
|
|
1517
|
+
// W12-06: diagnosedEntity is individually required — refuse on its own absence,
|
|
1518
|
+
// independent of the ≥3-of-4 count. A findings run always has a diagnosed entity;
|
|
1519
|
+
// its absence is never a legitimate empty.
|
|
1520
|
+
if (!shapes.diagnosedEntity) {
|
|
1521
|
+
throw new Error(
|
|
1522
|
+
`build-render-input: refusing to enrich — diagnosedEntity is missing on a run ` +
|
|
1523
|
+
`that HAS findings. A findings run always has a diagnosed subject; an empty ` +
|
|
1524
|
+
`entity card is forbidden (no silent placeholder). Pass --entity-context or ` +
|
|
1525
|
+
`populate findings.entities. Fail-loud (W12-06 / OP-7).`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// R1 §9.3: the other three may each be legitimately empty on a clean run; only
|
|
1530
|
+
// a ≥3-of-4 collective miss signals a genuinely starved input.
|
|
1531
|
+
const missing: string[] = [];
|
|
1532
|
+
if (shapes.bigStat.length === 0) missing.push("bigStat");
|
|
1533
|
+
if (shapes.hourlyHeatmap.cells.every((c) => c.count === 0)) missing.push("hourlyHeatmap");
|
|
1534
|
+
if (shapes.signalCensus.length === 0) missing.push("signalCensus");
|
|
1535
|
+
// diagnosedEntity is present here (guarded above) — count it among the 4 so the
|
|
1536
|
+
// threshold semantics ("3 of 4") stay intact even though it can't be the missing one.
|
|
1537
|
+
if (missing.length >= 3) {
|
|
1538
|
+
throw new Error(
|
|
1539
|
+
`build-render-input: refusing to enrich — ${missing.length} of 4 internal render ` +
|
|
1540
|
+
`shapes could not be built (${missing.join(", ")}). Inputs are starved: ` +
|
|
1541
|
+
`check tier0/metadata/findings. Fail-loud (R1 §9.3) — no silent placeholder.`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* W9-08 (R-CP-2): Assert RunMeta required fields are present before enrichment.
|
|
1549
|
+
* Fails loud when findings are present but runMeta is absent or missing key fields.
|
|
1550
|
+
* Required fields: totalTraces, tier0ScannedCount, llmReadCount, scopeFilter,
|
|
1551
|
+
* samplingStrategy, decisions, deepRead.
|
|
1552
|
+
*
|
|
1553
|
+
* NOTE: all RunMeta fields are optional for backward-compat. This check only fires
|
|
1554
|
+
* when findings are present — empty-findings runs are exempt (no run = no meta).
|
|
1555
|
+
*/
|
|
1556
|
+
function assertRunMetaPresent(runMeta: RunMeta | undefined, hasFindings: boolean): void {
|
|
1557
|
+
if (!hasFindings) return;
|
|
1558
|
+
if (!runMeta) {
|
|
1559
|
+
// Soft warning — RunMeta absent on pre-Wave-9 runs is expected; not a hard fail.
|
|
1560
|
+
// Hard fail only when a specific required field is missing on a run that SHOULD have it.
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const missing: string[] = [];
|
|
1564
|
+
if (runMeta.totalTraces === undefined) missing.push("runMeta.totalTraces");
|
|
1565
|
+
if (runMeta.tier0ScannedCount === undefined) missing.push("runMeta.tier0ScannedCount");
|
|
1566
|
+
if (runMeta.llmReadCount === undefined) missing.push("runMeta.llmReadCount");
|
|
1567
|
+
if (missing.length > 0) {
|
|
1568
|
+
// These three are mandatory on Wave-9+ runs — fail loud to catch misconfigured enrichers.
|
|
1569
|
+
throw new Error(
|
|
1570
|
+
`build-render-input: RunMeta is present but missing required fields ` +
|
|
1571
|
+
`(${missing.join(", ")}). Check the orchestrator's Step 8.5 enricher wiring. ` +
|
|
1572
|
+
`Fail-loud (W9-08 R-CP-2) — no silent placeholder fallback.`
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// ── S7-render: enricher-output self-validation ───────────────────────────────────
|
|
1578
|
+
|
|
1579
|
+
/** Local non-empty-string check (enricher-scoped; mirrors the validators' helper). */
|
|
1580
|
+
function isNonEmptyEnrichString(v: unknown): boolean {
|
|
1581
|
+
return typeof v === "string" && v.trim() !== "";
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* S7-render (Wave-15 Block B, operator option B): self-validate the assembled
|
|
1586
|
+
* RenderInput BEFORE it is returned to the orchestrator.
|
|
1587
|
+
*
|
|
1588
|
+
* WHY: previously the only RenderInput-shape gates were render.ts's
|
|
1589
|
+
* `validatePreRender` / `assertRenderShapesPresent`, which run AFTER 23×
|
|
1590
|
+
* `template.replaceAll()`. A field the enricher dropped (or built with the wrong
|
|
1591
|
+
* type) therefore reached broken markup before any gate fired. This guard moves
|
|
1592
|
+
* the catch UPSTREAM to the PRODUCER: the enricher asserts its own output shape —
|
|
1593
|
+
* every required RenderInput field present with the correct type — and THROWS
|
|
1594
|
+
* before returning, so a dropped field is caught pre-template at the producer.
|
|
1595
|
+
*
|
|
1596
|
+
* SCOPE: asserts the type-level required RenderInput contract (the six
|
|
1597
|
+
* non-optional members) plus, on a findings-bearing run, the internal Overview
|
|
1598
|
+
* shapes render.ts dereferences without a guard (diagnosedEntity / bigStat /
|
|
1599
|
+
* signalCensus / hourlyHeatmap.cells). It COMPLEMENTS — never replaces —
|
|
1600
|
+
* `assertShapesBuildable` (input-starvation predicate) and `assertRunMetaPresent`
|
|
1601
|
+
* (runMeta field presence): those gate the INPUTS, this gates the OUTPUT.
|
|
1602
|
+
*
|
|
1603
|
+
* DETERMINISTIC: pure structural assertion, no I/O.
|
|
1604
|
+
*/
|
|
1605
|
+
function assertRenderInputShape(input: RenderInput): void {
|
|
1606
|
+
const missing: string[] = [];
|
|
1607
|
+
|
|
1608
|
+
// ── Type-required RenderInput fields (the six non-optional members) ──────────
|
|
1609
|
+
if (!isNonEmptyEnrichString(input.sessionId)) missing.push("sessionId (non-empty string)");
|
|
1610
|
+
if (!isNonEmptyEnrichString(input.diagnosedAt)) missing.push("diagnosedAt (non-empty string)");
|
|
1611
|
+
if (!isNonEmptyEnrichString(input.sourcePlatform)) missing.push("sourcePlatform (non-empty string)");
|
|
1612
|
+
if (!isNonEmptyEnrichString(input.targetPlatform)) missing.push("targetPlatform (non-empty string)");
|
|
1613
|
+
if (typeof input.totalTraces !== "number" || Number.isNaN(input.totalTraces)) {
|
|
1614
|
+
missing.push("totalTraces (number)");
|
|
1615
|
+
}
|
|
1616
|
+
if (!Array.isArray(input.findings)) missing.push("findings (array)");
|
|
1617
|
+
|
|
1618
|
+
// ── Findings-bearing runs: the internal shapes render.ts dereferences raw ────
|
|
1619
|
+
// Empty-findings runs are exempt — a zero-finding report legitimately omits the
|
|
1620
|
+
// Overview tiles (mirrors assertShapesBuildable / completeness-check semantics).
|
|
1621
|
+
if (Array.isArray(input.findings) && input.findings.length > 0) {
|
|
1622
|
+
if (!input.diagnosedEntity) missing.push("diagnosedEntity (Entity)");
|
|
1623
|
+
if (!Array.isArray(input.bigStat)) missing.push("bigStat (BigStat[])");
|
|
1624
|
+
if (!Array.isArray(input.signalCensus)) missing.push("signalCensus (SignalCensusRow[])");
|
|
1625
|
+
if (!input.hourlyHeatmap || !Array.isArray(input.hourlyHeatmap.cells)) {
|
|
1626
|
+
missing.push("hourlyHeatmap.cells (HourlyHeatCell[])");
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (missing.length > 0) {
|
|
1631
|
+
throw new Error(
|
|
1632
|
+
`build-render-input: refusing to return a malformed RenderInput — ` +
|
|
1633
|
+
`${missing.length} required field(s) absent or wrong-typed (${missing.join(", ")}). ` +
|
|
1634
|
+
`A producer-side drop must be caught HERE (S7-render), before the renderer's ` +
|
|
1635
|
+
`template.replaceAll() reaches broken markup. Fail-loud — no silent placeholder.`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// ── Main entry ─────────────────────────────────────────────────────────────────
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* Build a fully-populated RenderInput from the 4 deterministic inputs.
|
|
1644
|
+
* DETERMINISTIC + IDEMPOTENT: same inputs (+ same opts.generatedAt) → byte-identical
|
|
1645
|
+
* RenderInput. Fail-loud when the inputs are starved.
|
|
1646
|
+
*/
|
|
1647
|
+
export function buildRenderInput(
|
|
1648
|
+
inputs: EnricherInputs,
|
|
1649
|
+
opts: EnricherOptions
|
|
1650
|
+
): RenderInput {
|
|
1651
|
+
const { tier0, slicePlan, findings, metadata } = inputs;
|
|
1652
|
+
|
|
1653
|
+
// SD self-diag: when config.self_diagnostics.enabled, the report diagnoses the
|
|
1654
|
+
// diagnostics skill ITSELF — force meta-report mode (cluster-grouped findings),
|
|
1655
|
+
// internal-only audience (PR-022), a [INTERNAL] sessionId prefix, and a
|
|
1656
|
+
// skill-typed entity. These overrides happen BEFORE the shapes are built so the
|
|
1657
|
+
// whole render input is consistent.
|
|
1658
|
+
const isSelfDiag = opts.selfDiag?.enabled === true;
|
|
1659
|
+
const isMetaReport = isSelfDiag ? true : findings.isMetaReport;
|
|
1660
|
+
const sessionId = isSelfDiag && !findings.sessionId.startsWith("[INTERNAL]")
|
|
1661
|
+
? `[INTERNAL] ${findings.sessionId}`
|
|
1662
|
+
: findings.sessionId;
|
|
1663
|
+
|
|
1664
|
+
// R1.3: normalize any legacy free-text assumptions → structured Assumption[].
|
|
1665
|
+
// PRD-CC-10: pass entityContext for no-code-access disclaimer synthesis.
|
|
1666
|
+
const entityContextForNorm = isSelfDiag && opts.selfDiag?.skillEntity
|
|
1667
|
+
? opts.selfDiag.skillEntity
|
|
1668
|
+
: opts.entityContext;
|
|
1669
|
+
// ② pass metadata so numeric-anomaly severity is calibrated corpus-relative (G-2 fix).
|
|
1670
|
+
const normalizedFindings = normalizeFindings(findings.findings, entityContextForNorm, metadata);
|
|
1671
|
+
|
|
1672
|
+
// ── DISMISSAL FINAL-CHECK (hidden suppression) ────────────────────────────────
|
|
1673
|
+
// When a dismissalContext is injected, findings that semantically match a valid prior
|
|
1674
|
+
// DISMISSAL for this entity are REMOVED here (never rendered — no "Suppressed" section;
|
|
1675
|
+
// the verdict ledger is the audit trail). The severity-escalation HARD GUARD inside the
|
|
1676
|
+
// partition runs BEFORE the semantic match (a dismissed-but-now-worse finding always
|
|
1677
|
+
// re-surfaces). PURE + deterministic: the reasoning verdicts are pre-computed upstream
|
|
1678
|
+
// (host-runtime, pinned temp-0). Absent ⇒ no-op (activeFindings === normalizedFindings).
|
|
1679
|
+
const activeFindings = opts.dismissalContext
|
|
1680
|
+
? partitionByDismissal(
|
|
1681
|
+
normalizedFindings,
|
|
1682
|
+
opts.dismissalContext.dismissedEntries,
|
|
1683
|
+
opts.dismissalContext.verdicts,
|
|
1684
|
+
{ entitySlug: opts.dismissalContext.entitySlug }
|
|
1685
|
+
).active
|
|
1686
|
+
: normalizedFindings;
|
|
1687
|
+
|
|
1688
|
+
const bigStat = buildBigStat(metadata, tier0);
|
|
1689
|
+
// W11-01: pass the ACTIVE (post-dismissal) findings for deep-read corroboration
|
|
1690
|
+
// (PR-049 step 3). activeFindings === normalizedFindings when no dismissal suppression
|
|
1691
|
+
// fired, so this is byte-unchanged for every non-dismissal run.
|
|
1692
|
+
// F3: build the census BEFORE the heatmap so the resolved primarySignal can key
|
|
1693
|
+
// the heatmap's metric + colour (cell metric FOLLOWS the primary signal).
|
|
1694
|
+
const signalCensus = buildSignalCensus(
|
|
1695
|
+
tier0,
|
|
1696
|
+
tier0.totalTraces || metadata.length || 1,
|
|
1697
|
+
activeFindings,
|
|
1698
|
+
// W17-C: discovered-signal context (corroborations · foldedDigests · sampledCount).
|
|
1699
|
+
// Absent until Block B/D1 wire it upstream → discovered signals stay capped at
|
|
1700
|
+
// SECONDARY (suspected-unconfirmed), never silently crowned.
|
|
1701
|
+
opts.signalCtx
|
|
1702
|
+
);
|
|
1703
|
+
// F3 (UR-2): heatmap metric follows the primary signal (default = latency, so a
|
|
1704
|
+
// latency-primary run is byte-identical to the pre-F3 behaviour).
|
|
1705
|
+
const hourlyHeatmap = aggregateHourlyHeatmap(
|
|
1706
|
+
metadata,
|
|
1707
|
+
signalCensus.primarySignal?.name
|
|
1708
|
+
);
|
|
1709
|
+
const scanFunnel = buildScanFunnel(tier0, slicePlan, findings.runMeta);
|
|
1710
|
+
// R1.7 + SD: self-diag uses the skill-typed EntityContext; otherwise the
|
|
1711
|
+
// normalizer's EntityContext (or legacy findings.entities[0]).
|
|
1712
|
+
const diagnosedEntity = isSelfDiag && opts.selfDiag?.skillEntity
|
|
1713
|
+
? entityFromContext(opts.selfDiag.skillEntity)
|
|
1714
|
+
: resolveDiagnosedEntity(findings, opts.entityContext);
|
|
1715
|
+
|
|
1716
|
+
assertShapesBuildable(
|
|
1717
|
+
{ diagnosedEntity, bigStat, hourlyHeatmap, signalCensus },
|
|
1718
|
+
activeFindings.length > 0
|
|
1719
|
+
);
|
|
1720
|
+
|
|
1721
|
+
// W9-08 (R-CP-2): assert RunMeta required fields when present + findings non-empty.
|
|
1722
|
+
// Uses the ACTIVE set: an all-suppressed run renders as a clean (zero-finding) report.
|
|
1723
|
+
assertRunMetaPresent(findings.runMeta, activeFindings.length > 0);
|
|
1724
|
+
|
|
1725
|
+
// ── W12-F1/F4 + W17-C: assemble the enriched runMeta ──────────────────────────
|
|
1726
|
+
// PRESERVE all upstream/LLM-produced fields from findings.runMeta — including the
|
|
1727
|
+
// threaded methodology widgets (tierBreakdown · blindSpots · awarenessSample) the
|
|
1728
|
+
// orchestrator assigned pre-enrich (codified in orchestrator-protocol Step 8.5).
|
|
1729
|
+
// The enricher NEVER recomputes those (they need an LLM); it only adds the fields
|
|
1730
|
+
// it can derive deterministically from the census scoring: primarySignal +
|
|
1731
|
+
// selectionRules + signalSelectionTrace (F1 / PR-038 / PR-049) + (W17-C, R7)
|
|
1732
|
+
// suspectedPrimaryUnconfirmed. The spread is unconditional so widget fields are
|
|
1733
|
+
// never dropped on a no-primary run.
|
|
1734
|
+
const enrichedRunMeta: RunMetaW17 | undefined = findings.runMeta
|
|
1735
|
+
? {
|
|
1736
|
+
...findings.runMeta,
|
|
1737
|
+
...(signalCensus.primarySignal
|
|
1738
|
+
? { primarySignal: signalCensus.primarySignal }
|
|
1739
|
+
: {}),
|
|
1740
|
+
// F1: enricher-computed selection metadata wins over any stale upstream copy
|
|
1741
|
+
// (they are deterministically re-derived from this run's census scoring).
|
|
1742
|
+
...(signalCensus.selectionRules
|
|
1743
|
+
? { selectionRules: signalCensus.selectionRules }
|
|
1744
|
+
: {}),
|
|
1745
|
+
...(signalCensus.signalSelectionTrace !== undefined
|
|
1746
|
+
? { signalSelectionTrace: signalCensus.signalSelectionTrace }
|
|
1747
|
+
: {}),
|
|
1748
|
+
// W17-C (R7): residual surfacing. When the top discovered signal is capped at
|
|
1749
|
+
// SECONDARY for lack of mechanical evidence and primary fell back to a cheap
|
|
1750
|
+
// signal, thread the flag so Block E (renderer) shows "suspected primary —
|
|
1751
|
+
// unconfirmed" instead of silently crowning the cheap signal.
|
|
1752
|
+
...(signalCensus.suspectedPrimaryUnconfirmed
|
|
1753
|
+
? { suspectedPrimaryUnconfirmed: signalCensus.suspectedPrimaryUnconfirmed }
|
|
1754
|
+
: {}),
|
|
1755
|
+
}
|
|
1756
|
+
: findings.runMeta;
|
|
1757
|
+
|
|
1758
|
+
const renderInput: RenderInput = {
|
|
1759
|
+
sessionId,
|
|
1760
|
+
diagnosedAt: findings.diagnosedAt,
|
|
1761
|
+
sourcePlatform: findings.sourcePlatform,
|
|
1762
|
+
targetPlatform: findings.targetPlatform,
|
|
1763
|
+
totalTraces: findings.totalTraces || metadata.length,
|
|
1764
|
+
// ACTIVE findings only — semantically-dismissed findings are removed (hidden). When no
|
|
1765
|
+
// dismissalContext is injected this is byte-identical to the normalized set.
|
|
1766
|
+
findings: activeFindings,
|
|
1767
|
+
generatedAt: opts.generatedAt,
|
|
1768
|
+
// SD/PR-022: meta-reports are ALWAYS internal — never strippable.
|
|
1769
|
+
audience: isMetaReport ? "internal" : findings.audience,
|
|
1770
|
+
isMetaReport,
|
|
1771
|
+
// Internal render shapes (R1.3/R1.7):
|
|
1772
|
+
diagnosedEntity,
|
|
1773
|
+
bigStat,
|
|
1774
|
+
signalCensus,
|
|
1775
|
+
scanFunnel,
|
|
1776
|
+
hourlyHeatmap,
|
|
1777
|
+
// Story-led copy (passed through verbatim when present):
|
|
1778
|
+
// W11-01/F1/F4: enriched runMeta — primarySignal + selectionRules +
|
|
1779
|
+
// signalSelectionTrace injected; upstream widget fields preserved (see above).
|
|
1780
|
+
runMeta: enrichedRunMeta,
|
|
1781
|
+
entities: findings.entities,
|
|
1782
|
+
decisionLog: findings.decisionLog ?? mapDecisionsToLog(findings.runMeta?.decisions),
|
|
1783
|
+
mermaidSequence: findings.mermaidSequence ?? findings.runMeta?.mermaidTopology,
|
|
1784
|
+
headerTitle: findings.headerTitle,
|
|
1785
|
+
overviewTitle: findings.overviewTitle,
|
|
1786
|
+
overviewSub: findings.overviewSub,
|
|
1787
|
+
overviewHeadline: findings.overviewHeadline,
|
|
1788
|
+
overviewLeverage: findings.overviewLeverage,
|
|
1789
|
+
decisionsBundle: findings.decisionsBundle,
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// S7-render: self-validate the assembled output shape BEFORE returning, so a
|
|
1793
|
+
// dropped/wrong-typed field is caught at the producer — never reaching the
|
|
1794
|
+
// renderer's template.replaceAll() and broken markup.
|
|
1795
|
+
assertRenderInputShape(renderInput);
|
|
1796
|
+
|
|
1797
|
+
return renderInput;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Minimal escaper for the decision-log grade badge text.
|
|
1802
|
+
* W13-C (D-5): hardened with `String(s ?? "")` — a decision row with an absent
|
|
1803
|
+
* rationale/reason field previously crashed on `s.slice` (hard render-input failure).
|
|
1804
|
+
* Now an absent field degrades to an empty badge instead of crashing.
|
|
1805
|
+
*/
|
|
1806
|
+
function escapeBadge(s: unknown): string {
|
|
1807
|
+
return String(s ?? "")
|
|
1808
|
+
.slice(0, 48)
|
|
1809
|
+
.replace(/</g, "<")
|
|
1810
|
+
.replace(/>/g, ">");
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* W13-C (D-5): map RunMeta.decisions[] → DecisionLogRow[] for the Methodology tab.
|
|
1815
|
+
*
|
|
1816
|
+
* Tolerates the producer/consumer field-name desync between the canonical decision
|
|
1817
|
+
* shape `{step, choice, rationale}` (scripts/normalize/trace.ts RunMeta.decisions)
|
|
1818
|
+
* and the alias names `{stepId, decision, reason}` that appeared in some
|
|
1819
|
+
* orchestrator-protocol examples. An orchestrator that followed those examples
|
|
1820
|
+
* literally would otherwise produce all-undefined rows (and crash escapeBadge).
|
|
1821
|
+
* We read the canonical name first, then the alias, so BOTH shapes render correctly.
|
|
1822
|
+
*
|
|
1823
|
+
* Pure + deterministic. Returns undefined when there are no decisions (preserves the
|
|
1824
|
+
* prior `?.map` semantics so the field stays absent rather than an empty array).
|
|
1825
|
+
*/
|
|
1826
|
+
export function mapDecisionsToLog(
|
|
1827
|
+
decisions: ReadonlyArray<Record<string, unknown>> | undefined
|
|
1828
|
+
): DecisionLogRow[] | undefined {
|
|
1829
|
+
if (!decisions || decisions.length === 0) return undefined;
|
|
1830
|
+
return decisions.map((d) => ({
|
|
1831
|
+
// canonical `step` → alias `stepId`
|
|
1832
|
+
decision: String(d.step ?? d.stepId ?? ""),
|
|
1833
|
+
// canonical `choice` → alias `decision`
|
|
1834
|
+
what: String(d.choice ?? d.decision ?? ""),
|
|
1835
|
+
// canonical `rationale` → alias `reason`; escapeBadge guards undefined either way
|
|
1836
|
+
grade: `<span class="badge b-info">${escapeBadge(d.rationale ?? d.reason)}</span>`,
|
|
1837
|
+
}));
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// ── CLI entrypoint ─────────────────────────────────────────────────────────────
|
|
1841
|
+
|
|
1842
|
+
if (import.meta.main) {
|
|
1843
|
+
const { readFileSync, writeFileSync } = await import("fs");
|
|
1844
|
+
const { resolve } = await import("path");
|
|
1845
|
+
|
|
1846
|
+
const argv = process.argv.slice(2);
|
|
1847
|
+
const get = (flag: string): string | undefined => {
|
|
1848
|
+
const i = argv.indexOf(flag);
|
|
1849
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
const tier0Path = get("--tier0");
|
|
1853
|
+
const slicePath = get("--slice-plan");
|
|
1854
|
+
const findingsPath = get("--findings");
|
|
1855
|
+
const metadataPath = get("--metadata");
|
|
1856
|
+
const outPath = get("--output");
|
|
1857
|
+
const generatedAt = get("--generated-at") ?? "1970-01-01T00:00:00.000Z";
|
|
1858
|
+
// PRD-CC-10: --entity-context takes precedence over findings.entities[0].
|
|
1859
|
+
const entityContextPath = get("--entity-context");
|
|
1860
|
+
// SD-3: --self-diag turns on the meta-report path (isMetaReport + internal
|
|
1861
|
+
// audience + [INTERNAL] sessionId prefix + skill-typed entity). It pairs with
|
|
1862
|
+
// --skill-entity <path>, an EntityContext JSON produced by the claude-code
|
|
1863
|
+
// normalizer's self-diag CLI (SD-2: `--mode self-diag --out-entity`). Without
|
|
1864
|
+
// the CLI flag this path was programmatic-only (EnricherOptions.selfDiag),
|
|
1865
|
+
// so the self-diag report could not run end-to-end via run.sh.
|
|
1866
|
+
const selfDiag = argv.includes("--self-diag");
|
|
1867
|
+
const skillEntityPath = get("--skill-entity");
|
|
1868
|
+
// W17-WIRING: --signal-ctx <path> threads the discovered-signal SELECTION-HUB
|
|
1869
|
+
// context (SignalCensusContext) into buildSignalCensus end-to-end. The orchestrator
|
|
1870
|
+
// assembles this JSON at Step 6.5 from THREE deterministic sources:
|
|
1871
|
+
// • corroborations — scripts/scan/trajectory.ts CLI over the sampled deep-read bodies
|
|
1872
|
+
// • foldedDigests — store.foldValidDigests(entity, …) over the entity's ledger
|
|
1873
|
+
// • sampledCount — the honest deep-read denominator (R6)
|
|
1874
|
+
// Additive + safe-by-default (Zone-1.5): flag ABSENT → signalCtx undefined → legacy
|
|
1875
|
+
// Tier-0-only census → discovered signals stay suspected-unconfirmed (never crowned).
|
|
1876
|
+
const signalCtxPath = get("--signal-ctx");
|
|
1877
|
+
// DISMISSAL FINAL-CHECK wiring: --dismissal-context <path> threads the Step 8.5b
|
|
1878
|
+
// suppression context — a JSON `{ entitySlug, dismissedEntries, verdicts }` where the
|
|
1879
|
+
// reasoning verdicts were computed UPSTREAM (host-runtime, pinned temp-0). Additive +
|
|
1880
|
+
// safe-by-default: flag ABSENT → no suppression → byte-unchanged output.
|
|
1881
|
+
const dismissalContextPath = get("--dismissal-context");
|
|
1882
|
+
|
|
1883
|
+
if (!tier0Path || !slicePath || !findingsPath || !metadataPath || !outPath) {
|
|
1884
|
+
process.stderr.write(
|
|
1885
|
+
"Usage: bun scripts/enrich/build-render-input.ts --tier0 <f> --slice-plan <f> " +
|
|
1886
|
+
"--findings <f> --metadata <f> --output <f> [--generated-at <iso>] [--entity-context <f>] " +
|
|
1887
|
+
"[--signal-ctx <f>] [--dismissal-context <f>] [--self-diag --skill-entity <f>]\n"
|
|
1888
|
+
);
|
|
1889
|
+
process.exit(1);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// SD-3: --self-diag and --skill-entity are paired — neither is useful alone.
|
|
1893
|
+
// --skill-entity without --self-diag would load a skill entity the enricher
|
|
1894
|
+
// ignores (selfDiag gate is off); --self-diag without --skill-entity would
|
|
1895
|
+
// synthesize a skill-typed card from findings only, defeating the SD-2 → SD-3
|
|
1896
|
+
// entity handoff. Fail loud rather than silently degrade.
|
|
1897
|
+
if (selfDiag && !skillEntityPath) {
|
|
1898
|
+
process.stderr.write("Error: --self-diag requires --skill-entity <path>\n");
|
|
1899
|
+
process.exit(1);
|
|
1900
|
+
}
|
|
1901
|
+
if (skillEntityPath && !selfDiag) {
|
|
1902
|
+
process.stderr.write("Error: --skill-entity requires --self-diag\n");
|
|
1903
|
+
process.exit(1);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
try {
|
|
1907
|
+
const inputs: EnricherInputs = {
|
|
1908
|
+
tier0: JSON.parse(readFileSync(resolve(tier0Path), "utf8")),
|
|
1909
|
+
slicePlan: JSON.parse(readFileSync(resolve(slicePath), "utf8")),
|
|
1910
|
+
findings: JSON.parse(readFileSync(resolve(findingsPath), "utf8")),
|
|
1911
|
+
metadata: JSON.parse(readFileSync(resolve(metadataPath), "utf8")),
|
|
1912
|
+
};
|
|
1913
|
+
// PRD-CC-10: load entity context from file when --entity-context is supplied.
|
|
1914
|
+
// When both file and findings.entities[0] differ, file takes precedence (Q1: override + warn).
|
|
1915
|
+
let entityContext: EntityContext | undefined;
|
|
1916
|
+
if (entityContextPath) {
|
|
1917
|
+
entityContext = JSON.parse(readFileSync(resolve(entityContextPath), "utf8")) as EntityContext;
|
|
1918
|
+
if (inputs.findings.entities && inputs.findings.entities.length > 0) {
|
|
1919
|
+
process.stderr.write(
|
|
1920
|
+
`[warn] --entity-context overrides findings.entities[0] (Q1 recommendation: override + warn)\n`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
// SD-3: load the skill-typed EntityContext (SD-2 self-diag output) and wire
|
|
1925
|
+
// the EnricherOptions.selfDiag path so the meta-report renders end-to-end.
|
|
1926
|
+
let selfDiagOpt: EnricherOptions["selfDiag"];
|
|
1927
|
+
if (selfDiag) {
|
|
1928
|
+
const skillEntity = JSON.parse(
|
|
1929
|
+
readFileSync(resolve(skillEntityPath as string), "utf8")
|
|
1930
|
+
) as EntityContext;
|
|
1931
|
+
selfDiagOpt = { enabled: true, skillEntity };
|
|
1932
|
+
}
|
|
1933
|
+
// W17-WIRING: load the SELECTION-HUB context when --signal-ctx is supplied.
|
|
1934
|
+
// Block C owns the consuming logic (buildSignalCensus); the CLI only threads the
|
|
1935
|
+
// orchestrator-assembled JSON through. Absent flag → undefined → safe-by-default.
|
|
1936
|
+
let signalCtx: SignalCensusContext | undefined;
|
|
1937
|
+
if (signalCtxPath) {
|
|
1938
|
+
signalCtx = JSON.parse(
|
|
1939
|
+
readFileSync(resolve(signalCtxPath), "utf8")
|
|
1940
|
+
) as SignalCensusContext;
|
|
1941
|
+
}
|
|
1942
|
+
// DISMISSAL FINAL-CHECK: load the injected suppression context (Step 8.5b). Absent →
|
|
1943
|
+
// undefined → no suppression (the partition is a no-op, output byte-unchanged).
|
|
1944
|
+
let dismissalContext: EnricherOptions["dismissalContext"];
|
|
1945
|
+
if (dismissalContextPath) {
|
|
1946
|
+
dismissalContext = JSON.parse(
|
|
1947
|
+
readFileSync(resolve(dismissalContextPath), "utf8")
|
|
1948
|
+
) as EnricherOptions["dismissalContext"];
|
|
1949
|
+
}
|
|
1950
|
+
const renderInput = buildRenderInput(inputs, {
|
|
1951
|
+
generatedAt,
|
|
1952
|
+
entityContext,
|
|
1953
|
+
selfDiag: selfDiagOpt,
|
|
1954
|
+
signalCtx,
|
|
1955
|
+
dismissalContext,
|
|
1956
|
+
});
|
|
1957
|
+
writeFileSync(resolve(outPath), JSON.stringify(renderInput, null, 2), "utf8");
|
|
1958
|
+
process.stdout.write(`RenderInput written to: ${outPath}\n`);
|
|
1959
|
+
process.exit(0);
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
process.stderr.write(`Error: ${err}\n`);
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
}
|