@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,3254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/report/render.ts
|
|
3
|
+
* Template + findings data → report.html (4-layer HITL report)
|
|
4
|
+
* Type A — Pure Script (template interpolation — no LLM, no I/O side effects except file write)
|
|
5
|
+
*
|
|
6
|
+
* R-SELF-14-b: Renderer emits semantic JSON-LD (<script type="application/ld+json">) for
|
|
7
|
+
* the full findings graph. Template embeds it via {{FINDINGS_JSONLD}} placeholder.
|
|
8
|
+
* Web-component slots in the default template can read this data without re-parsing HTML.
|
|
9
|
+
*
|
|
10
|
+
* Phase 3-B (Wave-3): flat iter9 anatomy, audience flag (FU-INT-1), Methodology + Trajectory
|
|
11
|
+
* internal tabs (I-024/I-030/I-041), entity cards (I-026/I-038), funnel/heatmap (I-027),
|
|
12
|
+
* meta-report cluster layout (I-029/I-034/I-036), copyDecisions handoff (I-033).
|
|
13
|
+
*
|
|
14
|
+
* RISK-05 (template-stamp fallback): procedural HTML-building is preserved as the primary
|
|
15
|
+
* rendering path. The template-stamp engine (template file + {{PLACEHOLDER}} substitution)
|
|
16
|
+
* was introduced in P4 and is the canonical rendering surface. Both coexist.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* bun scripts/report/render.ts --findings <findings.json> --output <report.html> [options]
|
|
20
|
+
* Options:
|
|
21
|
+
* --template <path> Override template file (default: assets/templates/report.html.tpl)
|
|
22
|
+
* --audience client|internal Audience mode (default: client — leak-safe). NODE-STRIP removes
|
|
23
|
+
* class="internal" nodes when audience=client.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
27
|
+
import { resolve, dirname as pathDirname, join } from "path";
|
|
28
|
+
import { parse as parseYaml } from "yaml";
|
|
29
|
+
import { Value } from "@sinclair/typebox/value";
|
|
30
|
+
import type {
|
|
31
|
+
Finding,
|
|
32
|
+
Remedy,
|
|
33
|
+
DiffStatus,
|
|
34
|
+
FailureOrigin,
|
|
35
|
+
EntityContext,
|
|
36
|
+
SizedText,
|
|
37
|
+
ToolInventoryEntry,
|
|
38
|
+
FeedbackSource,
|
|
39
|
+
TranslatedFeedback,
|
|
40
|
+
FeedbackOnFix,
|
|
41
|
+
} from "../normalize/trace.ts";
|
|
42
|
+
import {
|
|
43
|
+
SelfDiagnosisContractSchema,
|
|
44
|
+
type SelfDiagnosisContract,
|
|
45
|
+
} from "../contract/types.ts";
|
|
46
|
+
|
|
47
|
+
// ── Audience type (FU-INT-1) ─────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** FU-INT-1: Audience for this render pass. Default: "internal" (safe). */
|
|
50
|
+
export type Audience = "client" | "internal";
|
|
51
|
+
|
|
52
|
+
// ── Run metadata (I-024, I-030 — Methodology tab) ───────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* W17-E (R7 — residual surfacing): the canonical "suspected primary — unconfirmed"
|
|
56
|
+
* record. PRODUCED by buildSignalCensus in build-render-input.ts (Block C, which
|
|
57
|
+
* modelled it locally as a superset of this type because render.ts was out of its
|
|
58
|
+
* scope); CONSUMED here by the renderer. Present ONLY when the top DISCOVERED signal
|
|
59
|
+
* was capped at SECONDARY for lack of mechanical evidence (R1 evidence floor) AND
|
|
60
|
+
* primary fell back to a cheap signal. The renderer reads this to show a clear
|
|
61
|
+
* "suspected primary — unconfirmed" note instead of silently crowning the cheap
|
|
62
|
+
* fallback. Absent when the discovered signal passed the evidence floor (nothing
|
|
63
|
+
* unconfirmed) or no discovered signal was surfaced. All-primitive shape, so the
|
|
64
|
+
* enricher's local copy threads into RunMeta.suspectedPrimaryUnconfirmed structurally.
|
|
65
|
+
*/
|
|
66
|
+
export interface SuspectedPrimary {
|
|
67
|
+
/** The discovered WHAT that was suspected-but-uncorroborated. */
|
|
68
|
+
signal: string;
|
|
69
|
+
/** Distinct sampled traces it was seen on (honest prevalence numerator). */
|
|
70
|
+
seenCount: number;
|
|
71
|
+
/** Sampled-trace denominator (R6 — "seen in k/n sampled"). */
|
|
72
|
+
sampledCount: number;
|
|
73
|
+
/** Why it was capped at SECONDARY (the R1-floor reason). */
|
|
74
|
+
reason: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RunMeta {
|
|
78
|
+
/** Total traces in the session (may differ from totalTraces in RenderInput when filtered) */
|
|
79
|
+
totalTraces?: number;
|
|
80
|
+
/** How many traces were scanned by tier-0 static patterns */
|
|
81
|
+
tier0ScannedCount?: number;
|
|
82
|
+
/** How many traces were sent to LLM for deep analysis */
|
|
83
|
+
llmReadCount?: number;
|
|
84
|
+
/** Scope filter expression applied before sampling */
|
|
85
|
+
scopeFilter?: string;
|
|
86
|
+
/** Sampling strategy description */
|
|
87
|
+
samplingStrategy?: string;
|
|
88
|
+
/** Decision log: key choices made during the diagnostic run */
|
|
89
|
+
decisions?: Array<{ step: string; choice: string; rationale: string }>;
|
|
90
|
+
/** Signal census: how many of each signal type were surfaced */
|
|
91
|
+
signalCensus?: Array<{ signal: string; count: number }>;
|
|
92
|
+
/** Mermaid topology diagram source (agent/tool graph) */
|
|
93
|
+
mermaidTopology?: string;
|
|
94
|
+
/**
|
|
95
|
+
* W11-01 (PR-049): The reconciled primary signal for this run.
|
|
96
|
+
* Selected by: failure-validity gate → impact×prevalence → deep-read corroboration.
|
|
97
|
+
* Authoritative source for census badge · heatmap signal flag · funnel coverage note.
|
|
98
|
+
* Set by buildSignalCensus in build-render-input.ts.
|
|
99
|
+
*/
|
|
100
|
+
primarySignal?: {
|
|
101
|
+
/** Selected primary signal name (e.g. "latency-spike"). */
|
|
102
|
+
name: string;
|
|
103
|
+
/** One-sentence rationale (impact×prevalence + deep-read corroboration). */
|
|
104
|
+
why: string;
|
|
105
|
+
/** Signals ruled out by the failure-validity gate before scoring. */
|
|
106
|
+
ruledOut: string[];
|
|
107
|
+
/** Confidence level based on deep-read corroboration. */
|
|
108
|
+
confidence: "high" | "medium" | "low";
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ── Wave-6 R2.1 (+D1) — mandatory LLM deep-read accounting ────────────────
|
|
112
|
+
/**
|
|
113
|
+
* R2.1/D1: a cap-exceeded banner line. When an active cap tripped (max_trace /
|
|
114
|
+
* time / cost), the run STOPS, emits findings, and this banner is rendered at
|
|
115
|
+
* the top of the Methodology tab. Absent when no cap tripped.
|
|
116
|
+
*/
|
|
117
|
+
capBanner?: string;
|
|
118
|
+
/**
|
|
119
|
+
* R2.1: the deep-read gate verdict + reason. When the gate REFUSED, this carries
|
|
120
|
+
* the refusal banner (rendered prominently). Present on every fresh run.
|
|
121
|
+
*/
|
|
122
|
+
deepReadGate?: { verdict: "refuse" | "proceed" | "proceed-with-priors"; reason: string };
|
|
123
|
+
|
|
124
|
+
// ── Wave-6 R2.2 — awareness-layer LLM mini-sample ─────────────────────────
|
|
125
|
+
/**
|
|
126
|
+
* R2.2: a 5-trace LLM mini-sample fired BEFORE primary-signal selection to
|
|
127
|
+
* surface signals Tier-0 cannot MEASURE (the measurement-layer fix). Absent
|
|
128
|
+
* (with blindSpots placeholder) when library priors already exist (SKIP).
|
|
129
|
+
*/
|
|
130
|
+
awarenessSample?: {
|
|
131
|
+
traces: string[];
|
|
132
|
+
findings: string[];
|
|
133
|
+
firedAt: string;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* R2.2: blind-spots table — signals + whether Tier-0 can measure them + how they
|
|
137
|
+
* were checked + the result. Rendered as Methodology Step 1.5.
|
|
138
|
+
*/
|
|
139
|
+
blindSpots?: Array<{
|
|
140
|
+
signal: string;
|
|
141
|
+
measurable: "Tier-0" | "No";
|
|
142
|
+
checkedBy: string;
|
|
143
|
+
result: string;
|
|
144
|
+
}>;
|
|
145
|
+
|
|
146
|
+
// ── Wave-6 R2.4 — methodology widgets ─────────────────────────────────────
|
|
147
|
+
/**
|
|
148
|
+
* R2.4: tier-coverage breakdown for the SVG arc pie. Each entry = one tier with
|
|
149
|
+
* a finding count. The pie handles the 0-finding-tier case (renders an empty arc).
|
|
150
|
+
*/
|
|
151
|
+
tierBreakdown?: Array<{ tier: string; count: number; color?: string }>;
|
|
152
|
+
/**
|
|
153
|
+
* R2.4: per-signal selection-rule cards. Each card shows the signal's score +
|
|
154
|
+
* verdict; `discoveredByAwareness` marks signals surfaced by R2.2 (badge).
|
|
155
|
+
*/
|
|
156
|
+
selectionRules?: Array<{
|
|
157
|
+
signal: string;
|
|
158
|
+
score: string;
|
|
159
|
+
verdict: string;
|
|
160
|
+
discoveredByAwareness?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* D3 (dataset-candidate enrichment): when this signal links to a finding (by
|
|
163
|
+
* failureOrigin.what), these fields are DERIVED from that finding so each
|
|
164
|
+
* candidate card answers — in plain language — which scenario it is, what
|
|
165
|
+
* edge-case it represents, why it failed, why it is a high-value dataset
|
|
166
|
+
* candidate, and what regression it would guard against. All OPTIONAL and
|
|
167
|
+
* omitted gracefully when the source field is absent (never fabricated).
|
|
168
|
+
*/
|
|
169
|
+
/** The linked finding's id (for citation). */
|
|
170
|
+
linkedFindingId?: string;
|
|
171
|
+
/** SCENARIO — plain-words narration of what happened in the trace (failureOrigin.whatHappened). */
|
|
172
|
+
scenario?: string;
|
|
173
|
+
/** USE-CASE / EDGE-CASE this candidate represents (finding subDesc / title). */
|
|
174
|
+
useCase?: string;
|
|
175
|
+
/** WHY it failed — the root cause (why-chain origin / failureOrigin.why). */
|
|
176
|
+
whyFailed?: string;
|
|
177
|
+
/** WHY it is a high-value dataset candidate (severity · prevalence · novelty — derived). */
|
|
178
|
+
whyHighValue?: string;
|
|
179
|
+
/** What regression it would PREVENT (the finding's problem statement). */
|
|
180
|
+
prevents?: string;
|
|
181
|
+
}>;
|
|
182
|
+
/**
|
|
183
|
+
* R2.4: mermaid decision-tree source for the signal-selection trace. Rendered
|
|
184
|
+
* with partial data (the renderer tolerates an empty/short trace).
|
|
185
|
+
*/
|
|
186
|
+
signalSelectionTrace?: string;
|
|
187
|
+
|
|
188
|
+
// ── W17-E (R7) — residual surfacing (canonical slot) ──────────────────────
|
|
189
|
+
/**
|
|
190
|
+
* W17-E (R7): set ONLY when the top DISCOVERED signal was capped at SECONDARY for
|
|
191
|
+
* lack of mechanical evidence (R1 evidence floor) and primary fell back to a cheap
|
|
192
|
+
* signal. The renderer surfaces a "suspected primary — unconfirmed" note so the
|
|
193
|
+
* cheap fallback is NOT silently presented as the confident primary. Absent when
|
|
194
|
+
* the discovered signal passed the floor, or none was discovered. Produced by
|
|
195
|
+
* buildSignalCensus (build-render-input.ts, Block C).
|
|
196
|
+
*/
|
|
197
|
+
suspectedPrimaryUnconfirmed?: SuspectedPrimary;
|
|
198
|
+
|
|
199
|
+
// ── Wave-6 R2.6 (+D2) — operator invocation ────────────────────────────────
|
|
200
|
+
/**
|
|
201
|
+
* D2: the VERBATIM operator brief string. Stored even when parsing succeeds, so
|
|
202
|
+
* the brief can be re-parsed later + for library authenticity. Rendered as
|
|
203
|
+
* Methodology Step 0 ("Operator invocation (verbatim)").
|
|
204
|
+
*/
|
|
205
|
+
operatorInvocation?: string;
|
|
206
|
+
/**
|
|
207
|
+
* R2.6/D2: the parsed invocation shape (agent / timeWindow / focus / residual).
|
|
208
|
+
* Drives the 🎯 Guided tab + tooltip when `focus` is set.
|
|
209
|
+
*/
|
|
210
|
+
parsedInvocation?: {
|
|
211
|
+
agent?: string;
|
|
212
|
+
timeWindow?: string;
|
|
213
|
+
focus?: string;
|
|
214
|
+
/**
|
|
215
|
+
* W12-03: optional — a bare invocation on a neutral-survey run leaves
|
|
216
|
+
* `residual` undefined. The renderer guards with a `—` fallback; the type now
|
|
217
|
+
* reflects the runtime reality (was `string`, which masked the crash path).
|
|
218
|
+
*/
|
|
219
|
+
residual?: string;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ── W9-09 (PR-048): Deep-read escalation telemetry (mirrors trace.ts RunMeta.deepRead) ──
|
|
223
|
+
/**
|
|
224
|
+
* W9-07/W9-09: Deep-read coverage record populated by the trace-hungry escalation
|
|
225
|
+
* loop. Surfaced in the report header bigStat tile (tier reached · tier0 scanned ·
|
|
226
|
+
* llmReadCount · coverageConfidence · stopReason). Backward-compatible — absent on
|
|
227
|
+
* pre-Wave-9 runs.
|
|
228
|
+
*/
|
|
229
|
+
deepRead?: {
|
|
230
|
+
population: number;
|
|
231
|
+
tierReached: number;
|
|
232
|
+
llmReadCount: number;
|
|
233
|
+
coverageConfidence: "high" | "medium" | "low";
|
|
234
|
+
stopReason: "evidence-sufficient" | "ceiling-reached" | "time-budget";
|
|
235
|
+
batches: Array<{
|
|
236
|
+
tier: number;
|
|
237
|
+
newFailureCategories: number;
|
|
238
|
+
coverageConfidence: string;
|
|
239
|
+
}>;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// ── W9-07: Coverage warning flags (from deep-read-gate) ───────────────────
|
|
243
|
+
/**
|
|
244
|
+
* W9-07: Set by the deep-read-gate when trace pool is too thin for high confidence.
|
|
245
|
+
* Triggers a low-confidence banner in the coverage tile.
|
|
246
|
+
*/
|
|
247
|
+
coverageWarning?: boolean;
|
|
248
|
+
/**
|
|
249
|
+
* W9-07: Set by representative.ts when the sample is below the minimum useful size.
|
|
250
|
+
*/
|
|
251
|
+
tooThin?: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Entity type (I-026+I-038 — entity-definition card) ──────────────────────
|
|
255
|
+
|
|
256
|
+
export interface Entity {
|
|
257
|
+
/** Entity identifier / display name */
|
|
258
|
+
name: string;
|
|
259
|
+
/** Category: agent, tool, skill, or model */
|
|
260
|
+
entityType: "agent" | "tool" | "skill" | "model";
|
|
261
|
+
/** Whether source code is accessible for diagnostics */
|
|
262
|
+
codeAccess: boolean;
|
|
263
|
+
/** Short summary of entity purpose */
|
|
264
|
+
summary?: string;
|
|
265
|
+
/** Expandable definition content: system prompt / SKILL.md / source snippet */
|
|
266
|
+
definition?: string;
|
|
267
|
+
|
|
268
|
+
// ── Wave-5 R1.2 — gold-standard entity-card rows (all OPTIONAL) ────────────
|
|
269
|
+
/** Type-row text: "Multi-step agentic email-drafting agent (…)". */
|
|
270
|
+
typeLabel?: string;
|
|
271
|
+
/** Model identifier (rendered with <code>). */
|
|
272
|
+
model?: string;
|
|
273
|
+
/** System-prompt row (may contain HTML, e.g. char count + READABLE pill). */
|
|
274
|
+
systemPrompt?: string;
|
|
275
|
+
/** Tool names — rendered as `.b-tool` chips. */
|
|
276
|
+
tools?: string[];
|
|
277
|
+
/** Code-access row prose (overrides the boolean for richer client copy). */
|
|
278
|
+
codeAccessNote?: string;
|
|
279
|
+
/** Apply-target row prose (e.g. "client-side (client codebase)…"). */
|
|
280
|
+
applyTarget?: string;
|
|
281
|
+
/** Expandable agent-input / prompt sample (gold-standard `details.expand > pre`). */
|
|
282
|
+
inputSample?: string;
|
|
283
|
+
/** Summary text for the input-sample <summary> (defaults to a generic label). */
|
|
284
|
+
inputSampleSummary?: string;
|
|
285
|
+
/**
|
|
286
|
+
* SD self-diag: host runtime for the diagnosed skill ("claude-code" | "codex" | "cursor").
|
|
287
|
+
* Rendered as an extra entity-card row when entityType === "skill".
|
|
288
|
+
*/
|
|
289
|
+
hostRuntime?: "claude-code" | "codex" | "cursor";
|
|
290
|
+
|
|
291
|
+
// ── Wave-5 R1.7 (APPENDIX-A) — rich EntityContext fields (extracted at ingest) ─
|
|
292
|
+
// These make Entity a strict SUPERSET of EntityContext. When present, the
|
|
293
|
+
// renderer prefers them over the legacy string fields and wraps any field
|
|
294
|
+
// > 1 KB in an ExpandableSection. The normalizer populates these deterministically.
|
|
295
|
+
/** Rich system prompt with size metadata — ALWAYS rendered expandable + collapsed (PII). */
|
|
296
|
+
systemPromptCtx?: SizedText;
|
|
297
|
+
/** Aggregated per-tool usage stats — chip strip visible + nested expandable per-tool stats. */
|
|
298
|
+
toolInventory?: ToolInventoryEntry[];
|
|
299
|
+
/** Rich input sample with size metadata — expandable when > 1 KB. */
|
|
300
|
+
inputSampleCtx?: SizedText & { sanitized: boolean };
|
|
301
|
+
/** Provenance string from the normalizer (e.g. "langfuse-export"). */
|
|
302
|
+
source?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Wave-5 R1.7: lift a normalizer-emitted EntityContext into the render-layer
|
|
307
|
+
* Entity shape. The rich fields land on the *Ctx-suffixed slots so the renderer's
|
|
308
|
+
* ExpandableSection logic engages, while legacy display fields stay untouched.
|
|
309
|
+
*/
|
|
310
|
+
export function entityFromContext(ctx: EntityContext): Entity {
|
|
311
|
+
const e: Entity = {
|
|
312
|
+
name: ctx.name,
|
|
313
|
+
entityType: ctx.entityType,
|
|
314
|
+
codeAccess: ctx.codeAccess,
|
|
315
|
+
source: ctx.source,
|
|
316
|
+
};
|
|
317
|
+
if (ctx.model) e.model = ctx.model;
|
|
318
|
+
if (ctx.applyTarget) e.applyTarget = ctx.applyTarget;
|
|
319
|
+
if (ctx.systemPrompt) e.systemPromptCtx = ctx.systemPrompt;
|
|
320
|
+
if (ctx.toolInventory) e.toolInventory = ctx.toolInventory;
|
|
321
|
+
if (ctx.inputSample) e.inputSampleCtx = ctx.inputSample;
|
|
322
|
+
return e;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Wave-5 R1.3 — gold-standard render-only shapes ───────────────────────────
|
|
326
|
+
|
|
327
|
+
/** One tile in the 6-tile `.big-stat` row (Overview). */
|
|
328
|
+
export interface BigStat {
|
|
329
|
+
/** Display value (e.g. "54s", "$156.74", "1,946", "0"). */
|
|
330
|
+
value: string;
|
|
331
|
+
/** Small uppercase label under the value (e.g. "latency p50"). */
|
|
332
|
+
label: string;
|
|
333
|
+
/** Optional inline colour CSS var (e.g. "var(--y)", "var(--r)", "var(--g)"). */
|
|
334
|
+
color?: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** A row in the signal-census table (Overview). */
|
|
338
|
+
export interface SignalCensusRow {
|
|
339
|
+
/** Signal / failure-mode name (e.g. "latency-spike"). Rendered bold if `primary`. */
|
|
340
|
+
signal: string;
|
|
341
|
+
/** "Present?" cell prose (e.g. "YES", "0", "?"). */
|
|
342
|
+
present: string;
|
|
343
|
+
/** Optional colour for the present cell. */
|
|
344
|
+
presentColor?: string;
|
|
345
|
+
/** Measure cell prose (may contain <code>). */
|
|
346
|
+
measure: string;
|
|
347
|
+
/** Decision cell prose (may contain a badge). */
|
|
348
|
+
decision: string;
|
|
349
|
+
/** Marks the ★ PRIMARY row. */
|
|
350
|
+
primary?: boolean;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* W11-03: 4-segment scan-coverage funnel (Overview).
|
|
355
|
+
* Stages: total → tier0-scan → representative-sample N → deep-read 6/N.
|
|
356
|
+
* Denominator for the deep-read segment is the SAMPLE (not population) to be
|
|
357
|
+
* honest about what was actually read. Backward-compat: sample is optional;
|
|
358
|
+
* absent → funnel falls back to 3-segment display (total/code/llm).
|
|
359
|
+
*/
|
|
360
|
+
export interface ScanFunnel {
|
|
361
|
+
/** s-total segment. */
|
|
362
|
+
total: { value: string; label: string; detail: string };
|
|
363
|
+
/** s-code segment (Tier-0 static scan). */
|
|
364
|
+
code: { value: string; label: string; detail: string };
|
|
365
|
+
/**
|
|
366
|
+
* W11-03: s-sample segment (representative sample N drawn from tier0 output).
|
|
367
|
+
* Optional for backward-compat: absent → funnel renders without this segment.
|
|
368
|
+
*/
|
|
369
|
+
sample?: { value: string; label: string; detail: string };
|
|
370
|
+
/** s-llm segment (deep-read LLM analyzers; denominator = sample when present). */
|
|
371
|
+
llm: { value: string; label: string; detail: string };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** One hourly cell in the 24h latency heatmap. */
|
|
375
|
+
export interface HourlyHeatCell {
|
|
376
|
+
/** Hour 0..23. */
|
|
377
|
+
hour: number;
|
|
378
|
+
/** Trace count for the hour (rendered as the cell number). */
|
|
379
|
+
count: number;
|
|
380
|
+
/** Average latency in seconds — drives the l0..l4 colour bucket. */
|
|
381
|
+
avgS: number;
|
|
382
|
+
/** Max latency in seconds (shown in the cell tooltip). */
|
|
383
|
+
maxS: number;
|
|
384
|
+
/** Optional extra tooltip note (e.g. "VOLUME SPIKE", "SLOWEST"). */
|
|
385
|
+
note?: string;
|
|
386
|
+
/**
|
|
387
|
+
* F3 (UR-2): the l0..l4 colour level for THIS cell's active metric. When present,
|
|
388
|
+
* the renderer uses it directly (dynamic-metric heatmap); when absent it falls
|
|
389
|
+
* back to classifying avgS (latency — backward-compat).
|
|
390
|
+
*/
|
|
391
|
+
level?: number;
|
|
392
|
+
/** F3: the active-metric value for this cell (e.g. avg $/trace, error-rate). */
|
|
393
|
+
metricValue?: number;
|
|
394
|
+
/** F3: human label for the active metric value (e.g. "avg $0.04", "12% errors"). */
|
|
395
|
+
metricLabel?: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** F3 (UR-2): which metric the heatmap cells are keyed to. */
|
|
399
|
+
export interface HourlyHeatmapMetric {
|
|
400
|
+
/** The primary-signal name driving the metric choice (e.g. "cost-overshoot"). */
|
|
401
|
+
signal: string;
|
|
402
|
+
/** Legend / caption label (e.g. "avg latency", "avg cost", "error rate"). */
|
|
403
|
+
label: string;
|
|
404
|
+
/** Unit suffix for tooltips (e.g. "s", "$", "%"). */
|
|
405
|
+
unit: string;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The 24h heatmap (Overview). Metric follows the primary signal (F3 / UR-2). */
|
|
409
|
+
export interface HourlyHeatmap {
|
|
410
|
+
/** 24 hourly cells (one per hour). */
|
|
411
|
+
cells: HourlyHeatCell[];
|
|
412
|
+
/** l0..l4 legend boundaries label (e.g. "<50s · 50–65s · …"). Optional. */
|
|
413
|
+
legendLabels?: string[];
|
|
414
|
+
/** Narrative paragraph under the heatmap. */
|
|
415
|
+
narrative?: string;
|
|
416
|
+
/**
|
|
417
|
+
* F3 (UR-2): the active metric the cells are coloured by. Default = latency (so a
|
|
418
|
+
* latency-primary run is byte-identical to the pre-F3 behaviour). Absent → latency.
|
|
419
|
+
*/
|
|
420
|
+
metric?: HourlyHeatmapMetric;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** A graded decision-log row (Methodology). */
|
|
424
|
+
export interface DecisionLogRow {
|
|
425
|
+
/** Decision name (e.g. "Source ingest"). */
|
|
426
|
+
decision: string;
|
|
427
|
+
/** What the skill did. */
|
|
428
|
+
what: string;
|
|
429
|
+
/** Grade HTML (e.g. a badge span). */
|
|
430
|
+
grade: string;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Session trajectory (I-041 — Trajectory internal tab) ────────────────────
|
|
434
|
+
|
|
435
|
+
export interface SessionTrajectoryData {
|
|
436
|
+
/** Ordered steps in the session (tool calls, messages, etc.) */
|
|
437
|
+
steps?: Array<{ step: number; type: string; content: string; timestamp?: string }>;
|
|
438
|
+
/** Mermaid DAG source for the session flow diagram */
|
|
439
|
+
mermaidDag?: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Render input ─────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
export interface RenderInput {
|
|
445
|
+
sessionId: string;
|
|
446
|
+
diagnosedAt: string;
|
|
447
|
+
sourcePlatform: string;
|
|
448
|
+
targetPlatform: string;
|
|
449
|
+
totalTraces: number;
|
|
450
|
+
findings: Finding[];
|
|
451
|
+
discoveredChecks?: DiscoveredCheck[];
|
|
452
|
+
|
|
453
|
+
// ── Wave-5 R1.3 — gold-standard render shapes (built by the enricher) ──────
|
|
454
|
+
/** Header title text (e.g. "DIAGNOSTICS · SAMPLE-EMAIL-AGENT LATENCY"). */
|
|
455
|
+
headerTitle?: string;
|
|
456
|
+
/** Header meta line HTML (generated · source · traces · findings · model). */
|
|
457
|
+
headerMetaHtml?: string;
|
|
458
|
+
/** The diagnosed entity (gold-standard entity card on the Overview tab). */
|
|
459
|
+
diagnosedEntity?: Entity;
|
|
460
|
+
/** 6-tile big-stat row (Overview). */
|
|
461
|
+
bigStat?: BigStat[];
|
|
462
|
+
/** Signal-census table rows (Overview + Methodology). */
|
|
463
|
+
signalCensus?: SignalCensusRow[];
|
|
464
|
+
/** Scan-coverage funnel (Overview). */
|
|
465
|
+
scanFunnel?: ScanFunnel;
|
|
466
|
+
/** 24h latency heatmap (Overview). */
|
|
467
|
+
hourlyHeatmap?: HourlyHeatmap;
|
|
468
|
+
/** Graded decision-log rows (Methodology). */
|
|
469
|
+
decisionLog?: DecisionLogRow[];
|
|
470
|
+
/** Overview headline callout (gold-standard `.crit` box). */
|
|
471
|
+
overviewHeadline?: string;
|
|
472
|
+
/** Overview leverage callout (gold-standard `.alert` box). */
|
|
473
|
+
overviewLeverage?: string;
|
|
474
|
+
/** Overview h2 title + sub (story-led). */
|
|
475
|
+
overviewTitle?: string;
|
|
476
|
+
overviewSub?: string;
|
|
477
|
+
/** Decisions-tab recommended-bundle callout (`.alert`). */
|
|
478
|
+
decisionsBundle?: string;
|
|
479
|
+
/** Mermaid sequence-diagram source for the Methodology topology. */
|
|
480
|
+
mermaidSequence?: string;
|
|
481
|
+
/**
|
|
482
|
+
* FU-INT-1: Audience for this render pass. Default: "internal" (safe — full report).
|
|
483
|
+
* When "client": NODE-STRIP removes all class="internal" nodes (Methodology tab,
|
|
484
|
+
* Trajectory tab, internal-banner) — they are not emitted into the HTML at all.
|
|
485
|
+
*/
|
|
486
|
+
audience?: Audience;
|
|
487
|
+
/**
|
|
488
|
+
* I-024 + I-030: Run metadata for Methodology tab (INTERNAL).
|
|
489
|
+
* If absent, Methodology tab renders with a placeholder message.
|
|
490
|
+
*/
|
|
491
|
+
runMeta?: RunMeta;
|
|
492
|
+
/**
|
|
493
|
+
* I-026 + I-038: Entities involved in the diagnostic session (agents, tools, skills, models).
|
|
494
|
+
* Rendered as entity cards in the Methodology tab.
|
|
495
|
+
*/
|
|
496
|
+
entities?: Entity[];
|
|
497
|
+
/**
|
|
498
|
+
* I-041: Session trajectory data for the Trajectory tab (INTERNAL).
|
|
499
|
+
*/
|
|
500
|
+
sessionTrajectory?: SessionTrajectoryData;
|
|
501
|
+
/**
|
|
502
|
+
* I-035: Override the generated-at timestamp (ISO8601).
|
|
503
|
+
* Defaults to the current time at render. Override useful in tests.
|
|
504
|
+
*/
|
|
505
|
+
generatedAt?: string;
|
|
506
|
+
/**
|
|
507
|
+
* I-029 + I-034 + I-036: Meta-report mode — findings are grouped by cluster (failureOrigin.what)
|
|
508
|
+
* instead of one tab per finding. Used when diagnosing the diagnostics tool itself.
|
|
509
|
+
*/
|
|
510
|
+
isMetaReport?: boolean;
|
|
511
|
+
/**
|
|
512
|
+
* D-8 (Wave-13 Block A): Methodology/variance self-audit variant.
|
|
513
|
+
*
|
|
514
|
+
* A self-diagnosis can be one of two shapes:
|
|
515
|
+
* (a) the TRACE-METRIC variant — the skill diagnosed against real runtime
|
|
516
|
+
* traces, so latency/cost tiles (bigStat / hourlyHeatmap / signalCensus)
|
|
517
|
+
* exist and the R1 §9.3 fail-loud predicate rightly requires them; or
|
|
518
|
+
* (b) the PROCESS/METHODOLOGY variant — a variance / operational-deviance
|
|
519
|
+
* audit of the skill's OWN behaviour. It has NO runtime traces, so the
|
|
520
|
+
* metric tiles legitimately do not exist. Requiring them would force the
|
|
521
|
+
* renderer to either refuse or fabricate metrics.
|
|
522
|
+
*
|
|
523
|
+
* When `methodologyAudit` is true the renderer treats this as variant (b):
|
|
524
|
+
* it EXEMPTS the report from the trace-metric tile requirement (the tiles are
|
|
525
|
+
* absent by nature, not starved) WITHOUT relaxing the predicate for ordinary
|
|
526
|
+
* reports. It implies `isMetaReport` semantics and therefore `audience` is
|
|
527
|
+
* forced to internal (PR-022). The metric-shaped Overview tiles simply omit
|
|
528
|
+
* (they are already conditional); a process-shaped report renders the entity
|
|
529
|
+
* card, findings table, methodology tab, and clustered finding panels.
|
|
530
|
+
*/
|
|
531
|
+
methodologyAudit?: boolean;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export interface DiscoveredCheck {
|
|
535
|
+
checkId: string;
|
|
536
|
+
name: string;
|
|
537
|
+
description: string;
|
|
538
|
+
affectedTraceIds: string[];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── JSON-LD (R-SELF-14-b) ────────────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* R-SELF-14-b: Build a JSON-LD graph for the full findings/remedies graph.
|
|
545
|
+
* Wrapped in <script type="application/ld+json"> in the final HTML.
|
|
546
|
+
*/
|
|
547
|
+
export function buildFindingsJsonLd(input: RenderInput): string {
|
|
548
|
+
const graph = {
|
|
549
|
+
"@context": "https://schema.org",
|
|
550
|
+
"@type": "ItemList",
|
|
551
|
+
"name": "Diagnostic Findings",
|
|
552
|
+
"description": `Session ${safeSessionId(input.sessionId)} diagnosed at ${input.diagnosedAt}`,
|
|
553
|
+
"numberOfItems": input.findings.length,
|
|
554
|
+
"itemListElement": input.findings.map((f, i) => ({
|
|
555
|
+
"@type": "ListItem",
|
|
556
|
+
"position": i + 1,
|
|
557
|
+
"item": {
|
|
558
|
+
"@type": "CreativeWork",
|
|
559
|
+
"identifier": f.findingId,
|
|
560
|
+
"name": f.actionable,
|
|
561
|
+
"description": f.failureOrigin.evidence,
|
|
562
|
+
"keywords": [f.failureOrigin.what, f.failureOrigin.why, f.failureOrigin.where].join(" "),
|
|
563
|
+
"additionalProperty": [
|
|
564
|
+
{ "@type": "PropertyValue", "name": "what", "value": f.failureOrigin.what },
|
|
565
|
+
{ "@type": "PropertyValue", "name": "why", "value": f.failureOrigin.why },
|
|
566
|
+
{ "@type": "PropertyValue", "name": "where", "value": f.failureOrigin.where },
|
|
567
|
+
{ "@type": "PropertyValue", "name": "confidence", "value": f.failureOrigin.confidence },
|
|
568
|
+
],
|
|
569
|
+
"hasPart": f.remedies.map((r) => ({
|
|
570
|
+
"@type": "CreativeWork",
|
|
571
|
+
"identifier": r.remedyId,
|
|
572
|
+
"name": r.title,
|
|
573
|
+
"additionalProperty": [
|
|
574
|
+
{ "@type": "PropertyValue", "name": "cost", "value": r.cost },
|
|
575
|
+
{ "@type": "PropertyValue", "name": "correctness", "value": r.correctness },
|
|
576
|
+
{ "@type": "PropertyValue", "name": "rank", "value": r.rank },
|
|
577
|
+
],
|
|
578
|
+
})),
|
|
579
|
+
},
|
|
580
|
+
})),
|
|
581
|
+
};
|
|
582
|
+
return `<script type="application/ld+json">\n${JSON.stringify(graph, null, 2)}\n</script>`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ── Wave-5 R1.7 — ExpandableSection widget (APPENDIX-A §A.1) ──────────────────
|
|
586
|
+
|
|
587
|
+
/** Threshold above which a field is wrapped in an ExpandableSection (1 KB). */
|
|
588
|
+
const EXPAND_THRESHOLD_BYTES = 1024;
|
|
589
|
+
|
|
590
|
+
/** Human-readable KB string (1 dp) for a byte count. */
|
|
591
|
+
function formatKb(bytes: number): string {
|
|
592
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Wave-5 R1.7 (APPENDIX-A §A.1): the ExpandableSection building block. Renders a
|
|
597
|
+
* default-collapsed `<details class="expand">` with a size+token summary and the
|
|
598
|
+
* content inside a scrollable `<pre>`. Set `forceExpandable` for fields that must
|
|
599
|
+
* always be collapsed regardless of size (e.g. system prompt — PII).
|
|
600
|
+
*
|
|
601
|
+
* The `details.expand` / `summary` / `pre` CSS already lives in the gold-standard
|
|
602
|
+
* report.html.tpl (max-height:340px, overflow:auto, white-space:pre-wrap).
|
|
603
|
+
*/
|
|
604
|
+
export function renderExpandableSection(
|
|
605
|
+
label: string,
|
|
606
|
+
content: SizedText,
|
|
607
|
+
opts: { forceExpandable?: boolean } = {}
|
|
608
|
+
): string {
|
|
609
|
+
const meta = `${formatKb(content.sizeBytes)}${content.tokensApprox ? ` · ${content.tokensApprox.toLocaleString("en-US")} tokens` : ""}`;
|
|
610
|
+
const big = content.sizeBytes > EXPAND_THRESHOLD_BYTES || opts.forceExpandable;
|
|
611
|
+
if (!big) {
|
|
612
|
+
// Small + not force-collapsed: render inline (no details wrapper).
|
|
613
|
+
return `<div class="inline-field">${escapeHtml(content.text)}</div>`;
|
|
614
|
+
}
|
|
615
|
+
// NOTE: never emit the `open` attribute — sections are collapsed by default.
|
|
616
|
+
return `<details class="expand"><summary>${escapeHtml(label)} (${meta} · click to expand)</summary><pre>${escapeHtml(content.text)}</pre></details>`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── Wave-5 helpers — gold-standard class mappings ────────────────────────────
|
|
620
|
+
|
|
621
|
+
/** Map a finding severity → gold-standard badge class suffix (crit/high/med/info). */
|
|
622
|
+
function severityBadgeClass(sev: Finding["severity"] | undefined, confidence: FailureOrigin["confidence"]): "crit" | "high" | "med" | "info" {
|
|
623
|
+
if (sev) return sev;
|
|
624
|
+
// Fall back from confidence when severity is not explicitly set.
|
|
625
|
+
return confidence === "high" ? "crit" : confidence === "medium" ? "high" : "info";
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Map a severity → gold-standard tab sev-dot class. */
|
|
629
|
+
function severityDotClass(sev: "crit" | "high" | "med" | "info"): string {
|
|
630
|
+
return sev === "crit" ? "sev-crit" : sev === "high" ? "sev-high" : sev === "med" ? "sev-med" : "sev-info";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Map a heat avg-latency level (already classified l0..l4) — defensive clamp. */
|
|
634
|
+
function heatLevelClass(level: number): string {
|
|
635
|
+
const l = Math.max(0, Math.min(4, Math.round(level)));
|
|
636
|
+
return `l${l}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Classify an avg-latency value (seconds) into an l0..l4 bucket using gold-standard boundaries. */
|
|
640
|
+
function classifyHeatLevel(avgS: number): number {
|
|
641
|
+
if (avgS < 50) return 0;
|
|
642
|
+
if (avgS < 65) return 1;
|
|
643
|
+
if (avgS < 85) return 2;
|
|
644
|
+
if (avgS < 100) return 3;
|
|
645
|
+
return 4;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ── renderReport ─────────────────────────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
// ── W9-08: Pre-render validation (R-CP-1..4) ─────────────────────────────────
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* W9-08 (R-CP-1/2/3/4): Validate RenderInput before HTML is emitted.
|
|
655
|
+
*
|
|
656
|
+
* Checks (all throw on failure — NEVER emit partial HTML):
|
|
657
|
+
* 1. Template has zero unfilled {{PLACEHOLDER}} remnants vs known placeholder set.
|
|
658
|
+
* Reports any unrecognised {{...}} tokens left in the final rendered output.
|
|
659
|
+
* 2. Every Finding has required fields (findingId, actionable, failureOrigin.*,
|
|
660
|
+
* whyChain, remedies, sourceTraceIds, referenceIds).
|
|
661
|
+
* 3. isMetaReport implies audience === 'internal' (PR-022).
|
|
662
|
+
*
|
|
663
|
+
* This function is called from renderReport AFTER the template is stamped, BEFORE
|
|
664
|
+
* the HTML is returned. On any miss: throws with the full missing list (R-CP-1 —
|
|
665
|
+
* no partial HTML ever emitted).
|
|
666
|
+
*/
|
|
667
|
+
export function validatePreRender(
|
|
668
|
+
renderedHtml: string,
|
|
669
|
+
input: RenderInput
|
|
670
|
+
): void {
|
|
671
|
+
const errors: string[] = [];
|
|
672
|
+
|
|
673
|
+
// ── (1) Template placeholder check — no {{...}} remnants ──────────────────
|
|
674
|
+
const remnants = renderedHtml.match(/\{\{[A-Z_0-9]+\}\}/g);
|
|
675
|
+
if (remnants && remnants.length > 0) {
|
|
676
|
+
const unique = [...new Set(remnants)];
|
|
677
|
+
errors.push(
|
|
678
|
+
`Unresolved template placeholders (${unique.length}): ${unique.join(", ")}`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ── (2) Finding shape validation ──────────────────────────────────────────
|
|
683
|
+
for (let i = 0; i < input.findings.length; i++) {
|
|
684
|
+
const f = input.findings[i];
|
|
685
|
+
const prefix = `findings[${i}] (${String(f.findingId ?? "?")})`;
|
|
686
|
+
if (!f.findingId || typeof f.findingId !== "string") {
|
|
687
|
+
errors.push(`${prefix}: findingId missing or not a string`);
|
|
688
|
+
}
|
|
689
|
+
if (!f.actionable || typeof f.actionable !== "string") {
|
|
690
|
+
errors.push(`${prefix}: actionable missing or not a string`);
|
|
691
|
+
}
|
|
692
|
+
if (!f.failureOrigin || typeof f.failureOrigin !== "object") {
|
|
693
|
+
errors.push(`${prefix}: failureOrigin missing`);
|
|
694
|
+
} else {
|
|
695
|
+
if (!f.failureOrigin.what) errors.push(`${prefix}: failureOrigin.what missing`);
|
|
696
|
+
if (!f.failureOrigin.why) errors.push(`${prefix}: failureOrigin.why missing`);
|
|
697
|
+
if (!f.failureOrigin.where) errors.push(`${prefix}: failureOrigin.where missing`);
|
|
698
|
+
if (!f.failureOrigin.evidence) errors.push(`${prefix}: failureOrigin.evidence missing`);
|
|
699
|
+
if (!f.failureOrigin.confidence) errors.push(`${prefix}: failureOrigin.confidence missing`);
|
|
700
|
+
}
|
|
701
|
+
if (!Array.isArray(f.whyChain)) {
|
|
702
|
+
errors.push(`${prefix}: whyChain is not an array`);
|
|
703
|
+
}
|
|
704
|
+
if (!Array.isArray(f.remedies)) {
|
|
705
|
+
errors.push(`${prefix}: remedies is not an array`);
|
|
706
|
+
}
|
|
707
|
+
if (!Array.isArray(f.sourceTraceIds)) {
|
|
708
|
+
errors.push(`${prefix}: sourceTraceIds is not an array`);
|
|
709
|
+
}
|
|
710
|
+
if (!f.referenceIds || typeof f.referenceIds !== "object") {
|
|
711
|
+
errors.push(`${prefix}: referenceIds missing`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ── (3) isMetaReport ⇒ audience === 'internal' ───────────────────────────
|
|
716
|
+
// D-8: methodologyAudit implies a self-diag (meta) report, same PR-022 rule.
|
|
717
|
+
if ((input.isMetaReport || input.methodologyAudit) && input.audience === "client") {
|
|
718
|
+
errors.push(
|
|
719
|
+
"isMetaReport is true but audience is 'client' — self-diagnosis reports MUST be internal-only (PR-022)"
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (errors.length > 0) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`[W9-08] pre-render validation failed — ${errors.length} error(s). REFUSING to emit HTML:\n` +
|
|
726
|
+
errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Wave-5: Render the gold-standard HTML report from the ported template + a
|
|
733
|
+
* fully-populated RenderInput.
|
|
734
|
+
*
|
|
735
|
+
* Fail-loud predicate (R1 §9.3): the renderer REFUSES (throws) when ≥3 of the 4
|
|
736
|
+
* internal render shapes (diagnosedEntity / bigStat / hourlyHeatmap / signalCensus)
|
|
737
|
+
* are missing — no silent placeholder fallback. This is the guard that the whole
|
|
738
|
+
* regression hinged on (silent graceful-degradation hid the starved input).
|
|
739
|
+
*
|
|
740
|
+
* SD self-diag: when isMetaReport===true the renderer REFUSES audience="client"
|
|
741
|
+
* (PR-022 — self-diagnosis is always internal), force-renders the SELF-DIAGNOSIS
|
|
742
|
+
* banner, and groups findings into clusters by failureOrigin.what.
|
|
743
|
+
*
|
|
744
|
+
* Backward-compat: minimal templates (without the gold-standard slots) and the
|
|
745
|
+
* legacy {{FINDINGS_HTML}} / {{PANELS_HTML}} / {{TABS_NAV_HTML}} placeholders are
|
|
746
|
+
* still substituted, so the open-ended regression guard test keeps passing.
|
|
747
|
+
*/
|
|
748
|
+
export function renderReport(template: string, input: RenderInput): string {
|
|
749
|
+
// D-8: a methodology/variance self-audit is a self-diag (meta) report by
|
|
750
|
+
// definition — it implies isMetaReport so PR-022 (internal-only) still holds.
|
|
751
|
+
const isMetaReport = (input.isMetaReport ?? false) || (input.methodologyAudit ?? false);
|
|
752
|
+
|
|
753
|
+
// SD: self-diag is ALWAYS internal — refuse client (PR-022).
|
|
754
|
+
if (isMetaReport && input.audience === "client") {
|
|
755
|
+
throw new Error(
|
|
756
|
+
"renderReport: refusing --audience client for a self-diagnosis (isMetaReport) report. " +
|
|
757
|
+
"Self-diagnosis output is INTERNAL-only (PR-022)."
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
const audience: Audience = isMetaReport ? "internal" : input.audience ?? "client";
|
|
761
|
+
|
|
762
|
+
// R1 §9.3 fail-loud predicate: ≥3 of 4 internal shapes missing → refuse.
|
|
763
|
+
assertRenderShapesPresent(input);
|
|
764
|
+
|
|
765
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
766
|
+
|
|
767
|
+
// D-4 render guard: `input.sessionId` is typed required but the enricher does
|
|
768
|
+
// not always emit a top-level sessionId, so `.toUpperCase()` (and the slot
|
|
769
|
+
// interpolations below) crashed / printed `undefined` when it was absent.
|
|
770
|
+
// Resolve a safe sessionId ONCE and route every dereference through it.
|
|
771
|
+
const sessionId = safeSessionId(input.sessionId);
|
|
772
|
+
|
|
773
|
+
// ── Header ──
|
|
774
|
+
const headerTitle = input.headerTitle ?? `— DIAGNOSTICS · ${escapeHtml(sessionId.toUpperCase())}`;
|
|
775
|
+
const headerMeta = input.headerMetaHtml ?? defaultHeaderMeta(input, generatedAt);
|
|
776
|
+
|
|
777
|
+
// ── Tab nav ──
|
|
778
|
+
// R2.6: focus directive (from the parsed operator brief) swaps Overview → 🎯 Guided.
|
|
779
|
+
const guidedFocus = input.runMeta?.parsedInvocation?.focus;
|
|
780
|
+
const tabNavHtml = renderTabsNav(input.findings, audience, isMetaReport, guidedFocus);
|
|
781
|
+
|
|
782
|
+
// ── Panels ──
|
|
783
|
+
const overviewPanelHtml = renderOverviewTab(input);
|
|
784
|
+
const findingPanelsHtml = isMetaReport
|
|
785
|
+
? renderMetaReportPanels(input.findings)
|
|
786
|
+
: input.findings.map((f, idx) => renderFindingPanel(f, idx)).join("\n");
|
|
787
|
+
const decisionsPanelHtml = renderDecisionsTab(input);
|
|
788
|
+
|
|
789
|
+
// Methodology panel (INTERNAL only — NODE-STRIP for client).
|
|
790
|
+
const methodologyPanelHtml = audience === "internal" ? renderMethodologyTab(input) : "";
|
|
791
|
+
|
|
792
|
+
// FU-INT-1: internal audience banner (NODE-STRIPPED for client). The
|
|
793
|
+
// class="internal-banner" string must NOT appear in client output.
|
|
794
|
+
const internalBannerHtml =
|
|
795
|
+
audience === "internal" && !isMetaReport
|
|
796
|
+
? `<div class="internal-banner">⚙ INTERNAL — methodology + session-trajectory data included. NODE-STRIPPED for --audience client.</div>`
|
|
797
|
+
: "";
|
|
798
|
+
|
|
799
|
+
// SD: forced self-diagnosis banner (PR-022) — only when isMetaReport.
|
|
800
|
+
const internalBannerForce = isMetaReport
|
|
801
|
+
? `<div class="internal-banner" style="text-align:center;border-radius:0;margin:0;">⚙ SELF-DIAGNOSIS — analyzing the diagnostics skill itself. Findings target config.yaml / skill assets, NEVER source code (PR-022).</div>`
|
|
802
|
+
: "";
|
|
803
|
+
|
|
804
|
+
// I-027 backward-compat slot (kept so the .tpl + minimal templates substitute cleanly).
|
|
805
|
+
const funnelHeatmapHtml = renderFunnelHeatmap();
|
|
806
|
+
|
|
807
|
+
// R-SELF-14-b: JSON-LD semantic block.
|
|
808
|
+
const findingsJsonLd = buildFindingsJsonLd(input);
|
|
809
|
+
|
|
810
|
+
// PRD-CC-07: Live preview script — wires .remedy-cb + .remedy-notes + #general-feedback.
|
|
811
|
+
// PRD-SD-06: self-diag banner discipline — included in the same slot.
|
|
812
|
+
const mermaidScriptHtml = renderLivePreviewScript();
|
|
813
|
+
|
|
814
|
+
// Legacy combined panels (for minimal templates using {{PANELS_HTML}} /
|
|
815
|
+
// {{FINDINGS_HTML}}). Gold-standard templates ignore these.
|
|
816
|
+
const legacyPanelsHtml =
|
|
817
|
+
overviewPanelHtml + "\n" + findingPanelsHtml + "\n" + decisionsPanelHtml +
|
|
818
|
+
(methodologyPanelHtml ? "\n" + methodologyPanelHtml : "");
|
|
819
|
+
|
|
820
|
+
const discoveredChecksHtml =
|
|
821
|
+
input.discoveredChecks && input.discoveredChecks.length > 0
|
|
822
|
+
? renderDiscoveredChecks(input.discoveredChecks)
|
|
823
|
+
: "";
|
|
824
|
+
|
|
825
|
+
const denominator = countRemedies(input.findings);
|
|
826
|
+
|
|
827
|
+
// W9-08: Build HTML string then validate before returning (R-CP-1/2/3).
|
|
828
|
+
const html = template
|
|
829
|
+
.replaceAll("{{TITLE}}", escapeHtml(input.headerTitle ? stripTags(input.headerTitle) : `MUTAGENT-DIAGNOSTICS — ${sessionId}`))
|
|
830
|
+
.replaceAll("{{HEADER_TITLE}}", headerTitle)
|
|
831
|
+
.replaceAll("{{HEADER_META}}", headerMeta)
|
|
832
|
+
.replaceAll("{{TAB_NAV_HTML}}", tabNavHtml)
|
|
833
|
+
.replaceAll("{{METHODOLOGY_PANEL_HTML}}", methodologyPanelHtml)
|
|
834
|
+
.replaceAll("{{OVERVIEW_PANEL_HTML}}", overviewPanelHtml)
|
|
835
|
+
.replaceAll("{{FINDING_PANELS_HTML}}", findingPanelsHtml)
|
|
836
|
+
.replaceAll("{{DECISIONS_PANEL_HTML}}", decisionsPanelHtml)
|
|
837
|
+
.replaceAll("{{APPROVED_COUNT_DENOMINATOR}}", String(denominator))
|
|
838
|
+
.replaceAll("{{INTERNAL_BANNER_FORCE}}", internalBannerForce)
|
|
839
|
+
// Backward-compat + shared slots:
|
|
840
|
+
.replaceAll("{{SESSION_ID}}", escapeHtml(sessionId))
|
|
841
|
+
.replaceAll("{{DIAGNOSED_AT}}", escapeHtml(input.diagnosedAt))
|
|
842
|
+
.replaceAll("{{SOURCE_PLATFORM}}", escapeHtml(input.sourcePlatform))
|
|
843
|
+
.replaceAll("{{TARGET_PLATFORM}}", escapeHtml(input.targetPlatform))
|
|
844
|
+
.replaceAll("{{TOTAL_TRACES}}", String(input.totalTraces))
|
|
845
|
+
.replaceAll("{{FINDINGS_COUNT}}", String(input.findings.length))
|
|
846
|
+
.replaceAll("{{GENERATED_AT}}", escapeHtml(generatedAt))
|
|
847
|
+
.replaceAll("{{INTERNAL_BANNER_HTML}}", internalBannerHtml)
|
|
848
|
+
.replaceAll("{{MERMAID_SCRIPT_HTML}}", mermaidScriptHtml)
|
|
849
|
+
.replaceAll("{{FUNNEL_HEATMAP_HTML}}", funnelHeatmapHtml)
|
|
850
|
+
.replaceAll("{{TABS_NAV_HTML}}", tabNavHtml)
|
|
851
|
+
.replaceAll("{{PANELS_HTML}}", legacyPanelsHtml)
|
|
852
|
+
.replaceAll("{{FINDINGS_HTML}}", legacyPanelsHtml)
|
|
853
|
+
.replaceAll("{{DISCOVERED_CHECKS_HTML}}", discoveredChecksHtml)
|
|
854
|
+
.replaceAll("{{FINDINGS_JSONLD}}", findingsJsonLd);
|
|
855
|
+
|
|
856
|
+
// W9-08 (R-CP-1/2/3): validate placeholder + finding shape + isMetaReport => internal.
|
|
857
|
+
// THROW before emitting partial or invalid HTML — never write broken output.
|
|
858
|
+
validatePreRender(html, input);
|
|
859
|
+
return html;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* R1 §9.3 fail-loud predicate. The 4 internal render shapes are diagnosedEntity,
|
|
864
|
+
* bigStat, hourlyHeatmap, signalCensus. When ≥3 are missing the renderer refuses,
|
|
865
|
+
* rather than silently degrading to placeholders (the root cause of the regression).
|
|
866
|
+
*
|
|
867
|
+
* Empty findings (e.g. the open-ended regression guard with findings:[]) are
|
|
868
|
+
* exempt — a report with no findings legitimately has no render shapes to build.
|
|
869
|
+
*
|
|
870
|
+
* D-8 (Wave-13 Block A): a `methodologyAudit` report (process/variance self-audit)
|
|
871
|
+
* is also exempt — it has NO runtime traces, so the latency/cost tiles legitimately
|
|
872
|
+
* do not exist. The exemption is narrow (gated on the explicit flag) so it cannot
|
|
873
|
+
* mask a genuinely STARVED trace report, which is what the predicate guards against.
|
|
874
|
+
*/
|
|
875
|
+
function assertRenderShapesPresent(input: RenderInput): void {
|
|
876
|
+
if (input.findings.length === 0) return;
|
|
877
|
+
if (input.methodologyAudit) return;
|
|
878
|
+
const missing: string[] = [];
|
|
879
|
+
if (!input.diagnosedEntity) missing.push("diagnosedEntity");
|
|
880
|
+
if (!input.bigStat || input.bigStat.length === 0) missing.push("bigStat");
|
|
881
|
+
if (!input.hourlyHeatmap || input.hourlyHeatmap.cells.length === 0) missing.push("hourlyHeatmap");
|
|
882
|
+
if (!input.signalCensus || input.signalCensus.length === 0) missing.push("signalCensus");
|
|
883
|
+
if (missing.length >= 3) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
`renderReport: refusing to render — ${missing.length} of 4 internal render shapes missing ` +
|
|
886
|
+
`(${missing.join(", ")}). The enricher (scripts/enrich/build-render-input.ts) must ` +
|
|
887
|
+
`populate diagnosedEntity, bigStat, hourlyHeatmap, signalCensus before render. ` +
|
|
888
|
+
`Fail-loud (R1 §9.3) — no silent placeholder fallback.`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Count total remedies across all findings (approved-count denominator). */
|
|
894
|
+
function countRemedies(findings: Finding[]): number {
|
|
895
|
+
return findings.reduce((sum, f) => sum + f.remedies.length, 0);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Default header meta line when the enricher doesn't supply one. */
|
|
899
|
+
function defaultHeaderMeta(input: RenderInput, generatedAt: string): string {
|
|
900
|
+
return (
|
|
901
|
+
`<span class="mk">generated</span> <span class="mv">${escapeHtml(generatedAt)}</span>` +
|
|
902
|
+
`<span class="sep">·</span><span class="mk">source</span> ${escapeHtml(input.sourcePlatform)}` +
|
|
903
|
+
`<span class="sep">·</span><span class="mv">${input.totalTraces.toLocaleString("en-US")} traces</span>` +
|
|
904
|
+
`<span class="sep">·</span><span class="mv">${input.findings.length} findings</span>`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Strip HTML tags from a string (for <title>, which must be text-only). */
|
|
909
|
+
function stripTags(html: string): string {
|
|
910
|
+
return html.replace(/<[^>]*>/g, "").replace(/^—\s*/, "MUTAGENT-DIAGNOSTICS — ").trim();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ── renderTabsNav ─────────────────────────────────────────────────────────────
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Render the gold-standard tabs navigation. Emits one button per finding (client
|
|
917
|
+
* mode) or one per cluster (self-diag meta-report mode), bookended by the
|
|
918
|
+
* Methodology [INTERNAL] tab (t0, internal-only) and the Decisions tab.
|
|
919
|
+
*
|
|
920
|
+
* Tab id scheme (matches gold-standard):
|
|
921
|
+
* t0 → Methodology [INTERNAL] (only when audience===internal)
|
|
922
|
+
* t1 → Overview
|
|
923
|
+
* t2..tN+1 → one per finding (client mode)
|
|
924
|
+
* tcluster-K → one per cluster (self-diag isMetaReport mode)
|
|
925
|
+
* tN+2 / last → Decisions
|
|
926
|
+
*/
|
|
927
|
+
export function renderTabsNav(
|
|
928
|
+
findings: Finding[],
|
|
929
|
+
audience: Audience = "internal",
|
|
930
|
+
isMetaReport: boolean = false,
|
|
931
|
+
guidedFocus?: string
|
|
932
|
+
): string {
|
|
933
|
+
const buttons: string[] = [];
|
|
934
|
+
|
|
935
|
+
// Methodology [INTERNAL] — t0 — only for internal audience.
|
|
936
|
+
if (audience === "internal") {
|
|
937
|
+
buttons.push(
|
|
938
|
+
`<button data-tab="t0" class="internal active">⚙ Methodology [INTERNAL]</button>`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Overview — t1. Active when there is no internal methodology tab.
|
|
943
|
+
// R2.6: when a focus directive is present, 🎯 Guided REPLACES Overview (same t1
|
|
944
|
+
// slot) and carries a tooltip echoing the operator's focus.
|
|
945
|
+
const overviewActive = audience === "internal" ? "" : " class=\"active\"";
|
|
946
|
+
if (guidedFocus && guidedFocus.trim().length > 0) {
|
|
947
|
+
buttons.push(
|
|
948
|
+
`<button data-tab="t1"${overviewActive} title="Guided by operator focus: ${escapeHtml(guidedFocus)}">🎯 Guided</button>`
|
|
949
|
+
);
|
|
950
|
+
} else {
|
|
951
|
+
buttons.push(`<button data-tab="t1"${overviewActive}>① Overview</button>`);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (isMetaReport && findings.length > 0) {
|
|
955
|
+
// SD: tab-per-cluster (group by failureOrigin.what).
|
|
956
|
+
const clusters = clusterFindings(findings);
|
|
957
|
+
let idx = 0;
|
|
958
|
+
for (const [what, group] of clusters) {
|
|
959
|
+
const sev = group.some((f) => f.failureOrigin.confidence === "high")
|
|
960
|
+
? "crit"
|
|
961
|
+
: group.some((f) => f.failureOrigin.confidence === "medium")
|
|
962
|
+
? "high"
|
|
963
|
+
: "info";
|
|
964
|
+
buttons.push(
|
|
965
|
+
`<button data-tab="tcluster-${idx}"><span class="sev-dot ${severityDotClass(sev)}"></span>${escapeHtml(what)} (${group.length})</button>`
|
|
966
|
+
);
|
|
967
|
+
idx++;
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
// Client: one tab per finding.
|
|
971
|
+
findings.forEach((f, idx) => {
|
|
972
|
+
const sev = severityBadgeClass(f.severity, f.failureOrigin.confidence);
|
|
973
|
+
const label = escapeHtml(`${f.findingId} ${shortTitle(f)}`);
|
|
974
|
+
buttons.push(
|
|
975
|
+
`<button data-tab="t${idx + 2}"><span class="sev-dot ${severityDotClass(sev)}"></span>${label}</button>`
|
|
976
|
+
);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Decisions — last tab.
|
|
981
|
+
buttons.push(`<button data-tab="tdecisions">⑦ Decisions</button>`);
|
|
982
|
+
|
|
983
|
+
return buttons.join("\n ");
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** Short title for a finding tab (story-led title, falls back to actionable). */
|
|
987
|
+
function shortTitle(f: Finding): string {
|
|
988
|
+
const raw = f.title ?? f.actionable;
|
|
989
|
+
return raw.length > 32 ? raw.slice(0, 32) + "…" : raw;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ── renderEntityCard (gold-standard .entity / .entity-grid) ──────────────────
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Wave-5 R1.2: Render the gold-standard entity-definition card.
|
|
996
|
+
* Emits `.entity` → `.entity-head` + `.entity-grid` with type/model/system-prompt/
|
|
997
|
+
* tools/code-access/apply-target rows, plus an expandable input-sample `details.expand`.
|
|
998
|
+
*
|
|
999
|
+
* SD: when entityType==="skill" the card renders skill-specific rows
|
|
1000
|
+
* (Diagnostic skill (self-target) · source root · code access YES · apply target
|
|
1001
|
+
* config.yaml/skill assets NEVER source) + an optional host-runtime row.
|
|
1002
|
+
*/
|
|
1003
|
+
export function renderEntityCard(entity: Entity): string {
|
|
1004
|
+
const isSkill = entity.entityType === "skill";
|
|
1005
|
+
|
|
1006
|
+
const rows: string[] = [];
|
|
1007
|
+
|
|
1008
|
+
// Type row.
|
|
1009
|
+
const typeText = entity.typeLabel
|
|
1010
|
+
? entity.typeLabel
|
|
1011
|
+
: isSkill
|
|
1012
|
+
? "Diagnostic skill (self-target)"
|
|
1013
|
+
: escapeHtml(entity.summary ?? entity.entityType);
|
|
1014
|
+
rows.push(`<div class="k">Type</div><div class="v">${entity.typeLabel ? entity.typeLabel : typeText}</div>`);
|
|
1015
|
+
|
|
1016
|
+
// Model row.
|
|
1017
|
+
if (entity.model) {
|
|
1018
|
+
rows.push(`<div class="k">Model</div><div class="v"><code>${escapeHtml(entity.model)}</code></div>`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// SD: source root for skill self-target.
|
|
1022
|
+
if (isSkill) {
|
|
1023
|
+
rows.push(
|
|
1024
|
+
`<div class="k">Source root</div><div class="v"><code>~/.claude/skills/mutagent-diagnostics/</code></div>`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// System-prompt / SKILL.md row.
|
|
1029
|
+
// R1.7: when the rich systemPromptCtx is present, ALWAYS render it as a
|
|
1030
|
+
// forced-collapsed ExpandableSection (PII — explicit click to view) regardless
|
|
1031
|
+
// of size. Fall back to the legacy inline string field otherwise.
|
|
1032
|
+
const sysLabel = isSkill ? "SKILL.md" : "System prompt";
|
|
1033
|
+
if (entity.systemPromptCtx) {
|
|
1034
|
+
rows.push(
|
|
1035
|
+
`<div class="k">${sysLabel}</div><div class="v">${renderExpandableSection(sysLabel, entity.systemPromptCtx, { forceExpandable: true })}</div>`
|
|
1036
|
+
);
|
|
1037
|
+
} else if (entity.systemPrompt) {
|
|
1038
|
+
rows.push(`<div class="k">${sysLabel}</div><div class="v">${entity.systemPrompt}</div>`);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Tools row.
|
|
1042
|
+
// R1.7: rich toolInventory → visible chip strip + a nested expandable per-tool
|
|
1043
|
+
// stats list (collapsed by default). Legacy `tools: string[]` → plain chips.
|
|
1044
|
+
if (entity.toolInventory && entity.toolInventory.length > 0) {
|
|
1045
|
+
rows.push(
|
|
1046
|
+
`<div class="k">Tools (${entity.toolInventory.length})</div><div class="v">${renderToolInventory(entity.toolInventory)}</div>`
|
|
1047
|
+
);
|
|
1048
|
+
} else if (entity.tools && entity.tools.length > 0) {
|
|
1049
|
+
const chips = entity.tools.map((t) => `<span class="b-tool">${escapeHtml(t)}</span>`).join("");
|
|
1050
|
+
rows.push(`<div class="k">Tools (${entity.tools.length})</div><div class="v">${chips}</div>`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// SD: host runtime row.
|
|
1054
|
+
if (entity.hostRuntime) {
|
|
1055
|
+
rows.push(`<div class="k">Host runtime</div><div class="v"><code>${escapeHtml(entity.hostRuntime)}</code></div>`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Code-access row.
|
|
1059
|
+
const codeAccessText = entity.codeAccessNote
|
|
1060
|
+
? entity.codeAccessNote
|
|
1061
|
+
: entity.codeAccess
|
|
1062
|
+
? `<span class="access-yes">YES</span>`
|
|
1063
|
+
: `<span class="access-no">NO — remote export.</span>`;
|
|
1064
|
+
rows.push(`<div class="k">Code access</div><div class="v">${codeAccessText}</div>`);
|
|
1065
|
+
|
|
1066
|
+
// Apply-target row.
|
|
1067
|
+
const applyTargetText = entity.applyTarget
|
|
1068
|
+
? entity.applyTarget
|
|
1069
|
+
: isSkill
|
|
1070
|
+
? `<code>config.yaml</code> / skill assets (NEVER source)`
|
|
1071
|
+
: "";
|
|
1072
|
+
if (applyTargetText) {
|
|
1073
|
+
rows.push(`<div class="k">Apply target</div><div class="v">${applyTargetText}</div>`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Expandable input / prompt sample.
|
|
1077
|
+
// R1.7: rich inputSampleCtx → ExpandableSection (expandable when > 1 KB).
|
|
1078
|
+
// Legacy `inputSample: string` → the original details.expand block.
|
|
1079
|
+
let inputSampleHtml = "";
|
|
1080
|
+
if (entity.inputSampleCtx) {
|
|
1081
|
+
inputSampleHtml = renderExpandableSection("Agent input / prompt sample", entity.inputSampleCtx);
|
|
1082
|
+
} else if (entity.inputSample) {
|
|
1083
|
+
inputSampleHtml = `<details class="expand"><summary>${escapeHtml(entity.inputSampleSummary ?? "View agent input / prompt sample")}</summary><pre>${escapeHtml(entity.inputSample)}</pre></details>`;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return `<div class="entity">
|
|
1087
|
+
<div class="entity-head"><div class="entity-name">⬡ ${escapeHtml(entity.name)}</div><span class="badge b-info">DIAGNOSED ENTITY</span></div>
|
|
1088
|
+
<div class="entity-grid">
|
|
1089
|
+
${rows.join("\n ")}
|
|
1090
|
+
</div>
|
|
1091
|
+
${inputSampleHtml}
|
|
1092
|
+
</div>`;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Wave-5 R1.7: render the tool inventory as a visible chip strip plus a nested,
|
|
1097
|
+
* default-collapsed ExpandableSection listing per-tool stats (callCount,
|
|
1098
|
+
* callsPerTrace, avg/p95 latency, signature). The chip strip is always visible;
|
|
1099
|
+
* the detailed stats are one click away.
|
|
1100
|
+
*/
|
|
1101
|
+
export function renderToolInventory(tools: ToolInventoryEntry[]): string {
|
|
1102
|
+
const chips = tools
|
|
1103
|
+
.map((t) => `<span class="b-tool">${escapeHtml(t.name)}</span>`)
|
|
1104
|
+
.join("");
|
|
1105
|
+
const statRows = tools
|
|
1106
|
+
.map((t) => {
|
|
1107
|
+
const lat =
|
|
1108
|
+
t.avgLatencyMs !== undefined
|
|
1109
|
+
? `avg ${t.avgLatencyMs}ms${t.p95LatencyMs !== undefined ? ` · p95 ${t.p95LatencyMs}ms` : ""}`
|
|
1110
|
+
: "—";
|
|
1111
|
+
const sig = t.signature ? ` · <code>${escapeHtml(t.signature)}</code>` : "";
|
|
1112
|
+
return `<li><strong>${escapeHtml(t.name)}</strong> — ${t.callCount} call${t.callCount !== 1 ? "s" : ""} · ${t.callsPerTrace}/trace · ${lat}${sig}</li>`;
|
|
1113
|
+
})
|
|
1114
|
+
.join("\n ");
|
|
1115
|
+
// Nested expandable per-tool stats (collapsed by default — never `open`).
|
|
1116
|
+
const nested = `<details class="expand"><summary>Per-tool stats (${tools.length} tool${tools.length !== 1 ? "s" : ""} · click to expand)</summary><ul>
|
|
1117
|
+
${statRows}
|
|
1118
|
+
</ul></details>`;
|
|
1119
|
+
return `<div class="tool-chips">${chips}</div>${nested}`;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ── renderBigStat (gold-standard .big-stat) ──────────────────────────────────
|
|
1123
|
+
|
|
1124
|
+
/** Wave-5 R1.2: Render the 6-tile `.big-stat` row. */
|
|
1125
|
+
export function renderBigStat(stats: BigStat[]): string {
|
|
1126
|
+
if (!stats || stats.length === 0) return "";
|
|
1127
|
+
const tiles = stats
|
|
1128
|
+
.map(
|
|
1129
|
+
(s) =>
|
|
1130
|
+
`<div class="s"><div class="v"${s.color ? ` style="color:${s.color}"` : ""}>${escapeHtml(s.value)}</div><div class="l">${escapeHtml(s.label)}</div></div>`
|
|
1131
|
+
)
|
|
1132
|
+
.join("");
|
|
1133
|
+
return `<div class="big-stat">${tiles}</div>`;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
// ── W9-07: Deep-read coverage tile (PR-048) ──────────────────────────────────
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* W9-07 (PR-048): Render the deep-read coverage tile block for the report header
|
|
1141
|
+
* / Overview tab. Shows tierReached · tier0ScannedCount · llmReadCount ·
|
|
1142
|
+
* coverageConfidence · stopReason sourced from RunMeta.deepRead (B1's telemetry).
|
|
1143
|
+
*
|
|
1144
|
+
* Low-confidence banner: rendered when coverageConfidence !== 'high' OR when
|
|
1145
|
+
* RunMeta signals coverageWarning or tooThin. Brand-vars-only CSS.
|
|
1146
|
+
*
|
|
1147
|
+
* Returns "" when RunMeta.deepRead is absent (backward-compatible pre-Wave-9 runs).
|
|
1148
|
+
*/
|
|
1149
|
+
export function renderDeepReadTile(runMeta?: RunMeta): string {
|
|
1150
|
+
const dr = runMeta?.deepRead;
|
|
1151
|
+
if (!dr) return "";
|
|
1152
|
+
|
|
1153
|
+
const isLowConf = dr.coverageConfidence !== "high";
|
|
1154
|
+
// tooThin / coverageWarning come from the B1 deep-read-gate (optional fields)
|
|
1155
|
+
const tooThin = (runMeta as Record<string, unknown>).coverageWarning === true
|
|
1156
|
+
|| (runMeta as Record<string, unknown>).tooThin === true;
|
|
1157
|
+
const showBanner = isLowConf || tooThin;
|
|
1158
|
+
|
|
1159
|
+
const confColor =
|
|
1160
|
+
dr.coverageConfidence === "high"
|
|
1161
|
+
? "var(--g)"
|
|
1162
|
+
: dr.coverageConfidence === "medium"
|
|
1163
|
+
? "var(--y)"
|
|
1164
|
+
: "var(--r)";
|
|
1165
|
+
|
|
1166
|
+
const stopReasonLabel = {
|
|
1167
|
+
"evidence-sufficient": "evidence sufficient",
|
|
1168
|
+
"ceiling-reached": "ceiling reached",
|
|
1169
|
+
"time-budget": "time budget exhausted",
|
|
1170
|
+
}[dr.stopReason as string] ?? dr.stopReason;
|
|
1171
|
+
|
|
1172
|
+
const tiles = [
|
|
1173
|
+
{ value: String(dr.tierReached), label: "tier reached", color: "var(--p)" },
|
|
1174
|
+
{ value: dr.population.toLocaleString("en-US"), label: "trace population", color: "var(--muted)" },
|
|
1175
|
+
{ value: String(dr.llmReadCount), label: "llm-read", color: "var(--c)" },
|
|
1176
|
+
{ value: dr.coverageConfidence, label: "coverage confidence", color: confColor },
|
|
1177
|
+
{ value: stopReasonLabel, label: "stop reason", color: "var(--muted)" },
|
|
1178
|
+
];
|
|
1179
|
+
|
|
1180
|
+
const tileHtml = tiles
|
|
1181
|
+
.map(
|
|
1182
|
+
(t) =>
|
|
1183
|
+
`<div class="s"><div class="v" style="color:${t.color}">${escapeHtml(t.value)}</div><div class="l">${escapeHtml(t.label)}</div></div>`
|
|
1184
|
+
)
|
|
1185
|
+
.join("");
|
|
1186
|
+
|
|
1187
|
+
const banner = showBanner
|
|
1188
|
+
? `<div class="warn" style="margin-top:8px;">` +
|
|
1189
|
+
`<strong>Low read coverage</strong> — ` +
|
|
1190
|
+
`coverageConfidence: <strong>${escapeHtml(dr.coverageConfidence)}</strong>` +
|
|
1191
|
+
`${tooThin ? " · trace pool too thin for high confidence" : ""}` +
|
|
1192
|
+
`. Findings may under-represent the population. Re-run with a larger population or ` +
|
|
1193
|
+
`wider time window to improve coverage. (W9-07 / PR-048)</div>`
|
|
1194
|
+
: "";
|
|
1195
|
+
|
|
1196
|
+
return `<div class="deep-read-tile">
|
|
1197
|
+
<h3>Deep-read coverage — trace-hungry escalation telemetry</h3>
|
|
1198
|
+
<div class="big-stat deep-read-stat">${tileHtml}</div>
|
|
1199
|
+
${banner}
|
|
1200
|
+
</div>`;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// ── W9-F: Translated feedback block (PR-046 Layer 2) ─────────────────────────
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* W9-F (PR-046 Layer 2): Render translated-feedback entries for a finding.
|
|
1207
|
+
* Bridges raw user symptom → component-level RCA target. Shows "what the user
|
|
1208
|
+
* said" (rawQuote) vs "what it means for the agent" (component + reasoning).
|
|
1209
|
+
*
|
|
1210
|
+
* Backward-compatible: returns "" when translatedFeedback is absent or empty.
|
|
1211
|
+
*/
|
|
1212
|
+
export function renderTranslatedFeedbackBlock(finding: Finding): string {
|
|
1213
|
+
const entries: TranslatedFeedback[] = finding.translatedFeedback ?? [];
|
|
1214
|
+
if (entries.length === 0) return "";
|
|
1215
|
+
|
|
1216
|
+
const confPill = (c: TranslatedFeedback["confidence"]): string => {
|
|
1217
|
+
const cls = c === "high" ? "b-info" : c === "medium" ? "b-med" : "b-crit";
|
|
1218
|
+
return `<span class="badge ${cls}">${c.toUpperCase()}</span>`;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const items = entries
|
|
1222
|
+
.map((e) => {
|
|
1223
|
+
const sourceLine = e.sourceIndex !== undefined
|
|
1224
|
+
? `<span class="fb-source-type">source[${e.sourceIndex}]</span> · `
|
|
1225
|
+
: "";
|
|
1226
|
+
return `<div class="fb-item fb-translated">
|
|
1227
|
+
<div class="fb-head">${sourceLine}<strong>${escapeHtml(e.component)}</strong> <span class="tax-chip">${escapeHtml(e.affectedComponent)}</span> ${confPill(e.confidence)}</div>
|
|
1228
|
+
<blockquote class="fb-rawquote">"${escapeHtml(e.rawQuote)}"</blockquote>
|
|
1229
|
+
<p class="fb-reasoning">${escapeHtml(e.reasoning)}</p>
|
|
1230
|
+
</div>`;
|
|
1231
|
+
})
|
|
1232
|
+
.join("\n");
|
|
1233
|
+
|
|
1234
|
+
return `<div class="feedback-list feedback-translated">
|
|
1235
|
+
<h3>Translated feedback <span class="r-count">(${entries.length} translation${entries.length !== 1 ? "s" : ""})</span> <span class="badge b-med">L2</span></h3>
|
|
1236
|
+
${items}
|
|
1237
|
+
</div>`;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// ── W9-F: Fix-feedback block (PR-046 Layer 3) ─────────────────────────────────
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* W9-F (PR-046 Layer 3): Render per-remedy fix-outcome history from
|
|
1244
|
+
* Remedy.feedbackOnFix[]. Shows a status pill per outcome record.
|
|
1245
|
+
*
|
|
1246
|
+
* Backward-compatible: returns "" when feedbackOnFix is absent or empty.
|
|
1247
|
+
*/
|
|
1248
|
+
export function renderFeedbackOnFixBlock(remedy: Remedy): string {
|
|
1249
|
+
const entries: FeedbackOnFix[] = remedy.feedbackOnFix ?? [];
|
|
1250
|
+
if (entries.length === 0) return "";
|
|
1251
|
+
|
|
1252
|
+
const outcomeConfig = {
|
|
1253
|
+
closed: { label: "CLOSED", color: "var(--g)" },
|
|
1254
|
+
partial: { label: "PARTIAL", color: "var(--y)" },
|
|
1255
|
+
ineffective: { label: "INEFFECTIVE", color: "var(--r)" },
|
|
1256
|
+
regressed: { label: "REGRESSED", color: "var(--r)" },
|
|
1257
|
+
} as const;
|
|
1258
|
+
|
|
1259
|
+
const rows = entries
|
|
1260
|
+
.map((e) => {
|
|
1261
|
+
const cfg = outcomeConfig[e.outcome] ?? { label: e.outcome.toUpperCase(), color: "var(--muted)" };
|
|
1262
|
+
const pill = `<span style="font-family:var(--fm);font-size:11px;font-weight:700;text-transform:uppercase;padding:2px 7px;border-radius:3px;background:${cfg.color};color:var(--bg);">${escapeHtml(cfg.label)}</span>`;
|
|
1263
|
+
const nextSteps =
|
|
1264
|
+
e.nextSteps && e.nextSteps.length > 0
|
|
1265
|
+
? `<ul class="fix-next-steps">${e.nextSteps.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}</ul>`
|
|
1266
|
+
: "";
|
|
1267
|
+
const comment = e.comment ? `<p class="fix-comment">${escapeHtml(e.comment)}</p>` : "";
|
|
1268
|
+
return `<div class="fix-fb-row">
|
|
1269
|
+
<div class="fix-fb-head">${pill} <span class="fix-session"><code>${escapeHtml(e.testSessionId)}</code></span> · ${escapeHtml(e.testedAt)}</div>
|
|
1270
|
+
${comment}
|
|
1271
|
+
${nextSteps}
|
|
1272
|
+
</div>`;
|
|
1273
|
+
})
|
|
1274
|
+
.join("\n");
|
|
1275
|
+
|
|
1276
|
+
return `<div class="fix-feedback-list">
|
|
1277
|
+
<h4>Fix history <span class="r-count">(${entries.length} test${entries.length !== 1 ? "s" : ""})</span> <span class="badge b-med">L3</span></h4>
|
|
1278
|
+
${rows}
|
|
1279
|
+
</div>`;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// ── render24hHeatmap (gold-standard .heat / .cell l0..l4 / .heat-legend) ──────
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* W11-02 (Wave-5 R1.2): Render the 24h latency heatmap as 24 `.cell` divs.
|
|
1286
|
+
* Colouring: when `runMeta.primarySignal` is present, cells where the primary
|
|
1287
|
+
* signal flag fires get a `sig` marker on the title tooltip; latency is the
|
|
1288
|
+
* fallback colour when no signal flag is available (pre-W11 backward-compat).
|
|
1289
|
+
*
|
|
1290
|
+
* W11-02: adds a window selector (default 24h, selectable 7d / 30d). The selector
|
|
1291
|
+
* is rendered as a small pill row above the heatmap; switching windows is
|
|
1292
|
+
* client-side only (the heatmap data in this render pass is always 24h — the
|
|
1293
|
+
* selector communicates to the operator which window they're viewing and links
|
|
1294
|
+
* back to a broader trace pull if needed).
|
|
1295
|
+
*/
|
|
1296
|
+
export function render24hHeatmap(heatmap: HourlyHeatmap, runMeta?: RunMeta): string {
|
|
1297
|
+
if (!heatmap || heatmap.cells.length === 0) return "";
|
|
1298
|
+
|
|
1299
|
+
const primarySignalName = runMeta?.primarySignal?.name;
|
|
1300
|
+
|
|
1301
|
+
// F3 (UR-2): the active metric drives cell colour + legend + caption. Default =
|
|
1302
|
+
// latency (backward-compat: a latency-primary heatmap is byte-identical to pre-F3).
|
|
1303
|
+
const metricLabel = heatmap.metric?.label ?? "avg latency";
|
|
1304
|
+
|
|
1305
|
+
const cells = heatmap.cells
|
|
1306
|
+
.slice()
|
|
1307
|
+
.sort((a, b) => a.hour - b.hour)
|
|
1308
|
+
.map((c) => {
|
|
1309
|
+
// F3: prefer the enricher-supplied dynamic level; fall back to classifying
|
|
1310
|
+
// latency (pre-F3 cells that carry no `level`).
|
|
1311
|
+
const lvl = c.level !== undefined ? c.level : classifyHeatLevel(c.avgS);
|
|
1312
|
+
const level = heatLevelClass(lvl);
|
|
1313
|
+
const hh = c.hour.toString().padStart(2, "0");
|
|
1314
|
+
const signalNote = primarySignalName && c.note?.includes(primarySignalName)
|
|
1315
|
+
? ` · ★ ${escapeHtml(primarySignalName)}`
|
|
1316
|
+
: "";
|
|
1317
|
+
// F3: tooltip leads with the active metric value (metricLabel), retaining
|
|
1318
|
+
// latency context (avg/max) for reference.
|
|
1319
|
+
const metricNote = c.metricLabel ? `${c.metricLabel} · ` : "";
|
|
1320
|
+
const title = `${hh}h ${metricNote}avg ${c.avgS}s max ${c.maxS}s${c.note ? " · " + c.note : ""}${signalNote}`;
|
|
1321
|
+
return `<div class="cell ${level}" title="${escapeHtml(title)}"><span class="ch">${hh}</span><span class="cn">${c.count}</span></div>`;
|
|
1322
|
+
})
|
|
1323
|
+
.join("\n ");
|
|
1324
|
+
|
|
1325
|
+
// F3: latency keeps its absolute-band legend; relative-metric modes get a
|
|
1326
|
+
// low→high relative legend (no spurious absolute boundaries).
|
|
1327
|
+
const isLatency = (heatmap.metric?.signal ?? "latency-spike") === "latency-spike" || heatmap.metric === undefined;
|
|
1328
|
+
const defaultLegend = isLatency
|
|
1329
|
+
? ["<50s", "50–65s", "65–85s", "85–100s", ">100s"]
|
|
1330
|
+
: ["low", "", "mid", "", "high"];
|
|
1331
|
+
const legend = (heatmap.legendLabels ?? defaultLegend)
|
|
1332
|
+
.map((label, i) => `<span><span class="sw l${i}"></span>${escapeHtml(label)}</span>`)
|
|
1333
|
+
.join("");
|
|
1334
|
+
|
|
1335
|
+
const narrative = heatmap.narrative ? `<p style="font-size:12px;color:var(--muted);">${heatmap.narrative}</p>` : "";
|
|
1336
|
+
|
|
1337
|
+
// W11-02: window selector — default 24h, selectable 7d / 30d.
|
|
1338
|
+
// Data in this render pass is always 24h; the selector communicates the active
|
|
1339
|
+
// window and can be extended when multi-window data is available.
|
|
1340
|
+
const primaryNote = primarySignalName
|
|
1341
|
+
? ` · primary signal: <strong style="color:var(--r);">${escapeHtml(primarySignalName)}</strong>`
|
|
1342
|
+
: "";
|
|
1343
|
+
const windowSelector = `<div class="heat-window-selector" style="font-size:11px;margin-bottom:4px;color:var(--muted);">
|
|
1344
|
+
<span class="heat-win active" data-win="24h" style="font-weight:700;color:var(--c);">24h</span>
|
|
1345
|
+
<span style="margin:0 4px;">·</span>
|
|
1346
|
+
<span class="heat-win" data-win="7d" style="cursor:pointer;">7d</span>
|
|
1347
|
+
<span style="margin:0 4px;">·</span>
|
|
1348
|
+
<span class="heat-win" data-win="30d" style="cursor:pointer;">30d</span>
|
|
1349
|
+
${primaryNote}
|
|
1350
|
+
</div>`;
|
|
1351
|
+
|
|
1352
|
+
return `${windowSelector}<div class="heat" id="heat">
|
|
1353
|
+
${cells}
|
|
1354
|
+
</div>
|
|
1355
|
+
<div class="heat-legend">${escapeHtml(metricLabel)}: ${legend} · hover a cell for detail</div>
|
|
1356
|
+
${narrative}`;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ── renderSignalCensus (gold-standard signal-census table) ───────────────────
|
|
1360
|
+
|
|
1361
|
+
/** Wave-5 R1.2: Render the signal-census table. */
|
|
1362
|
+
export function renderSignalCensus(rows: SignalCensusRow[]): string {
|
|
1363
|
+
if (!rows || rows.length === 0) return "";
|
|
1364
|
+
const body = rows
|
|
1365
|
+
.map((r) => {
|
|
1366
|
+
const signalCell = r.primary ? `<strong>${escapeHtml(r.signal)}</strong>` : escapeHtml(r.signal);
|
|
1367
|
+
const presentStyle = r.presentColor ? ` style="color:${r.presentColor}"` : "";
|
|
1368
|
+
return `<tr><td>${signalCell}</td><td${presentStyle}>${escapeHtml(r.present)}</td><td>${r.measure}</td><td>${r.decision}</td></tr>`;
|
|
1369
|
+
})
|
|
1370
|
+
.join("\n ");
|
|
1371
|
+
return `<table>
|
|
1372
|
+
<thead><tr><th>Signal / failure-mode</th><th>Present?</th><th>Measure</th><th>Decision</th></tr></thead>
|
|
1373
|
+
<tbody>
|
|
1374
|
+
${body}
|
|
1375
|
+
</tbody>
|
|
1376
|
+
</table>`;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// ── renderAssumptionsBlock (gold-standard .assumptions w/ pills) ─────────────
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Wave-5 R1.2: Render the `.assumptions` block — one `<li>` per assumption with a
|
|
1383
|
+
* verified / unverified / hypothesis-pending pill.
|
|
1384
|
+
*/
|
|
1385
|
+
export function renderAssumptionsBlock(finding: Finding): string {
|
|
1386
|
+
const assumptions = finding.assumptions ?? [];
|
|
1387
|
+
if (assumptions.length === 0) return "";
|
|
1388
|
+
const items = assumptions
|
|
1389
|
+
.map((a) => {
|
|
1390
|
+
const pillClass = a.status; // "verified" | "unverified" | "hypothesis-pending"
|
|
1391
|
+
const pillLabel =
|
|
1392
|
+
a.status === "verified" ? "VERIFIED" : a.status === "unverified" ? "UNVERIFIED" : "HYPOTHESIS-PENDING";
|
|
1393
|
+
return `<li>${escapeHtml(a.text)} <span class="${pillClass}">${pillLabel} — ${escapeHtml(a.basis)}</span></li>`;
|
|
1394
|
+
})
|
|
1395
|
+
.join("\n ");
|
|
1396
|
+
return `<div class="assumptions"><h4>⚠ Assumptions</h4><ul>
|
|
1397
|
+
${items}
|
|
1398
|
+
</ul></div>`;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// ── renderRemedyCard (gold-standard .remedy.recommended) ─────────────────────
|
|
1402
|
+
|
|
1403
|
+
/** Map a remedy applyTarget label → gold-standard apply-pill class. */
|
|
1404
|
+
function applyPillClass(label: string | undefined): string {
|
|
1405
|
+
if (!label) return "apply-none";
|
|
1406
|
+
const l = label.toLowerCase();
|
|
1407
|
+
if (l.includes("code")) return "apply-code";
|
|
1408
|
+
if (l.includes("prompt")) return "apply-prompt";
|
|
1409
|
+
if (l.includes("config")) return "apply-config";
|
|
1410
|
+
if (l.includes("no change") || l.includes("none")) return "apply-none";
|
|
1411
|
+
return "apply-config";
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/** Map a remedy targetClass → canonical tclass-pill class. */
|
|
1415
|
+
function tclassPillClass(targetClass: string | undefined): string {
|
|
1416
|
+
if (!targetClass) return "";
|
|
1417
|
+
const l = targetClass.toLowerCase();
|
|
1418
|
+
if (l.includes("local-agent")) return "tclass-local-agent";
|
|
1419
|
+
if (l.includes("local-code")) return "tclass-local-code-construct";
|
|
1420
|
+
if (l.includes("remote")) return "tclass-remote";
|
|
1421
|
+
return "tclass-local-agent";
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/** Map a remedy changeType → canonical ctype-pill class. */
|
|
1425
|
+
function ctypePillClass(changeType: Remedy["changeType"]): string {
|
|
1426
|
+
if (!changeType) return "";
|
|
1427
|
+
return `ctype-${changeType}`;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/** Map a remedy correctness → canonical b-correctness class. */
|
|
1431
|
+
function correctnessPillClass(correctness: Remedy["correctness"]): string {
|
|
1432
|
+
// D-1 render guard: an absent correctness must NOT yield `b-correctness-undefined`.
|
|
1433
|
+
// Block C owns making the DATA always-present (type↔producer↔gate); this is the
|
|
1434
|
+
// render safety-net so a missing field can never reach the operator as `undefined`.
|
|
1435
|
+
return correctness ? `b-correctness-${correctness}` : "b-correctness-na";
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* D-1 render guard. The remedy badge fields (`rank` / `cost` / `correctness`) are
|
|
1440
|
+
* REQUIRED on the canonical Remedy type, yet the broken-report evidence showed
|
|
1441
|
+
* them rendering as the literal string `undefined` when a producer/contract
|
|
1442
|
+
* desync left them unset at runtime. This is the render-side safety-net: an
|
|
1443
|
+
* absent value becomes a neutral `n/a` marker, never `undefined`.
|
|
1444
|
+
*
|
|
1445
|
+
* `value` is `unknown` deliberately — the guard must survive a value the type
|
|
1446
|
+
* system claims cannot be missing (defense-in-depth; Block C fixes the data).
|
|
1447
|
+
*/
|
|
1448
|
+
function badgeField(value: unknown): string {
|
|
1449
|
+
if (value === undefined || value === null || value === "") return "n/a";
|
|
1450
|
+
return String(value);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* D-4 render guard. `RenderInput.sessionId` is typed required, but the enricher
|
|
1455
|
+
* does not always emit a top-level sessionId — when absent the renderer crashed
|
|
1456
|
+
* on `sessionId.toUpperCase()` (the report would not render at all). Resolve a
|
|
1457
|
+
* safe, non-empty session label so the header / title / footer never crash or
|
|
1458
|
+
* print `undefined`. The data-side fix (enricher populating sessionId) is
|
|
1459
|
+
* tracked separately; this is the render-side null-guard.
|
|
1460
|
+
*
|
|
1461
|
+
* `value` is `unknown` for defense-in-depth — the guard must survive a value
|
|
1462
|
+
* the type system claims cannot be missing.
|
|
1463
|
+
*/
|
|
1464
|
+
function safeSessionId(value: unknown): string {
|
|
1465
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1466
|
+
return "unknown-session";
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* PRD-CC-08: Build the pre-assembled markdown bundle for a remedy.
|
|
1471
|
+
* Emitted as a hidden <script type="text/plain"> so the Live Preview reader
|
|
1472
|
+
* can use it for deterministic, DOM-extraction-free bundle assembly.
|
|
1473
|
+
*/
|
|
1474
|
+
export function buildRemedyMarkdownBundle(remedy: Remedy, finding: Finding): string {
|
|
1475
|
+
const lines: string[] = [];
|
|
1476
|
+
lines.push(`## ${escapeMarkdown(remedy.remedyId)} — ${escapeMarkdown(remedy.title)}`);
|
|
1477
|
+
lines.push(`**Finding:** ${escapeMarkdown(finding.findingId)} — ${escapeMarkdown(finding.title ?? finding.actionable)}`);
|
|
1478
|
+
lines.push(`**Severity:** ${finding.severity ?? finding.failureOrigin.confidence}`);
|
|
1479
|
+
if (remedy.applyTarget) lines.push(`**Apply target:** ${escapeMarkdown(remedy.applyTarget)}`);
|
|
1480
|
+
if (remedy.targetClass) lines.push(`**Target class:** ${escapeMarkdown(remedy.targetClass)}`);
|
|
1481
|
+
if (remedy.changeType) lines.push(`**Change type:** ${remedy.changeType}`);
|
|
1482
|
+
// D-1 render guard: the copy-back markdown bundle must not leak `undefined` either.
|
|
1483
|
+
lines.push(`**Cost:** ${badgeField(remedy.cost)} · **Correctness:** ${badgeField(remedy.correctness)} · **Rank:** ${badgeField(remedy.rank)}`);
|
|
1484
|
+
lines.push("");
|
|
1485
|
+
|
|
1486
|
+
if (remedy.rationale) {
|
|
1487
|
+
lines.push("### Why this remedy");
|
|
1488
|
+
lines.push(escapeMarkdown(remedy.rationale));
|
|
1489
|
+
lines.push("");
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (remedy.whyWorks) {
|
|
1493
|
+
lines.push("### Why this works");
|
|
1494
|
+
lines.push(escapeMarkdown(remedy.whyWorks));
|
|
1495
|
+
lines.push("");
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (remedy.diff) {
|
|
1499
|
+
lines.push("### Diff");
|
|
1500
|
+
lines.push("**Before:**");
|
|
1501
|
+
lines.push("```diff");
|
|
1502
|
+
lines.push(`- ${remedy.diff.before}`);
|
|
1503
|
+
lines.push("```");
|
|
1504
|
+
lines.push("**After:**");
|
|
1505
|
+
lines.push("```diff");
|
|
1506
|
+
lines.push(`+ ${remedy.diff.after}`);
|
|
1507
|
+
lines.push("```");
|
|
1508
|
+
lines.push("");
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (remedy.plan) {
|
|
1512
|
+
lines.push("### Apply plan");
|
|
1513
|
+
for (const f of remedy.plan.files) {
|
|
1514
|
+
lines.push(`- [${f.action}] ${f.path}${f.lineRange ? ` (${f.lineRange})` : ""}`);
|
|
1515
|
+
}
|
|
1516
|
+
if (remedy.plan.verify.length > 0) {
|
|
1517
|
+
lines.push("");
|
|
1518
|
+
lines.push("**Verify:**");
|
|
1519
|
+
for (const v of remedy.plan.verify) {
|
|
1520
|
+
lines.push(`- \`${v}\``);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (remedy.plan.acceptance) {
|
|
1524
|
+
lines.push(`**Acceptance:** ${remedy.plan.acceptance}`);
|
|
1525
|
+
}
|
|
1526
|
+
if (remedy.plan.commitMessage) {
|
|
1527
|
+
lines.push(`**Commit:** \`${remedy.plan.commitMessage}\``);
|
|
1528
|
+
}
|
|
1529
|
+
lines.push("");
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
if (remedy.applyInstructions && remedy.applyInstructions.length > 0) {
|
|
1533
|
+
lines.push("### Apply instructions");
|
|
1534
|
+
remedy.applyInstructions.forEach((step, i) => {
|
|
1535
|
+
lines.push(`${i + 1}. ${escapeMarkdown(step)}`);
|
|
1536
|
+
});
|
|
1537
|
+
lines.push("");
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
return lines.join("\n");
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/** Minimal markdown escaper for inline text (not block-level). */
|
|
1544
|
+
function escapeMarkdown(s: string): string {
|
|
1545
|
+
return s.replace(/[*_`[\]]/g, "\\$&");
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* PRD-CC-03: Canonical remedy card anatomy (D1/D2/D3/D4/D7).
|
|
1550
|
+
* Render order:
|
|
1551
|
+
* checkbox (top-right) → header row (rank · id · title) →
|
|
1552
|
+
* meta strip (cost · correctness · targetClass · changeType · applyTarget pills) →
|
|
1553
|
+
* purple r-rationale → cyan r-why-works →
|
|
1554
|
+
* dashed r-target row (IFF applyTarget) →
|
|
1555
|
+
* r-diff-grid 2-col Before/After (IFF diff) →
|
|
1556
|
+
* r-apply-grid 2-col Apply plan + Apply instructions (IFF plan OR applyInstructions) →
|
|
1557
|
+
* remedy-notes textarea.
|
|
1558
|
+
* NO <details> collapsibles. NO per-remedy Copy button.
|
|
1559
|
+
* Hidden <script type="text/plain"> payload for Live Preview (CC-08).
|
|
1560
|
+
*/
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* W12-08 (PR-052 proposed): render the "source not found — hypothesis" caveat
|
|
1564
|
+
* block shown in place of the Before/After grid when a remedy carries a
|
|
1565
|
+
* `diffStatus` instead of a `diff`. Honors `feedback_model_intent_sacred` — a
|
|
1566
|
+
* remedy with no findable source surfaces an explicit marker, never a guessed diff.
|
|
1567
|
+
*/
|
|
1568
|
+
export function renderDiffStatusCaveat(diffStatus: DiffStatus): string {
|
|
1569
|
+
const reason =
|
|
1570
|
+
diffStatus === "source-unavailable"
|
|
1571
|
+
? "The apply target's current source is not accessible to the analyzer, so a Before/After cannot be cited."
|
|
1572
|
+
: "The failure origin could not be pinned to a concrete source location.";
|
|
1573
|
+
return `<div class="r-diff-caveat" data-diff-status="${escapeHtml(diffStatus)}">
|
|
1574
|
+
<div class="r-diff-caveat-label">Source not found — hypothesis</div>
|
|
1575
|
+
<p>${escapeHtml(reason)} No diff is shown; apply this remedy as a hypothesis and verify against the live target.</p>
|
|
1576
|
+
</div>`;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
export function renderRemedyCard(remedy: Remedy, findingId: string, finding?: Finding): string {
|
|
1580
|
+
const isRecommended = remedy.rank === 1;
|
|
1581
|
+
const rid = remedy.remedyId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1582
|
+
|
|
1583
|
+
// ── Checkbox (top-right, part of header row) ──
|
|
1584
|
+
const checkbox = `<input type="checkbox" class="remedy-cb" data-id="${escapeHtml(remedy.remedyId)}" data-finding="${escapeHtml(findingId)}"${isRecommended ? " checked" : ""}>`;
|
|
1585
|
+
|
|
1586
|
+
// ── Header row ──
|
|
1587
|
+
// W12-04: the checkbox is a direct child of `.remedy` (a flex ROW:
|
|
1588
|
+
// `[checkbox] [.remedy-body]`), NOT inside the header. The header is part of
|
|
1589
|
+
// the `.remedy-body` column. This restores the `[checkbox][.remedy-body]`
|
|
1590
|
+
// structure the orphaned `.remedy-body{flex:1}` rule + the template's
|
|
1591
|
+
// `.remedy-body`-targeting note-injection JS were written for.
|
|
1592
|
+
// D-1 render guard: never interpolate a bare `remedy.rank` — absent ⇒ "n/a", not "undefined".
|
|
1593
|
+
const rankPill = `<span class="rank-pill">RANK ${badgeField(remedy.rank)}</span>`;
|
|
1594
|
+
const header = `<div class="remedy-header"><div class="remedy-rank-id">${rankPill} <span class="remedy-id">${escapeHtml(remedy.remedyId)}</span></div><div class="remedy-what">${escapeHtml(remedy.title)}</div></div>`;
|
|
1595
|
+
|
|
1596
|
+
// ── Meta strip ──
|
|
1597
|
+
// D-1 render guard: cost/correctness drive BOTH the pill text and a CSS class.
|
|
1598
|
+
// Guard both surfaces so a missing field never prints `undefined` nor produces
|
|
1599
|
+
// a `b-cost-undefined` class. The class helper for cost is inlined here.
|
|
1600
|
+
const costClass = remedy.cost ? `b-cost-${remedy.cost}` : "b-cost-na";
|
|
1601
|
+
const costPill = `<span class="${costClass}">cost:${badgeField(remedy.cost)}</span>`;
|
|
1602
|
+
const correctnessPill = `<span class="${correctnessPillClass(remedy.correctness)}">correct:${badgeField(remedy.correctness)}</span>`;
|
|
1603
|
+
const applyLabel = remedy.applyTarget ?? "";
|
|
1604
|
+
const applyPill = applyLabel
|
|
1605
|
+
? `<span class="apply-pill ${applyPillClass(applyLabel)}">${escapeHtml(applyLabel)}</span>`
|
|
1606
|
+
: "";
|
|
1607
|
+
const tclassVal = remedy.targetClass ?? "";
|
|
1608
|
+
const tclassPill = tclassVal
|
|
1609
|
+
? `<span class="tclass-pill ${tclassPillClass(tclassVal)}">${escapeHtml(tclassVal)}</span>`
|
|
1610
|
+
: "";
|
|
1611
|
+
const ctypePill = remedy.changeType
|
|
1612
|
+
? `<span class="ctype-pill ${ctypePillClass(remedy.changeType)}">${remedy.changeType}</span>`
|
|
1613
|
+
: "";
|
|
1614
|
+
const metaStrip = `<div class="remedy-meta">${costPill}${correctnessPill}${tclassPill}${ctypePill}${applyPill}</div>`;
|
|
1615
|
+
|
|
1616
|
+
// ── Purple r-rationale (D1) ──
|
|
1617
|
+
const rationaleText = remedy.rationale ?? `${escapeHtml(remedy.failureOrigin.what)}: ${escapeHtml(remedy.failureOrigin.why)} at ${escapeHtml(remedy.failureOrigin.where)}`;
|
|
1618
|
+
const rationaleBlock = `<div class="r-rationale"><span class="r-block-label">Why this remedy</span><p>${remedy.rationale ? escapeHtml(remedy.rationale) : rationaleText}</p></div>`;
|
|
1619
|
+
|
|
1620
|
+
// ── Cyan r-why-works (D1) ──
|
|
1621
|
+
const whyWorksText = remedy.whyWorks ?? "(causal mechanism not documented)";
|
|
1622
|
+
const whyWorksBlock = `<div class="r-why-works"><span class="r-block-label">Why this works</span><p>${escapeHtml(whyWorksText)}</p></div>`;
|
|
1623
|
+
|
|
1624
|
+
// ── Dashed r-target row (IFF applyTarget) ──
|
|
1625
|
+
const targetRow = applyLabel
|
|
1626
|
+
? `<div class="r-target"><span class="r-target-label">Apply target</span><code>${escapeHtml(applyLabel)}</code></div>`
|
|
1627
|
+
: "";
|
|
1628
|
+
|
|
1629
|
+
// ── r-diff-grid 2-col Before/After (IFF diff) (D3) ──
|
|
1630
|
+
// W12-08: when diff is absent but diffStatus is set, render a labeled
|
|
1631
|
+
// "source not found — hypothesis" caveat block instead of the Before/After
|
|
1632
|
+
// grid (renderDiffStatusCaveat). NEVER a fabricated diff (PR-052 proposed).
|
|
1633
|
+
let diffGrid = "";
|
|
1634
|
+
if (remedy.diff) {
|
|
1635
|
+
diffGrid = `<div class="r-diff-grid">
|
|
1636
|
+
<div class="diff-col"><div class="diff-label label-before">Before</div><pre>${escapeHtml(remedy.diff.before)}</pre></div>
|
|
1637
|
+
<div class="diff-col"><div class="diff-label label-after">After</div><pre>${escapeHtml(remedy.diff.after)}</pre></div>
|
|
1638
|
+
</div>`;
|
|
1639
|
+
} else if (remedy.diffStatus) {
|
|
1640
|
+
diffGrid = renderDiffStatusCaveat(remedy.diffStatus);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// ── r-apply-grid 2-col Apply plan + Apply instructions (IFF plan OR applyInstructions) (D4) ──
|
|
1644
|
+
let applyGrid = "";
|
|
1645
|
+
if (remedy.plan || (remedy.applyInstructions && remedy.applyInstructions.length > 0)) {
|
|
1646
|
+
const planCol = remedy.plan
|
|
1647
|
+
? `<div class="apply-col">
|
|
1648
|
+
<div class="apply-col-label">Apply plan</div>
|
|
1649
|
+
<ul class="r-apply-plan">
|
|
1650
|
+
${remedy.plan.files.map((f) => `<li><code>${escapeHtml(f.action)}</code> ${escapeHtml(f.path)}${f.lineRange ? ` <span class="r-line-range">(${escapeHtml(f.lineRange)})</span>` : ""}</li>`).join("\n ")}
|
|
1651
|
+
</ul>
|
|
1652
|
+
${remedy.plan.verify.length > 0 ? `<div class="r-verify-label">Verify</div><ul class="r-apply-plan">${remedy.plan.verify.map((v) => `<li><code>${escapeHtml(v)}</code></li>`).join("")}</ul>` : ""}
|
|
1653
|
+
${remedy.plan.acceptance ? `<div class="r-acceptance">${escapeHtml(remedy.plan.acceptance)}</div>` : ""}
|
|
1654
|
+
${remedy.plan.commitMessage ? `<div class="r-commit"><code>${escapeHtml(remedy.plan.commitMessage)}</code></div>` : ""}
|
|
1655
|
+
</div>`
|
|
1656
|
+
: `<div class="apply-col apply-col-empty"><span class="r-block-label">Apply plan</span><p class="r-muted">No structured plan provided.</p></div>`;
|
|
1657
|
+
|
|
1658
|
+
const instrCol =
|
|
1659
|
+
remedy.applyInstructions && remedy.applyInstructions.length > 0
|
|
1660
|
+
? `<div class="apply-col">
|
|
1661
|
+
<div class="apply-col-label">Apply instructions</div>
|
|
1662
|
+
<ol class="r-apply-instr">
|
|
1663
|
+
${remedy.applyInstructions.map((step) => `<li>${escapeHtml(step)}</li>`).join("\n ")}
|
|
1664
|
+
</ol>
|
|
1665
|
+
</div>`
|
|
1666
|
+
: `<div class="apply-col apply-col-empty"><span class="r-block-label">Apply instructions</span><p class="r-muted">No step-by-step instructions provided.</p></div>`;
|
|
1667
|
+
|
|
1668
|
+
applyGrid = `<div class="r-apply-grid">${planCol}${instrCol}</div>`;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// ── W9-F L3: fix-feedback history (feedbackOnFix[]) ──
|
|
1672
|
+
const fixFeedbackHtml = renderFeedbackOnFixBlock(remedy);
|
|
1673
|
+
|
|
1674
|
+
// ── Remedy notes textarea (D7) ──
|
|
1675
|
+
const notesArea = `<textarea class="remedy-notes" placeholder="Feedback on this remedy — overrides, conditions, why-not, modifications. Merged verbatim into the master Copy decisions bundle."></textarea>`;
|
|
1676
|
+
|
|
1677
|
+
// ── Hidden payload script (CC-08) ──
|
|
1678
|
+
const payloadScript = finding
|
|
1679
|
+
? `<script type="text/plain" id="payload-${escapeHtml(rid)}">${escapeHtml(buildRemedyMarkdownBundle(remedy, finding))}</script>`
|
|
1680
|
+
: "";
|
|
1681
|
+
|
|
1682
|
+
// W12-04: `.remedy` is a flex ROW of [checkbox][.remedy-body]; `.remedy-body`
|
|
1683
|
+
// is a flex COLUMN that stacks the ~9 content blocks full-width. Without the
|
|
1684
|
+
// wrapper the blocks laid out as a horizontal row crushed into narrow columns.
|
|
1685
|
+
// The hidden text/plain payload stays outside `.remedy-body` (not visible).
|
|
1686
|
+
return `<div class="remedy${isRecommended ? " recommended" : ""}">
|
|
1687
|
+
${payloadScript}
|
|
1688
|
+
${checkbox}
|
|
1689
|
+
<div class="remedy-body">
|
|
1690
|
+
${header}
|
|
1691
|
+
${metaStrip}
|
|
1692
|
+
${rationaleBlock}
|
|
1693
|
+
${whyWorksBlock}
|
|
1694
|
+
${targetRow}
|
|
1695
|
+
${diffGrid}
|
|
1696
|
+
${applyGrid}
|
|
1697
|
+
${fixFeedbackHtml}
|
|
1698
|
+
${notesArea}
|
|
1699
|
+
</div>
|
|
1700
|
+
</div>`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// ── renderFeedbackBlock (PRD-CC-04, D5/D9) ────────────────────────────────────
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* PRD-CC-04 (D5): Render the feedback-grounding block for a finding.
|
|
1707
|
+
* Displayed between Problem and Evidence. Returns "" when no sources.
|
|
1708
|
+
* Legacy: when feedbackSources is absent, auto-promotes userFeedback to
|
|
1709
|
+
* a single chat source entry (PRD-CC-02 backward-compat).
|
|
1710
|
+
* NO emojis on the header or source-type labels (D9).
|
|
1711
|
+
*/
|
|
1712
|
+
export function renderFeedbackBlock(finding: Finding): string {
|
|
1713
|
+
// Resolve sources: prefer feedbackSources, fall back to userFeedback promotion.
|
|
1714
|
+
let sources: FeedbackSource[] = finding.feedbackSources ?? [];
|
|
1715
|
+
if (sources.length === 0 && finding.userFeedback) {
|
|
1716
|
+
sources = [
|
|
1717
|
+
{
|
|
1718
|
+
sourceType: "chat",
|
|
1719
|
+
provenance: "legacy userFeedback field",
|
|
1720
|
+
body: finding.userFeedback,
|
|
1721
|
+
},
|
|
1722
|
+
];
|
|
1723
|
+
}
|
|
1724
|
+
if (sources.length === 0) return "";
|
|
1725
|
+
|
|
1726
|
+
const prettyLabel = (s: FeedbackSource): string => {
|
|
1727
|
+
if (s.sourceType === "chat") return "Operator chat";
|
|
1728
|
+
if (s.sourceType === "trace-score") return "Langfuse trace score";
|
|
1729
|
+
return s.externalPlatform ? `External: ${s.externalPlatform}` : "External feedback platform";
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
const items = sources
|
|
1733
|
+
.map((s) => {
|
|
1734
|
+
const scoreNote =
|
|
1735
|
+
s.score !== undefined
|
|
1736
|
+
? ` <span class="fb-score-note">${escapeHtml(s.score.name)}: ${escapeHtml(String(s.score.value))}${s.score.scorerType ? ` (${escapeHtml(s.score.scorerType)})` : ""}</span>`
|
|
1737
|
+
: "";
|
|
1738
|
+
const traceNote = s.traceId
|
|
1739
|
+
? ` · trace <code>${escapeHtml(s.traceId)}</code>`
|
|
1740
|
+
: "";
|
|
1741
|
+
const timeNote = s.capturedAt ? ` · ${escapeHtml(s.capturedAt)}` : "";
|
|
1742
|
+
return `<div class="fb-item fb-${s.sourceType}">
|
|
1743
|
+
<div class="fb-head"><span class="fb-source-type">${escapeHtml(prettyLabel(s))}</span> · ${escapeHtml(s.provenance)}${scoreNote}${traceNote}${timeNote}</div>
|
|
1744
|
+
<blockquote>${escapeHtml(s.body)}</blockquote>
|
|
1745
|
+
</div>`;
|
|
1746
|
+
})
|
|
1747
|
+
.join("\n");
|
|
1748
|
+
|
|
1749
|
+
return `<div class="feedback-list">
|
|
1750
|
+
<h3>Feedback grounding this finding <span class="r-count">(${sources.length} source${sources.length !== 1 ? "s" : ""})</span></h3>
|
|
1751
|
+
${items}
|
|
1752
|
+
</div>`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// ── renderFindingPanel (gold-standard finding panel) ─────────────────────────
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Wave-5 R1.2: Render a single finding as a gold-standard panel.
|
|
1759
|
+
* Anatomy: severity-badged h2 + sub + worst-case callout + taxonomy chips +
|
|
1760
|
+
* Problem (.f-desc) + Evidence + Why-chain (.whychain w/ origin marker) +
|
|
1761
|
+
* Assumptions block + Remedies (ranked, rank-1 recommended).
|
|
1762
|
+
*
|
|
1763
|
+
* Panel id scheme: client findings get t2..tN+1 (Overview is t1).
|
|
1764
|
+
*/
|
|
1765
|
+
/**
|
|
1766
|
+
* Wave-6 R2.5: Render the per-finding sampling-representativeness proof. Shown
|
|
1767
|
+
* BELOW the why-chain. WARN-only — a "low" coverageConfidence surfaces a caveat
|
|
1768
|
+
* banner but never blocks. Returns "" when the finding carries no coverageProof.
|
|
1769
|
+
*
|
|
1770
|
+
* Renders: a confidence headline (value + level pill), the 4-dimension coverage
|
|
1771
|
+
* table (latency · score · temporal · tool-trajectory), and a population-vs-sample
|
|
1772
|
+
* bias row (mean-badness proof). The colour cue derives from `level` only.
|
|
1773
|
+
*
|
|
1774
|
+
* W12-13 (OP-4): when the per-finding proof was NOT computed (sampler did not
|
|
1775
|
+
* attach `coverageProof`), render an EXPLICIT "representativeness not computed"
|
|
1776
|
+
* marker rather than a blank slot — so the absence is legible, not silent.
|
|
1777
|
+
*/
|
|
1778
|
+
export function renderCoverageProof(f: Finding): string {
|
|
1779
|
+
const cp = f.coverageProof;
|
|
1780
|
+
if (!cp) {
|
|
1781
|
+
return `<div class="coverage-proof coverage-proof-absent">
|
|
1782
|
+
<h3>Sampling coverage — representativeness proof <span class="badge b-info">NOT COMPUTED</span></h3>
|
|
1783
|
+
<p class="sub">Per-finding representativeness was not computed for this finding (no per-finding sample proof attached). The run-level sampling methodology still applies; see the Methodology tab.</p>
|
|
1784
|
+
</div>`;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const levelPill =
|
|
1788
|
+
cp.level === "high"
|
|
1789
|
+
? `<span class="badge b-info" style="background:var(--g,#43c39a);">HIGH</span>`
|
|
1790
|
+
: cp.level === "medium"
|
|
1791
|
+
? `<span class="badge b-med">MEDIUM</span>`
|
|
1792
|
+
: `<span class="badge b-crit">LOW</span>`;
|
|
1793
|
+
|
|
1794
|
+
const lowBanner =
|
|
1795
|
+
cp.level === "low"
|
|
1796
|
+
? `<div class="warn">⚠ <strong>Low sampling coverage (${escapeHtml(cp.coverageConfidence.toFixed(1))}%).</strong> This finding's evidence may under-represent the population. Non-blocking — re-run with a wider sample or pass <code>--accept-low-confidence</code> to acknowledge. (R2.5)</div>`
|
|
1797
|
+
: "";
|
|
1798
|
+
|
|
1799
|
+
const dimRows = cp.dimensions
|
|
1800
|
+
.map(
|
|
1801
|
+
(d) =>
|
|
1802
|
+
`<tr><td>${escapeHtml(d.dimension)}</td><td>${d.coveredBuckets} / ${d.populationBuckets}</td><td>${escapeHtml(d.coveragePct.toFixed(1))}%</td></tr>`
|
|
1803
|
+
)
|
|
1804
|
+
.join("\n ");
|
|
1805
|
+
|
|
1806
|
+
const p = cp.population;
|
|
1807
|
+
const biasRow = `<p class="sub">Population bias proof — sampled <strong>${p.sampleSize}</strong> of <strong>${p.populationSize}</strong> traces (${escapeHtml((p.sampleFraction * 100).toFixed(1))}%). Mean-badness population <code>${escapeHtml(p.populationMeanBadness.toFixed(4))}</code> vs sample <code>${escapeHtml(p.sampleMeanBadness.toFixed(4))}</code>.</p>`;
|
|
1808
|
+
|
|
1809
|
+
return `<div class="coverage-proof">
|
|
1810
|
+
<h3>Sampling coverage — representativeness proof ${levelPill} <code>${escapeHtml(cp.coverageConfidence.toFixed(1))}%</code></h3>
|
|
1811
|
+
${lowBanner}
|
|
1812
|
+
<table>
|
|
1813
|
+
<thead><tr><th>Dimension</th><th>Buckets covered</th><th>Coverage</th></tr></thead>
|
|
1814
|
+
<tbody>
|
|
1815
|
+
${dimRows}
|
|
1816
|
+
</tbody>
|
|
1817
|
+
</table>
|
|
1818
|
+
${biasRow}
|
|
1819
|
+
</div>`;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* EV-1 (Wave-15): render a cited trace as a narration line rather than a bare
|
|
1824
|
+
* pointer. Shape: trace `<id>` — <whatHappened> with the optional `example`
|
|
1825
|
+
* appended as a quoted excerpt («…»). Block-0 (trace.ts) guarantees
|
|
1826
|
+
* `whatHappened` on every FailureOrigin / WhyChainEntry, but we degrade
|
|
1827
|
+
* gracefully if it is absent (e.g. legacy findings): fall back to the `evidence`
|
|
1828
|
+
* pointer alone so the panel never renders an empty narration.
|
|
1829
|
+
*
|
|
1830
|
+
* `traceId` is the cited trace for this slice (failureOrigin → referenceIds /
|
|
1831
|
+
* sourceTraceIds[0]; whyChain[] → the evidence pointer already carries it). When
|
|
1832
|
+
* no trace id is known the "trace `<id>` —" prefix is omitted, not faked.
|
|
1833
|
+
*
|
|
1834
|
+
* escapeHtml discipline (no raw interpolation) — every dynamic field is escaped.
|
|
1835
|
+
*/
|
|
1836
|
+
function renderTraceNarration(
|
|
1837
|
+
whatHappened: string | undefined,
|
|
1838
|
+
evidence: string,
|
|
1839
|
+
example: string | undefined,
|
|
1840
|
+
traceId?: string,
|
|
1841
|
+
): string {
|
|
1842
|
+
const narration =
|
|
1843
|
+
whatHappened && whatHappened.trim().length > 0
|
|
1844
|
+
? escapeHtml(whatHappened)
|
|
1845
|
+
: escapeHtml(evidence);
|
|
1846
|
+
const prefix =
|
|
1847
|
+
traceId && traceId.trim().length > 0
|
|
1848
|
+
? `trace <code>${escapeHtml(traceId)}</code> — `
|
|
1849
|
+
: "";
|
|
1850
|
+
const excerpt =
|
|
1851
|
+
example && example.trim().length > 0
|
|
1852
|
+
? ` <q class="ev-example">«${escapeHtml(example)}»</q>`
|
|
1853
|
+
: "";
|
|
1854
|
+
return `${prefix}${narration}${excerpt}`;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* DISMISSAL capture control — the per-finding "Valid / Invalid (not an issue)" verdict,
|
|
1859
|
+
* rendered at the FOOTER of each finding panel. It rides the EXISTING copy-back channel
|
|
1860
|
+
* (mirrors the remedy checkbox / notes / hidden-payload pattern) — NO new egress, NO new
|
|
1861
|
+
* button. Default UNCHECKED = Valid (a report never pre-dismisses a finding); checked =
|
|
1862
|
+
* Invalid → the operator's copy-back carries the dismissal, which the orchestrator folds
|
|
1863
|
+
* into the unified verdict ledger, suppressing the finding on the next run for this entity.
|
|
1864
|
+
*
|
|
1865
|
+
* The hidden `text/plain` payload carries the machine-readable record the copy-back reads
|
|
1866
|
+
* on paste-back (findingId + the failure-mode triple + severity) so the marks survive
|
|
1867
|
+
* without DOM scraping. `text/plain` scripts are SKIPPED by the render-js-syntax gate
|
|
1868
|
+
* (PR-050) — non-executable data, not JS. Placed at the footer (beyond the finalize-gate
|
|
1869
|
+
* finding-panel 1500-char window) so the taxonomy→Problem anchor scan is unaffected.
|
|
1870
|
+
*/
|
|
1871
|
+
export function renderDismissControl(f: Finding): string {
|
|
1872
|
+
const fidNorm = f.findingId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1873
|
+
const payload = {
|
|
1874
|
+
findingId: f.findingId,
|
|
1875
|
+
verdict: "dismissed",
|
|
1876
|
+
what: f.failureOrigin.what,
|
|
1877
|
+
why: f.failureOrigin.why,
|
|
1878
|
+
where: f.failureOrigin.where,
|
|
1879
|
+
whatHappened: f.failureOrigin.whatHappened,
|
|
1880
|
+
severity: f.severity ?? "info",
|
|
1881
|
+
};
|
|
1882
|
+
const payloadScript = `<script type="text/plain" id="dismiss-payload-${escapeHtml(fidNorm)}">${escapeHtml(JSON.stringify(payload))}</script>`;
|
|
1883
|
+
const checkbox = `<input type="checkbox" class="finding-invalid" data-finding="${escapeHtml(f.findingId)}">`;
|
|
1884
|
+
const reason = `<textarea class="dismiss-reason" placeholder="Why is this not an issue? Folded verbatim into the Copy decisions bundle."></textarea>`;
|
|
1885
|
+
return `<div class="finding-verdict">
|
|
1886
|
+
${payloadScript}
|
|
1887
|
+
<label class="fv-toggle">${checkbox} <span class="fv-label">Invalid — not an issue (dismiss & suppress next run)</span></label>
|
|
1888
|
+
${reason}
|
|
1889
|
+
</div>`;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
export function renderFindingPanel(f: Finding, panelIdx: number): string {
|
|
1893
|
+
const panelId = `t${panelIdx + 2}`;
|
|
1894
|
+
const sev = severityBadgeClass(f.severity, f.failureOrigin.confidence);
|
|
1895
|
+
const sevLabel = sev === "crit" ? "CRITICAL" : sev === "high" ? "HIGH" : sev === "med" ? "MEDIUM" : "RULE-OUT";
|
|
1896
|
+
|
|
1897
|
+
const title = escapeHtml(f.title ?? f.actionable.slice(0, 90));
|
|
1898
|
+
const sub = f.subDesc ? `<p class="sub">${escapeHtml(f.subDesc)}</p>` : "";
|
|
1899
|
+
const worstCase = f.worstCaseCallout ? `<div class="crit"><strong>Worst:</strong> ${f.worstCaseCallout}</div>` : "";
|
|
1900
|
+
|
|
1901
|
+
// Taxonomy chips (WHAT/WHY/WHERE + apply + confidence).
|
|
1902
|
+
const applyLabel = f.applyLabel ?? "";
|
|
1903
|
+
const taxonomy = `<div class="taxonomy">
|
|
1904
|
+
<span class="tax-chip"><strong>WHAT</strong>: ${escapeHtml(f.failureOrigin.what)}</span>
|
|
1905
|
+
<span class="tax-chip"><strong>WHY</strong>: ${escapeHtml(f.failureOrigin.why)}</span>
|
|
1906
|
+
<span class="tax-chip"><strong>WHERE</strong>: ${escapeHtml(f.failureOrigin.where)}</span>
|
|
1907
|
+
${applyLabel ? `<span class="tax-chip"><strong>APPLY</strong>: ${escapeHtml(applyLabel)}</span>` : ""}
|
|
1908
|
+
<span class="tax-chip"><strong>Confidence</strong>: ${escapeHtml(f.failureOrigin.confidence)}</span>
|
|
1909
|
+
</div>`;
|
|
1910
|
+
|
|
1911
|
+
// Problem prose — PRIMARY block (W18-problem): ALWAYS rendered, at the TOP of the
|
|
1912
|
+
// panel (before Evidence / why-chain / remedies), from the descriptive `f.problem`
|
|
1913
|
+
// statement. The fallback to the action-biased `f.actionable` is KILLED: if `problem`
|
|
1914
|
+
// is somehow absent we emit a LOUD placeholder rather than silently showing a todo.
|
|
1915
|
+
const problemBody =
|
|
1916
|
+
f.problem && f.problem.trim().length > 0
|
|
1917
|
+
? f.problem
|
|
1918
|
+
: `<strong class="problem-missing">PROBLEM STATEMENT MISSING</strong>`;
|
|
1919
|
+
const problem = `<h3>Problem</h3>
|
|
1920
|
+
<div class="f-desc">${problemBody}</div>`;
|
|
1921
|
+
|
|
1922
|
+
// PRD-CC-04: Feedback block between Problem and Evidence (D5).
|
|
1923
|
+
// L1: raw feedbackSources (or legacy userFeedback promotion)
|
|
1924
|
+
const feedbackBlock = renderFeedbackBlock(f);
|
|
1925
|
+
// W9-F L2: translated feedback (raw symptom → component-level RCA target)
|
|
1926
|
+
const translatedFeedbackBlock = renderTranslatedFeedbackBlock(f);
|
|
1927
|
+
|
|
1928
|
+
// Evidence. EV-1: when no pre-rendered evidenceHtml is supplied, narrate the
|
|
1929
|
+
// cited trace ("trace <id> — <whatHappened> «example»") instead of a bare pointer.
|
|
1930
|
+
const citedTraceId = f.referenceIds?.traceId ?? f.sourceTraceIds[0];
|
|
1931
|
+
const evidence = `<h3>Evidence</h3>
|
|
1932
|
+
${
|
|
1933
|
+
f.evidenceHtml
|
|
1934
|
+
? f.evidenceHtml
|
|
1935
|
+
: `<p class="ev-narration">${renderTraceNarration(
|
|
1936
|
+
f.failureOrigin.whatHappened,
|
|
1937
|
+
f.failureOrigin.evidence,
|
|
1938
|
+
f.failureOrigin.example,
|
|
1939
|
+
citedTraceId,
|
|
1940
|
+
)}</p>`
|
|
1941
|
+
}`;
|
|
1942
|
+
|
|
1943
|
+
// Why-chain. EV-1: each step narrates what happened in the cited trace via
|
|
1944
|
+
// whatHappened (+ optional «example»); the evidence pointer is the trace id.
|
|
1945
|
+
const whyChainItems = f.whyChain
|
|
1946
|
+
.map(
|
|
1947
|
+
(w) =>
|
|
1948
|
+
`<li class="${w.isOrigin ? "origin" : ""}">${escapeHtml(w.why)}<em>${renderTraceNarration(
|
|
1949
|
+
w.whatHappened,
|
|
1950
|
+
w.evidence,
|
|
1951
|
+
w.example,
|
|
1952
|
+
w.evidence,
|
|
1953
|
+
)}</em></li>`
|
|
1954
|
+
)
|
|
1955
|
+
.join("\n ");
|
|
1956
|
+
const whyChain = f.whyChain.length > 0
|
|
1957
|
+
? `<h3>Why-chain</h3>
|
|
1958
|
+
<ol class="whychain">
|
|
1959
|
+
${whyChainItems}
|
|
1960
|
+
</ol>`
|
|
1961
|
+
: "";
|
|
1962
|
+
|
|
1963
|
+
// R2.5: sampling-representativeness proof — rendered directly below the why-chain.
|
|
1964
|
+
const coverageProof = renderCoverageProof(f);
|
|
1965
|
+
|
|
1966
|
+
// Assumptions block.
|
|
1967
|
+
const assumptions = renderAssumptionsBlock(f);
|
|
1968
|
+
|
|
1969
|
+
// Remedies — pass finding ref to renderRemedyCard for hidden payload scripts (CC-08).
|
|
1970
|
+
const remedies = f.remedies.length > 0
|
|
1971
|
+
? `<h3>Remedies</h3>
|
|
1972
|
+
${f.remedies.map((r) => renderRemedyCard(r, f.findingId, f)).join("\n ")}`
|
|
1973
|
+
: "";
|
|
1974
|
+
|
|
1975
|
+
// W18-problem: Problem is the PRIMARY content block of the finding — rendered at the
|
|
1976
|
+
// TOP of the finding body, BEFORE feedback / Evidence / why-chain / remedies. It sits
|
|
1977
|
+
// immediately after the taxonomy chip-strip (head matter: title + sub + worstCase
|
|
1978
|
+
// callout + WHAT/WHY/WHERE chips). Keeping Problem adjacent-after the taxonomy anchor
|
|
1979
|
+
// also satisfies the finalize-gate `finding-panel` row, which scans forward from
|
|
1980
|
+
// `<div class="taxonomy">` for the required "Problem" marker.
|
|
1981
|
+
return `
|
|
1982
|
+
<section class="panel" id="${panelId}">
|
|
1983
|
+
<h2><span class="badge b-${sev}">${sevLabel}</span> ${escapeHtml(f.findingId)} — ${title}</h2>
|
|
1984
|
+
${sub}
|
|
1985
|
+
${worstCase}
|
|
1986
|
+
${taxonomy}
|
|
1987
|
+
${problem}
|
|
1988
|
+
${feedbackBlock}
|
|
1989
|
+
${translatedFeedbackBlock}
|
|
1990
|
+
${evidence}
|
|
1991
|
+
${whyChain}
|
|
1992
|
+
${coverageProof}
|
|
1993
|
+
${assumptions}
|
|
1994
|
+
${remedies}
|
|
1995
|
+
${renderDismissControl(f)}
|
|
1996
|
+
</section>`;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// ── renderMetaReportPanels (SD cluster-grouped panels) ───────────────────────
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* SD self-diag: render cluster panels — one panel per failureOrigin.what cluster.
|
|
2003
|
+
* Dedup findings within a cluster by (traceId, what+why+where). Each finding inside
|
|
2004
|
+
* the cluster renders the same gold-standard depth (taxonomy + evidence + assumptions
|
|
2005
|
+
* + remedies) as the per-finding client panels.
|
|
2006
|
+
*/
|
|
2007
|
+
function renderMetaReportPanels(findings: Finding[]): string {
|
|
2008
|
+
const clusters = clusterFindings(findings);
|
|
2009
|
+
let html = "";
|
|
2010
|
+
let clusterIdx = 0;
|
|
2011
|
+
for (const [what, group] of clusters) {
|
|
2012
|
+
const panelId = `tcluster-${clusterIdx}`;
|
|
2013
|
+
const deduped = dedupFindings(group);
|
|
2014
|
+
const subPanels = deduped
|
|
2015
|
+
.map((f) => {
|
|
2016
|
+
const sev = severityBadgeClass(f.severity, f.failureOrigin.confidence);
|
|
2017
|
+
// SD-6: parity with renderFindingPanel — the meta sub-panel must carry the
|
|
2018
|
+
// WHAT chip (it previously had only WHY/WHERE/Confidence) so the
|
|
2019
|
+
// finding-panel checklist row (require: taxonomy + WHAT + Problem) passes
|
|
2020
|
+
// on a meta-report WITHOUT a checklist exemption.
|
|
2021
|
+
const taxonomy = `<div class="taxonomy">
|
|
2022
|
+
<span class="tax-chip"><strong>WHAT</strong>: ${escapeHtml(f.failureOrigin.what)}</span>
|
|
2023
|
+
<span class="tax-chip"><strong>WHY</strong>: ${escapeHtml(f.failureOrigin.why)}</span>
|
|
2024
|
+
<span class="tax-chip"><strong>WHERE</strong>: ${escapeHtml(f.failureOrigin.where)}</span>
|
|
2025
|
+
<span class="tax-chip"><strong>Confidence</strong>: ${escapeHtml(f.failureOrigin.confidence)}</span>
|
|
2026
|
+
</div>`;
|
|
2027
|
+
// SD-6 + W18-problem: Problem block parity. renderFindingPanel emits an
|
|
2028
|
+
// <h3>Problem</h3> PRIMARY block from the descriptive `f.problem` statement with
|
|
2029
|
+
// NO actionable fallback. Mirror it here: render ALWAYS from `f.problem`; if
|
|
2030
|
+
// absent, emit the same LOUD placeholder rather than silently showing a todo.
|
|
2031
|
+
const metaProblemBody =
|
|
2032
|
+
f.problem && f.problem.trim().length > 0
|
|
2033
|
+
? f.problem
|
|
2034
|
+
: `<strong class="problem-missing">PROBLEM STATEMENT MISSING</strong>`;
|
|
2035
|
+
const problem = `<h3 style="font-size:13px;margin:8px 0 4px;">Problem</h3>
|
|
2036
|
+
<div class="f-desc">${metaProblemBody}</div>`;
|
|
2037
|
+
// EV-1: narrate the cited trace ("trace <id> — <whatHappened> «example»")
|
|
2038
|
+
// in the SD cluster panel too, instead of a bare evidence pointer.
|
|
2039
|
+
const sdTraceId = f.referenceIds?.traceId ?? f.sourceTraceIds[0];
|
|
2040
|
+
// W18-problem: Problem is the PRIMARY content block of the meta sub-panel —
|
|
2041
|
+
// rendered at the TOP of the finding body, BEFORE the evidence narration /
|
|
2042
|
+
// remedies. It sits immediately after the taxonomy chip-strip (after the
|
|
2043
|
+
// entity-head + title line), keeping it adjacent-after the `<div class="taxonomy">`
|
|
2044
|
+
// anchor so the finalize-gate `finding-panel` row finds the "Problem" marker.
|
|
2045
|
+
return `<div class="entity" style="margin-bottom:14px;">
|
|
2046
|
+
<div class="entity-head"><div class="entity-name" style="font-size:14px;">${escapeHtml(f.findingId)}</div><span class="badge b-${sev}">${escapeHtml(f.failureOrigin.confidence)}</span></div>
|
|
2047
|
+
<div class="f-desc">${escapeHtml(f.title ?? f.actionable)}</div>
|
|
2048
|
+
${taxonomy}
|
|
2049
|
+
${problem}
|
|
2050
|
+
<p class="ev-narration" style="font-size:12px;color:var(--muted);">${renderTraceNarration(
|
|
2051
|
+
f.failureOrigin.whatHappened,
|
|
2052
|
+
f.failureOrigin.evidence,
|
|
2053
|
+
f.failureOrigin.example,
|
|
2054
|
+
sdTraceId,
|
|
2055
|
+
)}</p>
|
|
2056
|
+
${renderAssumptionsBlock(f)}
|
|
2057
|
+
${f.remedies.map((r) => renderRemedyCard(r, f.findingId, f)).join("\n ")}
|
|
2058
|
+
</div>`;
|
|
2059
|
+
})
|
|
2060
|
+
.join("\n");
|
|
2061
|
+
|
|
2062
|
+
html += `
|
|
2063
|
+
<section class="panel" id="${panelId}">
|
|
2064
|
+
<h2>${escapeHtml(what)} <span style="font-size:13px;color:var(--dim);">(${deduped.length} finding${deduped.length !== 1 ? "s" : ""})</span></h2>
|
|
2065
|
+
${subPanels}
|
|
2066
|
+
</section>`;
|
|
2067
|
+
clusterIdx++;
|
|
2068
|
+
}
|
|
2069
|
+
return html;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/** Group findings by failureOrigin.what for the SD cluster layout. */
|
|
2073
|
+
function clusterFindings(findings: Finding[]): Map<string, Finding[]> {
|
|
2074
|
+
const clusters = new Map<string, Finding[]>();
|
|
2075
|
+
for (const f of findings) {
|
|
2076
|
+
const key = f.failureOrigin.what;
|
|
2077
|
+
if (!clusters.has(key)) clusters.set(key, []);
|
|
2078
|
+
clusters.get(key)!.push(f);
|
|
2079
|
+
}
|
|
2080
|
+
return clusters;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/** Dedup findings within a cluster by (traceId, what+why+where). */
|
|
2084
|
+
function dedupFindings(findings: Finding[]): Finding[] {
|
|
2085
|
+
const seen = new Set<string>();
|
|
2086
|
+
const out: Finding[] = [];
|
|
2087
|
+
for (const f of findings) {
|
|
2088
|
+
const traceId = f.referenceIds?.traceId ?? f.sourceTraceIds[0] ?? "";
|
|
2089
|
+
const key = `${traceId}|${f.failureOrigin.what}+${f.failureOrigin.why}+${f.failureOrigin.where}`;
|
|
2090
|
+
if (seen.has(key)) continue;
|
|
2091
|
+
seen.add(key);
|
|
2092
|
+
out.push(f);
|
|
2093
|
+
}
|
|
2094
|
+
return out;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// ── renderOverviewTab (gold-standard Overview) ───────────────────────────────
|
|
2098
|
+
|
|
2099
|
+
/**
|
|
2100
|
+
* Wave-5 R1.2: Render the Overview panel (t1).
|
|
2101
|
+
* Entity card + big-stat row + headline callout + signal census + scan funnel +
|
|
2102
|
+
* 24h heatmap + findings summary table + leverage callout.
|
|
2103
|
+
*/
|
|
2104
|
+
export function renderOverviewTab(input: RenderInput): string {
|
|
2105
|
+
// R2.6: when the operator gave a focus, this panel leads as 🎯 Guided (replacing
|
|
2106
|
+
// the neutral Overview survey). The focus drives the heading + a sub-line tooltip.
|
|
2107
|
+
const focus = input.runMeta?.parsedInvocation?.focus;
|
|
2108
|
+
const guided = typeof focus === "string" && focus.trim().length > 0;
|
|
2109
|
+
const defaultTitle = guided ? `🎯 Guided — ${escapeHtml(focus)}` : "① Overview";
|
|
2110
|
+
const title = input.overviewTitle ? escapeHtml(input.overviewTitle) : defaultTitle;
|
|
2111
|
+
const guidedSub = guided
|
|
2112
|
+
? `<p class="sub" title="Guided by operator focus: ${escapeHtml(focus)}">Operator focus: <code>${escapeHtml(focus)}</code> — this report leads with the focused view (R2.6).</p>`
|
|
2113
|
+
: "";
|
|
2114
|
+
const sub = (input.overviewSub ? `<p class="sub">${escapeHtml(input.overviewSub)}</p>` : "") + guidedSub;
|
|
2115
|
+
|
|
2116
|
+
const entityCard = input.diagnosedEntity ? renderEntityCard(input.diagnosedEntity) : "";
|
|
2117
|
+
const bigStat = input.bigStat ? renderBigStat(input.bigStat) : "";
|
|
2118
|
+
// W9-07: deep-read coverage tile sourced from RunMeta.deepRead (B1 telemetry)
|
|
2119
|
+
const deepReadTile = renderDeepReadTile(input.runMeta);
|
|
2120
|
+
const headline = input.overviewHeadline ? `<div class="crit"><strong>Headline:</strong> ${input.overviewHeadline}</div>` : "";
|
|
2121
|
+
|
|
2122
|
+
// W17-E (R7): residual surfacing — when the top discovered signal was capped at
|
|
2123
|
+
// SECONDARY for lack of mechanical evidence and primary fell back to a cheap signal,
|
|
2124
|
+
// render the caveat BEFORE the census so the reader sees it ahead of the ★ PRIMARY
|
|
2125
|
+
// badge (never silently present the cheap fallback as the confident primary).
|
|
2126
|
+
const suspectedNote = renderSuspectedPrimaryNote(input.runMeta);
|
|
2127
|
+
|
|
2128
|
+
// W11-01: annotate the census heading with primarySignal.why when available.
|
|
2129
|
+
const primaryWhy = input.runMeta?.primarySignal?.why;
|
|
2130
|
+
const censusSub = primaryWhy
|
|
2131
|
+
? `<p style="font-size:12px;color:var(--muted);margin:2px 0 6px;">${escapeHtml(primaryWhy)}</p>`
|
|
2132
|
+
: "";
|
|
2133
|
+
const census = input.signalCensus
|
|
2134
|
+
? `<h3>Signal census → primary selected by failure-validity gate + impact×prevalence + deep-read corroboration</h3>
|
|
2135
|
+
${censusSub}
|
|
2136
|
+
${renderSignalCensus(input.signalCensus)}`
|
|
2137
|
+
: "";
|
|
2138
|
+
|
|
2139
|
+
const funnel = input.scanFunnel ? renderScanFunnel(input.scanFunnel) : "";
|
|
2140
|
+
// W11-02: pass runMeta so heatmap can annotate primary-signal cells.
|
|
2141
|
+
// F3 (UR-2): caption follows the active metric (default = avg latency).
|
|
2142
|
+
const heatMetricLabel = input.hourlyHeatmap?.metric?.label ?? "avg latency";
|
|
2143
|
+
const heatmap = input.hourlyHeatmap
|
|
2144
|
+
? `<h3>24h timeline heatmap (colour = ${escapeHtml(heatMetricLabel)}, number = trace count)</h3>
|
|
2145
|
+
${render24hHeatmap(input.hourlyHeatmap, input.runMeta)}`
|
|
2146
|
+
: "";
|
|
2147
|
+
|
|
2148
|
+
const findingsTable = renderFindingsSummaryTable(input.findings);
|
|
2149
|
+
const leverage = input.overviewLeverage ? `<div class="alert"><strong>Highest-leverage:</strong> ${input.overviewLeverage}</div>` : "";
|
|
2150
|
+
|
|
2151
|
+
// Overview is the active tab only when there is no internal Methodology tab.
|
|
2152
|
+
// D-8: a methodologyAudit is a meta report, so treat it as such here.
|
|
2153
|
+
const effectiveMeta = (input.isMetaReport ?? false) || (input.methodologyAudit ?? false);
|
|
2154
|
+
const isActive = (input.audience ?? "client") === "internal" && !effectiveMeta ? "" : " active";
|
|
2155
|
+
|
|
2156
|
+
return `<section class="panel${isActive}" id="t1">
|
|
2157
|
+
<h2>${title}</h2>
|
|
2158
|
+
${sub}
|
|
2159
|
+
${entityCard}
|
|
2160
|
+
${bigStat}
|
|
2161
|
+
${deepReadTile}
|
|
2162
|
+
${headline}
|
|
2163
|
+
${suspectedNote}
|
|
2164
|
+
${census}
|
|
2165
|
+
${funnel}
|
|
2166
|
+
${heatmap}
|
|
2167
|
+
${findingsTable}
|
|
2168
|
+
${leverage}
|
|
2169
|
+
</section>`;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* W11-03: Render the scan-coverage funnel.
|
|
2174
|
+
* Supports 4 segments (total → tier0 → sample → deep-read) when `funnel.sample`
|
|
2175
|
+
* is present; falls back to 3-segment display for backward-compat.
|
|
2176
|
+
*/
|
|
2177
|
+
function renderScanFunnel(funnel: ScanFunnel): string {
|
|
2178
|
+
const seg = (cls: string, s: { value: string; label: string; detail: string }) =>
|
|
2179
|
+
`<div class="seg ${cls}"><div class="fv">${escapeHtml(s.value)}</div><div class="fl">${escapeHtml(s.label)}</div><div class="fp">${escapeHtml(s.detail)}</div></div>`;
|
|
2180
|
+
const sampleSeg = funnel.sample ? `\n ${seg("s-sample", funnel.sample)}` : "";
|
|
2181
|
+
return `<h3>Scan coverage</h3>
|
|
2182
|
+
<div class="funnel">
|
|
2183
|
+
${seg("s-total", funnel.total)}
|
|
2184
|
+
${seg("s-code", funnel.code)}${sampleSeg}
|
|
2185
|
+
${seg("s-llm", funnel.llm)}
|
|
2186
|
+
</div>`;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
/** Render the findings summary table on the Overview tab. */
|
|
2190
|
+
function renderFindingsSummaryTable(findings: Finding[]): string {
|
|
2191
|
+
if (findings.length === 0) return "";
|
|
2192
|
+
const rows = findings
|
|
2193
|
+
.map((f) => {
|
|
2194
|
+
const sev = severityBadgeClass(f.severity, f.failureOrigin.confidence);
|
|
2195
|
+
const sevLabel = sev === "crit" ? "CRIT" : sev === "high" ? "HIGH" : sev === "med" ? "MED" : "RULE-OUT";
|
|
2196
|
+
const applyLabel = f.applyLabel ?? "";
|
|
2197
|
+
const applyPill = applyLabel
|
|
2198
|
+
? `<span class="apply-pill ${applyPillClass(applyLabel)}">${escapeHtml(applyLabel)}</span>`
|
|
2199
|
+
: "";
|
|
2200
|
+
const oneLiner = f.subDesc ?? f.title ?? f.actionable.slice(0, 80);
|
|
2201
|
+
return `<tr><td>${escapeHtml(f.findingId)}</td><td><span class="badge b-${sev}">${sevLabel}</span></td><td>${escapeHtml(f.failureOrigin.what)} / ${escapeHtml(f.failureOrigin.why)} / ${escapeHtml(f.failureOrigin.where)}</td><td>${applyPill}</td><td>${escapeHtml(oneLiner)}</td></tr>`;
|
|
2202
|
+
})
|
|
2203
|
+
.join("\n ");
|
|
2204
|
+
return `<table>
|
|
2205
|
+
<thead><tr><th>ID</th><th>Severity</th><th>WHAT/WHY/WHERE</th><th>Apply</th><th>One-liner</th></tr></thead>
|
|
2206
|
+
<tbody>
|
|
2207
|
+
${rows}
|
|
2208
|
+
</tbody>
|
|
2209
|
+
</table>`;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// ── renderDecisionsTab (gold-standard Decisions) ─────────────────────────────
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* PRD-CC-06: Render the Decisions panel — recommended bundle + general feedback +
|
|
2216
|
+
* live-preview textarea (D11). The live preview updates in real-time as remedies
|
|
2217
|
+
* are checked/unchecked and feedback is typed. The sticky global Copy button
|
|
2218
|
+
* (#copy-decisions) reads #lp-body.value and writes to clipboard.
|
|
2219
|
+
*/
|
|
2220
|
+
export function renderDecisionsTab(input: RenderInput): string {
|
|
2221
|
+
const bundle = input.decisionsBundle
|
|
2222
|
+
? `<div class="alert"><strong>Recommended bundle:</strong> ${input.decisionsBundle}</div>`
|
|
2223
|
+
: "";
|
|
2224
|
+
|
|
2225
|
+
// SD-06: self-diag subject header row in Decisions
|
|
2226
|
+
// D-8: a methodologyAudit is also a self-diag report.
|
|
2227
|
+
const subjectRow = input.isMetaReport || input.methodologyAudit
|
|
2228
|
+
? `<div class="decisions-subject-row"><span class="r-block-label">Subject</span> skill source (skill maintainer mode — PR-022)</div>`
|
|
2229
|
+
: "";
|
|
2230
|
+
|
|
2231
|
+
return `<section class="panel" id="tdecisions">
|
|
2232
|
+
<div class="decisions-cta-bar"><button id="copy-decisions" title="Copy the approved-remedies bundle as markdown for handoff">Copy decisions as markdown</button></div>
|
|
2233
|
+
<h2>⑦ Decisions — copy approvals</h2>
|
|
2234
|
+
<p class="sub">★ green = recommended (pre-checked). Apply-target tags show change-type per remedy.</p>
|
|
2235
|
+
${subjectRow}
|
|
2236
|
+
${bundle}
|
|
2237
|
+
<div class="gfeedback">
|
|
2238
|
+
<h4>General feedback (speech-to-text friendly)</h4>
|
|
2239
|
+
<textarea id="general-feedback" id="general-feedback-dr" placeholder="Voice-dump anything here — approve a remedy AND add conditions, reject with rationale, request a remedy not listed, scope overrides, questions, priorities… Folded into the Copy-decisions export verbatim."></textarea>
|
|
2240
|
+
</div>
|
|
2241
|
+
<div class="live-preview" id="live-preview">
|
|
2242
|
+
<div class="lp-head"><span class="lp-label">Live Preview — Master Plan markdown</span><span class="lp-meta" id="lp-meta">0 remedies · 0 chars</span></div>
|
|
2243
|
+
<textarea class="lp-body" id="lp-body" readonly placeholder="Check remedies above and type feedback — bundle assembles here live. Click the sticky Copy to Clipboard button at the bottom of the page to copy."></textarea>
|
|
2244
|
+
</div>
|
|
2245
|
+
<p>Each remedy also has its own notes box. The live preview above updates as you select remedies and type feedback.</p>
|
|
2246
|
+
</section>`;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// ── renderLivePreviewScript (PRD-CC-07 — live preview + global Copy button) ───
|
|
2250
|
+
|
|
2251
|
+
/**
|
|
2252
|
+
* PRD-CC-07: Render the inline JS for the live preview + event wiring.
|
|
2253
|
+
* renderLivePreview() walks .remedy-cb:checked, prefers hidden payload-{id}
|
|
2254
|
+
* scripts over DOM extraction, appends .remedy-notes content (lines prefixed "> "),
|
|
2255
|
+
* appends general feedback, writes result to #lp-body and updates #lp-meta.
|
|
2256
|
+
* Wired to: .remedy-cb change, .remedy-notes input, #general-feedback input,
|
|
2257
|
+
* #general-feedback-dr input, DOMContentLoaded.
|
|
2258
|
+
* The #copy-decisions button reads #lp-body.value and writes to clipboard.
|
|
2259
|
+
*/
|
|
2260
|
+
export function renderLivePreviewScript(): string {
|
|
2261
|
+
return `<script>
|
|
2262
|
+
(function(){
|
|
2263
|
+
function renderLivePreview(){
|
|
2264
|
+
var checked = Array.from(document.querySelectorAll('.remedy-cb:checked'));
|
|
2265
|
+
var invalids = Array.from(document.querySelectorAll('.finding-invalid:checked'));
|
|
2266
|
+
var lines = [];
|
|
2267
|
+
if (checked.length === 0 && invalids.length === 0) {
|
|
2268
|
+
document.getElementById('lp-body').value = '';
|
|
2269
|
+
document.getElementById('lp-meta').textContent = '0 remedies · 0 chars';
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
if (checked.length > 0) {
|
|
2273
|
+
lines.push('# Approved Remedies (handoff)');
|
|
2274
|
+
lines.push('');
|
|
2275
|
+
lines.push('Generated: ' + new Date().toISOString());
|
|
2276
|
+
lines.push('');
|
|
2277
|
+
lines.push('---');
|
|
2278
|
+
lines.push('');
|
|
2279
|
+
checked.forEach(function(cb){
|
|
2280
|
+
var rid = (cb.dataset.id || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g,'-');
|
|
2281
|
+
var payloadEl = document.getElementById('payload-' + rid);
|
|
2282
|
+
if (payloadEl) {
|
|
2283
|
+
lines.push(payloadEl.textContent || '');
|
|
2284
|
+
} else {
|
|
2285
|
+
// DOM fallback
|
|
2286
|
+
var card = cb.closest('.remedy');
|
|
2287
|
+
if (!card) return;
|
|
2288
|
+
var fid = cb.dataset.finding || '?';
|
|
2289
|
+
var remedyId = cb.dataset.id || '?';
|
|
2290
|
+
var what = (card.querySelector('.remedy-what') || {textContent:''}).textContent.trim();
|
|
2291
|
+
lines.push('## ' + remedyId + ' (' + fid + ')');
|
|
2292
|
+
lines.push('**Remedy:** ' + what);
|
|
2293
|
+
lines.push('');
|
|
2294
|
+
}
|
|
2295
|
+
var note = card ? card.querySelector('.remedy-notes') : null;
|
|
2296
|
+
if (note && note.value.trim()) {
|
|
2297
|
+
note.value.trim().split(/\\r?\\n/).forEach(function(ln){ lines.push('> ' + ln); });
|
|
2298
|
+
lines.push('');
|
|
2299
|
+
}
|
|
2300
|
+
lines.push('---');
|
|
2301
|
+
lines.push('');
|
|
2302
|
+
});
|
|
2303
|
+
} // end if (checked.length > 0)
|
|
2304
|
+
// DISMISSAL capture — the SAME copy-back channel (no new egress). Each finding marked
|
|
2305
|
+
// "Invalid" folds its hidden dismiss-payload (findingId + failure-mode triple + severity)
|
|
2306
|
+
// + optional reason into the bundle; the orchestrator persists these to the unified
|
|
2307
|
+
// verdict ledger. This walk also lets a dismissal-only paste (0 remedies) still export.
|
|
2308
|
+
if (invalids.length > 0) {
|
|
2309
|
+
lines.push('## Invalid findings (dismissed — "not an issue")');
|
|
2310
|
+
lines.push('');
|
|
2311
|
+
invalids.forEach(function(cb){
|
|
2312
|
+
var fid = cb.dataset.finding || '?';
|
|
2313
|
+
var fidNorm = fid.toLowerCase().replace(/[^a-z0-9-]/g,'-');
|
|
2314
|
+
lines.push('- finding: ' + fid);
|
|
2315
|
+
var card = cb.closest('.panel');
|
|
2316
|
+
var reasonEl = card ? card.querySelector('.dismiss-reason') : null;
|
|
2317
|
+
if (reasonEl && reasonEl.value.trim()) {
|
|
2318
|
+
reasonEl.value.trim().split(/\\r?\\n/).forEach(function(ln){ lines.push(' reason: ' + ln); });
|
|
2319
|
+
}
|
|
2320
|
+
var payloadEl = document.getElementById('dismiss-payload-' + fidNorm);
|
|
2321
|
+
if (payloadEl) { lines.push(' ' + (payloadEl.textContent || '').trim()); }
|
|
2322
|
+
lines.push('');
|
|
2323
|
+
});
|
|
2324
|
+
lines.push('---');
|
|
2325
|
+
lines.push('');
|
|
2326
|
+
}
|
|
2327
|
+
var gf1 = document.getElementById('general-feedback');
|
|
2328
|
+
var gf2 = document.getElementById('general-feedback-dr');
|
|
2329
|
+
var gfVal = (gf1 && gf1.value.trim()) || (gf2 && gf2.value.trim()) || '';
|
|
2330
|
+
if (gfVal) {
|
|
2331
|
+
lines.push('## General feedback');
|
|
2332
|
+
lines.push('');
|
|
2333
|
+
lines.push(gfVal);
|
|
2334
|
+
lines.push('');
|
|
2335
|
+
}
|
|
2336
|
+
var text = lines.join('\\n');
|
|
2337
|
+
var lp = document.getElementById('lp-body');
|
|
2338
|
+
lp.value = text;
|
|
2339
|
+
document.getElementById('lp-meta').textContent = checked.length + ' remedies · ' + invalids.length + ' invalid · ' + text.length + ' chars';
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// Wire events
|
|
2343
|
+
function wireEvents(){
|
|
2344
|
+
document.querySelectorAll('.remedy-cb').forEach(function(cb){
|
|
2345
|
+
cb.addEventListener('change', renderLivePreview);
|
|
2346
|
+
});
|
|
2347
|
+
document.querySelectorAll('.remedy-notes').forEach(function(ta){
|
|
2348
|
+
ta.addEventListener('input', renderLivePreview);
|
|
2349
|
+
});
|
|
2350
|
+
document.querySelectorAll('.finding-invalid').forEach(function(cb){
|
|
2351
|
+
cb.addEventListener('change', renderLivePreview);
|
|
2352
|
+
});
|
|
2353
|
+
document.querySelectorAll('.dismiss-reason').forEach(function(ta){
|
|
2354
|
+
ta.addEventListener('input', renderLivePreview);
|
|
2355
|
+
});
|
|
2356
|
+
var gf1 = document.getElementById('general-feedback');
|
|
2357
|
+
var gf2 = document.getElementById('general-feedback-dr');
|
|
2358
|
+
if (gf1) gf1.addEventListener('input', renderLivePreview);
|
|
2359
|
+
if (gf2) gf2.addEventListener('input', renderLivePreview);
|
|
2360
|
+
renderLivePreview();
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
if (document.readyState === 'loading') {
|
|
2364
|
+
document.addEventListener('DOMContentLoaded', wireEvents);
|
|
2365
|
+
} else {
|
|
2366
|
+
wireEvents();
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// Override #copy-decisions to read from #lp-body (D11)
|
|
2370
|
+
function overrideCopyBtn(){
|
|
2371
|
+
var btn = document.getElementById('copy-decisions');
|
|
2372
|
+
if (!btn) return;
|
|
2373
|
+
// Remove old listeners by cloning
|
|
2374
|
+
var newBtn = btn.cloneNode(true);
|
|
2375
|
+
btn.parentNode.replaceChild(newBtn, btn);
|
|
2376
|
+
newBtn.addEventListener('click', function(){
|
|
2377
|
+
var lp = document.getElementById('lp-body');
|
|
2378
|
+
var text = lp ? lp.value : '';
|
|
2379
|
+
if (!text) { text = '# No remedies selected'; }
|
|
2380
|
+
var originalText = newBtn.textContent;
|
|
2381
|
+
navigator.clipboard.writeText(text).then(function(){
|
|
2382
|
+
newBtn.textContent = '✓ Copied';
|
|
2383
|
+
// Log to changelog if present
|
|
2384
|
+
var cl = document.getElementById('changelog-log');
|
|
2385
|
+
if (cl) {
|
|
2386
|
+
var row = document.createElement('tr');
|
|
2387
|
+
row.innerHTML = '<td>' + new Date().toLocaleTimeString() + '</td><td>Copied ' + text.length + ' chars to clipboard</td>';
|
|
2388
|
+
cl.prepend(row);
|
|
2389
|
+
}
|
|
2390
|
+
setTimeout(function(){ newBtn.textContent = originalText; }, 2200);
|
|
2391
|
+
}).catch(function(){
|
|
2392
|
+
window.prompt('Copy:', text);
|
|
2393
|
+
});
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
if (document.readyState === 'loading') {
|
|
2398
|
+
document.addEventListener('DOMContentLoaded', overrideCopyBtn);
|
|
2399
|
+
} else {
|
|
2400
|
+
overrideCopyBtn();
|
|
2401
|
+
}
|
|
2402
|
+
})();
|
|
2403
|
+
</script>`;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// ── renderFunnelHeatmap (legacy I-027 slot — backward compat) ────────────────
|
|
2407
|
+
|
|
2408
|
+
/**
|
|
2409
|
+
* I-027 backward-compat: the {{FUNNEL_HEATMAP_HTML}} slot. In the gold-standard
|
|
2410
|
+
* layout the funnel + heatmap live INSIDE the Overview tab, so this slot is
|
|
2411
|
+
* emitted empty unless a legacy template relies on it AND no Overview was built.
|
|
2412
|
+
* Returns "" for the gold-standard path (avoids double-render).
|
|
2413
|
+
*/
|
|
2414
|
+
export function renderFunnelHeatmap(): string {
|
|
2415
|
+
return "";
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// ── sanitizeMermaid (F-SELF-011 — mermaid v10 compatibility) ─────────────────
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* Sanitize orchestrator-authored mermaid before it lands in <div class="mermaid">.
|
|
2422
|
+
* Mermaid v10 lexer is strict: parens in participant aliases and raw HTML in Note
|
|
2423
|
+
* blocks cause silent render failure. This rewrites both into compatible forms.
|
|
2424
|
+
* Pure + deterministic. Emits a single console.warn listing what changed so the
|
|
2425
|
+
* author learns the constraints.
|
|
2426
|
+
*/
|
|
2427
|
+
export function sanitizeMermaid(src: string): { sanitized: string; rewrites: string[] } {
|
|
2428
|
+
const rewrites: string[] = [];
|
|
2429
|
+
let out = src;
|
|
2430
|
+
|
|
2431
|
+
// 1) Strip parens from participant alias lines: `participant X as Foo (bar)` → `participant X as Foo bar`
|
|
2432
|
+
out = out.replace(/^(\s*participant\s+\S+\s+as\s+)([^\n]+)$/gm, (_, prefix, alias) => {
|
|
2433
|
+
if (alias.includes("(") || alias.includes(")")) {
|
|
2434
|
+
const clean = alias.replace(/[()]/g, "").replace(/\s+/g, " ").trim();
|
|
2435
|
+
rewrites.push(`participant alias "${alias.trim()}" → "${clean}"`);
|
|
2436
|
+
return prefix + clean;
|
|
2437
|
+
}
|
|
2438
|
+
return prefix + alias;
|
|
2439
|
+
});
|
|
2440
|
+
|
|
2441
|
+
// 2) Split `<br/?>` inside Note over X,Y: lines into consecutive Note over lines.
|
|
2442
|
+
out = out.replace(/^(\s*Note\s+over\s+[^:]+:\s*)([^\n]+)$/gm, (_, prefix, body) => {
|
|
2443
|
+
if (!/<br\s*\/?>/i.test(body)) return prefix + body;
|
|
2444
|
+
const parts = body.split(/<br\s*\/?>/i).map((s: string) => s.trim()).filter(Boolean);
|
|
2445
|
+
rewrites.push(`<br/> in Note → ${parts.length} consecutive Note lines`);
|
|
2446
|
+
return parts.map((p: string) => `${prefix}${p}`).join("\n");
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
if (rewrites.length > 0) {
|
|
2450
|
+
console.warn(`[render] sanitizeMermaid rewrote ${rewrites.length} construct(s): ${rewrites.join("; ")}`);
|
|
2451
|
+
}
|
|
2452
|
+
return { sanitized: out, rewrites };
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// ── renderMethodologyTab (gold-standard Methodology [INTERNAL]) ──────────────
|
|
2456
|
+
|
|
2457
|
+
/**
|
|
2458
|
+
* Wave-5 R1.2: Render the Methodology [INTERNAL] panel (t0).
|
|
2459
|
+
* Mermaid sequence diagram + graded decision log + signal census table.
|
|
2460
|
+
* NODE-STRIPPED for client audience (FU-INT-1).
|
|
2461
|
+
*/
|
|
2462
|
+
export function renderMethodologyTab(input: RenderInput): string {
|
|
2463
|
+
const mermaidRaw = input.mermaidSequence ?? input.runMeta?.mermaidTopology;
|
|
2464
|
+
// F-SELF-011: sanitize orchestrator-authored mermaid (parens in participant
|
|
2465
|
+
// aliases + raw <br/> in Notes) before embedding — mermaid v10 silently fails otherwise.
|
|
2466
|
+
const mermaid = mermaidRaw !== undefined ? sanitizeMermaid(mermaidRaw).sanitized : undefined;
|
|
2467
|
+
const sequenceHtml = mermaid
|
|
2468
|
+
? `<h3>Sequence — orchestrator → scripts → analyzers</h3>
|
|
2469
|
+
<div class="mermaid">
|
|
2470
|
+
${mermaid}
|
|
2471
|
+
</div>`
|
|
2472
|
+
: "";
|
|
2473
|
+
|
|
2474
|
+
const decisionLogHtml = input.decisionLog && input.decisionLog.length > 0
|
|
2475
|
+
? `<h3>Decision log (graded)</h3>
|
|
2476
|
+
<table>
|
|
2477
|
+
<thead><tr><th>Decision</th><th>What the skill did</th><th>Grade</th></tr></thead>
|
|
2478
|
+
<tbody>
|
|
2479
|
+
${input.decisionLog
|
|
2480
|
+
.map((d) => `<tr><td>${escapeHtml(d.decision)}</td><td>${escapeHtml(d.what)}</td><td>${d.grade}</td></tr>`)
|
|
2481
|
+
.join("\n ")}
|
|
2482
|
+
</tbody>
|
|
2483
|
+
</table>`
|
|
2484
|
+
: "";
|
|
2485
|
+
|
|
2486
|
+
// W11-01/PR-049: annotate Methodology census heading with primarySignal rationale.
|
|
2487
|
+
const primarySig = input.runMeta?.primarySignal;
|
|
2488
|
+
const primarySigNote = primarySig
|
|
2489
|
+
? `<p style="font-size:12px;color:var(--muted);margin:2px 0 6px;">
|
|
2490
|
+
Selected: <strong>${escapeHtml(primarySig.name)}</strong> — ${escapeHtml(primarySig.why)}
|
|
2491
|
+
· confidence: <em>${escapeHtml(primarySig.confidence)}</em>
|
|
2492
|
+
${primarySig.ruledOut.length > 0 ? `· ruled out: ${primarySig.ruledOut.map((r) => escapeHtml(r)).join(", ")}` : ""}
|
|
2493
|
+
</p>`
|
|
2494
|
+
: "";
|
|
2495
|
+
// W17-E (R7): residual surfacing inside Methodology too — the floor cap is part of
|
|
2496
|
+
// the selection story, so the caveat sits with Step 1's census + selection cards.
|
|
2497
|
+
const suspectedNoteHtml = renderSuspectedPrimaryNote(input.runMeta);
|
|
2498
|
+
const censusHtml = input.signalCensus
|
|
2499
|
+
? `<h3>Step 1 — Primary-signal selection (PR-049: failure-validity gate → impact×prevalence → deep-read)</h3>
|
|
2500
|
+
${primarySigNote}
|
|
2501
|
+
${suspectedNoteHtml}
|
|
2502
|
+
${renderSignalCensus(input.signalCensus)}`
|
|
2503
|
+
: "";
|
|
2504
|
+
|
|
2505
|
+
// R2.6/D2: Methodology Step 0 — verbatim operator invocation (rendered first).
|
|
2506
|
+
const step0Html = renderOperatorInvocationStep(input.runMeta);
|
|
2507
|
+
|
|
2508
|
+
// F5 (defense-in-depth): on INTERNAL / META-audience reports, a widget whose data
|
|
2509
|
+
// was NOT threaded into runMeta renders a LOUD marker instead of vanishing silently
|
|
2510
|
+
// — so the next threading regression is visible, not invisible. CLIENT / PRODUCT
|
|
2511
|
+
// reports stay clean (these are internal-only nodes, node-stripped at publish).
|
|
2512
|
+
const isInternalAudience = (input.audience ?? "client") !== "client";
|
|
2513
|
+
const loudOrEmpty = (html: string, widget: string, threaded: boolean): string => {
|
|
2514
|
+
if (html.length > 0) return html;
|
|
2515
|
+
if (isInternalAudience && !threaded) {
|
|
2516
|
+
return `<div class="warn loud-missing-widget" data-widget="${escapeHtml(widget)}">⚠ ${escapeHtml(widget)} not threaded into runMeta — see orchestrator-protocol Step 8.5 (F4 widget-threading table).</div>`;
|
|
2517
|
+
}
|
|
2518
|
+
return html;
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
// R2.2: Methodology Step 1.5 — awareness-layer mini-sample + blind-spots.
|
|
2522
|
+
const awarenessThreaded =
|
|
2523
|
+
input.runMeta?.awarenessSample !== undefined ||
|
|
2524
|
+
(input.runMeta?.blindSpots !== undefined && input.runMeta.blindSpots.length > 0);
|
|
2525
|
+
// REQ-050: the empty-state must be HONEST. An absent awareness witness has TWO
|
|
2526
|
+
// distinct causes the generic "not threaded" marker conflates:
|
|
2527
|
+
// (a) awareness genuinely NOT RUN / NOT APPLICABLE — the orchestrator recorded a
|
|
2528
|
+
// skip/exemption decision (e.g. library priors exist, or the run shape makes
|
|
2529
|
+
// the 5-trace mini-sample inapplicable). This is correct behaviour, not a bug.
|
|
2530
|
+
// (b) awareness RAN but its witness was never threaded into runMeta — a real
|
|
2531
|
+
// threading regression that the loud F5 marker rightly surfaces.
|
|
2532
|
+
// We distinguish them via runMeta.decisions: an awareness skip/not-applicable
|
|
2533
|
+
// decision is the (a) signal. INTERNAL audience only (the whole tab is stripped for
|
|
2534
|
+
// clients — we never force-render anything here for client reports).
|
|
2535
|
+
const awarenessSkip = findAwarenessSkipDecision(input.runMeta);
|
|
2536
|
+
let step15Html: string;
|
|
2537
|
+
if (awarenessThreaded) {
|
|
2538
|
+
// Case: awareness ran AND was threaded → render the real Step 1.5 content.
|
|
2539
|
+
step15Html = renderAwarenessStep(input.runMeta);
|
|
2540
|
+
} else if (awarenessSkip && isInternalAudience) {
|
|
2541
|
+
// Case (a): awareness genuinely not run / not applicable — HONEST placeholder
|
|
2542
|
+
// that surfaces the recorded reason, not a misleading "not threaded" marker.
|
|
2543
|
+
step15Html = renderAwarenessNotRun(awarenessSkip);
|
|
2544
|
+
} else {
|
|
2545
|
+
// Case (b): awareness absent with no skip decision → the loud F5 not-threaded
|
|
2546
|
+
// marker (INTERNAL only; "" for client — the tab is stripped anyway).
|
|
2547
|
+
step15Html = loudOrEmpty(
|
|
2548
|
+
renderAwarenessStep(input.runMeta),
|
|
2549
|
+
"awareness sample + blind-spots",
|
|
2550
|
+
awarenessThreaded
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// R2.4: three methodology widgets — tier pie, selection-rule cards, mermaid trace.
|
|
2555
|
+
const tierPieHtml = loudOrEmpty(
|
|
2556
|
+
renderTierPie(input.runMeta?.tierBreakdown),
|
|
2557
|
+
"tier-coverage pie (tierBreakdown)",
|
|
2558
|
+
input.runMeta?.tierBreakdown !== undefined
|
|
2559
|
+
);
|
|
2560
|
+
const selectionCardsHtml = renderSelectionRuleCards(input.runMeta?.selectionRules);
|
|
2561
|
+
const selectionTraceHtml = loudOrEmpty(
|
|
2562
|
+
renderSignalSelectionTrace(input.runMeta?.signalSelectionTrace),
|
|
2563
|
+
"signal-selection trace (signalSelectionTrace)",
|
|
2564
|
+
input.runMeta?.signalSelectionTrace !== undefined
|
|
2565
|
+
);
|
|
2566
|
+
|
|
2567
|
+
// R2.1 (+D1): cap-exceeded banner + deep-read gate banner (rendered FIRST).
|
|
2568
|
+
const capBannerHtml = input.runMeta?.capBanner
|
|
2569
|
+
? `<div class="crit"><strong>${escapeHtml(input.runMeta.capBanner)}</strong></div>`
|
|
2570
|
+
: "";
|
|
2571
|
+
const gate = input.runMeta?.deepReadGate;
|
|
2572
|
+
const gateBannerHtml = gate
|
|
2573
|
+
? gate.verdict === "refuse"
|
|
2574
|
+
? `<div class="crit"><strong>${escapeHtml(gate.reason)}</strong></div>`
|
|
2575
|
+
: gate.verdict === "proceed-with-priors"
|
|
2576
|
+
? `<div class="warn">${escapeHtml(gate.reason)}</div>`
|
|
2577
|
+
: ""
|
|
2578
|
+
: "";
|
|
2579
|
+
|
|
2580
|
+
return `<section class="panel active" id="t0">
|
|
2581
|
+
<div class="internal-banner">⚙ INTERNAL-ONLY — hidden in public/client release. Traces the topology of skill operations + sub-agent dispatch for this run.</div>
|
|
2582
|
+
<h2>⚙ Methodology — operation topology</h2>
|
|
2583
|
+
<p class="sub">How this diagnosis was produced: which orchestrator steps, scripts, and sub-agents ran, pulled from the run trajectory.</p>
|
|
2584
|
+
${capBannerHtml}
|
|
2585
|
+
${gateBannerHtml}
|
|
2586
|
+
${step0Html}
|
|
2587
|
+
${sequenceHtml}
|
|
2588
|
+
${decisionLogHtml}
|
|
2589
|
+
${censusHtml}
|
|
2590
|
+
${step15Html}
|
|
2591
|
+
${selectionCardsHtml}
|
|
2592
|
+
${tierPieHtml}
|
|
2593
|
+
${selectionTraceHtml}
|
|
2594
|
+
</section>`;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/**
|
|
2598
|
+
* Wave-6 R2.6/D2: Render Methodology Step 0 — the VERBATIM operator invocation
|
|
2599
|
+
* brief that initiated the run, plus the parsed shape (agent / timeWindow / focus /
|
|
2600
|
+
* residual) when present. The verbatim is shown EXACTLY as the operator typed it
|
|
2601
|
+
* (D2 — never reworded/dropped). Returns "" when no invocation was recorded.
|
|
2602
|
+
*/
|
|
2603
|
+
export function renderOperatorInvocationStep(runMeta?: RunMeta): string {
|
|
2604
|
+
const verbatim = runMeta?.operatorInvocation;
|
|
2605
|
+
const parsed = runMeta?.parsedInvocation;
|
|
2606
|
+
if (!verbatim && !parsed) return "";
|
|
2607
|
+
|
|
2608
|
+
const verbatimHtml = verbatim
|
|
2609
|
+
? `<p class="sub">Operator invocation (verbatim):</p>
|
|
2610
|
+
<pre>${escapeHtml(verbatim)}</pre>`
|
|
2611
|
+
: "";
|
|
2612
|
+
|
|
2613
|
+
const parsedHtml = parsed
|
|
2614
|
+
? `<table>
|
|
2615
|
+
<thead><tr><th>Parsed field</th><th>Value</th></tr></thead>
|
|
2616
|
+
<tbody>
|
|
2617
|
+
<tr><td>agent</td><td>${parsed.agent ? `<code>${escapeHtml(parsed.agent)}</code>` : "<em>—</em>"}</td></tr>
|
|
2618
|
+
<tr><td>timeWindow</td><td>${parsed.timeWindow ? `<code>${escapeHtml(parsed.timeWindow)}</code>` : "<em>—</em>"}</td></tr>
|
|
2619
|
+
<tr><td>focus</td><td>${parsed.focus ? `<code>${escapeHtml(parsed.focus)}</code>` : "<em>neutral survey</em>"}</td></tr>
|
|
2620
|
+
<tr><td>residual</td><td>${parsed.residual ? escapeHtml(parsed.residual) : "<em>—</em>"}</td></tr>
|
|
2621
|
+
</tbody>
|
|
2622
|
+
</table>`
|
|
2623
|
+
: "";
|
|
2624
|
+
|
|
2625
|
+
return `<h3>Step 0 — Operator invocation</h3>
|
|
2626
|
+
${verbatimHtml}
|
|
2627
|
+
${parsedHtml}`;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// ── Wave-6 R2.4 — Methodology widgets (SVG pie · selection cards · mermaid trace) ──
|
|
2631
|
+
|
|
2632
|
+
/** Deterministic polar→cartesian for SVG arc math. cx/cy/r in px, angle in deg. */
|
|
2633
|
+
function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number): { x: number; y: number } {
|
|
2634
|
+
const a = ((angleDeg - 90) * Math.PI) / 180;
|
|
2635
|
+
return { x: round3(cx + r * Math.cos(a)), y: round3(cy + r * Math.sin(a)) };
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
function round3(n: number): number {
|
|
2639
|
+
return Math.round(n * 1000) / 1000;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
/** Default tier colours (cycled deterministically when a tier omits `color`). */
|
|
2643
|
+
const TIER_PIE_COLORS = ["#45b8cc", "#7E47D7", "#e8a64d", "#43c39a", "#b794f4", "#8a8698"];
|
|
2644
|
+
|
|
2645
|
+
/**
|
|
2646
|
+
* Wave-6 R2.4: SVG arc-path tier-coverage pie. Reads runMeta.tierBreakdown.
|
|
2647
|
+
* Handles the 0-finding case (renders an empty ring + "no findings" note) and the
|
|
2648
|
+
* single-tier case (full circle). Deterministic: arc math is pure, colours cycle
|
|
2649
|
+
* by index. Returns "" when tierBreakdown is absent.
|
|
2650
|
+
*/
|
|
2651
|
+
export function renderTierPie(tierBreakdown?: RunMeta["tierBreakdown"]): string {
|
|
2652
|
+
if (!tierBreakdown) return "";
|
|
2653
|
+
|
|
2654
|
+
const total = tierBreakdown.reduce((sum, t) => sum + Math.max(0, t.count), 0);
|
|
2655
|
+
const cx = 80;
|
|
2656
|
+
const cy = 80;
|
|
2657
|
+
const r = 70;
|
|
2658
|
+
|
|
2659
|
+
// 0-finding case: render an empty ring with a note (handle 0-tiers, plan §4 R2.4).
|
|
2660
|
+
if (total === 0) {
|
|
2661
|
+
return `<h3>Tier coverage</h3>
|
|
2662
|
+
<svg width="160" height="160" viewBox="0 0 160 160" role="img" aria-label="Tier coverage — no findings">
|
|
2663
|
+
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="var(--border,#444)" stroke-width="2"/>
|
|
2664
|
+
</svg>
|
|
2665
|
+
<p class="sub">No findings across any tier — empty coverage ring.</p>`;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// Build pie slices. A single tier with all the findings → full circle.
|
|
2669
|
+
const slices: string[] = [];
|
|
2670
|
+
const legend: string[] = [];
|
|
2671
|
+
let cursor = 0;
|
|
2672
|
+
tierBreakdown.forEach((t, i) => {
|
|
2673
|
+
const count = Math.max(0, t.count);
|
|
2674
|
+
const color = t.color ?? TIER_PIE_COLORS[i % TIER_PIE_COLORS.length];
|
|
2675
|
+
const frac = count / total;
|
|
2676
|
+
legend.push(
|
|
2677
|
+
`<span class="pie-leg"><span class="pie-sw" style="background:${escapeHtml(color)}"></span>${escapeHtml(t.tier)} (${count})</span>`
|
|
2678
|
+
);
|
|
2679
|
+
if (count === 0) return; // 0-finding tier contributes legend only, no slice.
|
|
2680
|
+
|
|
2681
|
+
const startAngle = cursor * 360;
|
|
2682
|
+
const endAngle = (cursor + frac) * 360;
|
|
2683
|
+
cursor += frac;
|
|
2684
|
+
|
|
2685
|
+
if (frac >= 1) {
|
|
2686
|
+
// Full circle — a path arc cannot draw 360°, use a circle element.
|
|
2687
|
+
slices.push(`<circle cx="${cx}" cy="${cy}" r="${r}" fill="${escapeHtml(color)}"/>`);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
const start = polarToCartesian(cx, cy, r, endAngle);
|
|
2691
|
+
const end = polarToCartesian(cx, cy, r, startAngle);
|
|
2692
|
+
const largeArc = endAngle - startAngle <= 180 ? 0 : 1;
|
|
2693
|
+
slices.push(
|
|
2694
|
+
`<path d="M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 0 ${end.x} ${end.y} Z" fill="${escapeHtml(color)}"/>`
|
|
2695
|
+
);
|
|
2696
|
+
});
|
|
2697
|
+
|
|
2698
|
+
return `<h3>Tier coverage</h3>
|
|
2699
|
+
<svg width="160" height="160" viewBox="0 0 160 160" role="img" aria-label="Tier coverage pie">
|
|
2700
|
+
${slices.join("\n ")}
|
|
2701
|
+
</svg>
|
|
2702
|
+
<div class="pie-legend">${legend.join(" ")}</div>`;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/**
|
|
2706
|
+
* W11-09 (Wave-6 R2.4 + PR-049): per-signal selection-rule cards.
|
|
2707
|
+
* Each card shows the signal's score + verdict. Signals discovered by the R2.2
|
|
2708
|
+
* awareness layer get a "discovered" badge.
|
|
2709
|
+
*
|
|
2710
|
+
* PR-049 5-step detect+select process (rendered as a sub-header when no cards present):
|
|
2711
|
+
* 1. Enumerate all signals from Tier-0 static scan (PR-001).
|
|
2712
|
+
* 2. Failure-validity gate — drop benign observability artifacts.
|
|
2713
|
+
* 3. Impact×prevalence scoring (impact = $/latency/correctness harm × trace rate).
|
|
2714
|
+
* 4. Deep-read corroboration — findings[0].failureOrigin.what confirms or overrides.
|
|
2715
|
+
* 5. Emit ONE RunMeta.primarySignal driving census·heatmap·funnel.
|
|
2716
|
+
*
|
|
2717
|
+
* Reads runMeta.selectionRules. Returns the 5-step methodology note when absent/empty.
|
|
2718
|
+
*/
|
|
2719
|
+
/**
|
|
2720
|
+
* W17-E (R1 evidence-floor visibility): derive an explicit floor-outcome badge from a
|
|
2721
|
+
* selection-rule verdict string. The enricher (buildSelectionMeta) encodes the floor
|
|
2722
|
+
* outcome in the verdict prose; this lifts it into a visible badge so a reader can see
|
|
2723
|
+
* — without parsing prose — which discovered signals were CORROBORATED (admitted to
|
|
2724
|
+
* PRIMARY by the evidence floor) vs CAPPED at SECONDARY for lack of mechanical evidence.
|
|
2725
|
+
*
|
|
2726
|
+
* - "★ PRIMARY (… mechanically corroborated)" → corroborated-PRIMARY badge
|
|
2727
|
+
* - "secondary (discovered, corroborated)" → corroborated badge
|
|
2728
|
+
* - "… unconfirmed (no mechanical evidence …)" → capped-at-SECONDARY badge
|
|
2729
|
+
* - anything else (cheap-signal cards / ruled-out) → no floor badge ("")
|
|
2730
|
+
*
|
|
2731
|
+
* Deterministic, case-insensitive substring classification. Pure string.
|
|
2732
|
+
*/
|
|
2733
|
+
export function floorVerdictBadge(verdict: string): string {
|
|
2734
|
+
const v = verdict.toLowerCase();
|
|
2735
|
+
// Unevidenced discovered signal: the floor capped it at SECONDARY. Most specific —
|
|
2736
|
+
// check first so a "corroborated" substring elsewhere can't mask it.
|
|
2737
|
+
if (v.includes("unconfirmed") || v.includes("no mechanical evidence")) {
|
|
2738
|
+
return `<span class="badge b-med floor-capped">⚠ capped at SECONDARY — unevidenced (R1 floor)</span>`;
|
|
2739
|
+
}
|
|
2740
|
+
// Discovered signal that passed the floor (corroborated).
|
|
2741
|
+
if (v.includes("corroborated")) {
|
|
2742
|
+
return `<span class="badge b-crit floor-corroborated">✓ evidence-floor: corroborated</span>`;
|
|
2743
|
+
}
|
|
2744
|
+
return "";
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
export function renderSelectionRuleCards(rules?: RunMeta["selectionRules"]): string {
|
|
2748
|
+
const methodologyNote = `<div style="font-size:12px;color:var(--muted);background:var(--bg2);border:1px solid var(--border);border-radius:4px;padding:8px 12px;margin:4px 0;">
|
|
2749
|
+
<strong>PR-049 signal detect+select process (5 steps):</strong><br>
|
|
2750
|
+
1. Enumerate all Tier-0 signals (PR-001 static scan).<br>
|
|
2751
|
+
2. Failure-validity gate — admit only mechanical signals that map to a user-visible failure WHAT; rule out benign observability artifacts (e.g. missing-metadata).<br>
|
|
2752
|
+
3. Impact×prevalence scoring: impact = $/latency/correctness harm × trace prevalence rate.<br>
|
|
2753
|
+
4. Deep-read corroboration — LLM findings[0].failureOrigin.what confirms or overrides rank.<br>
|
|
2754
|
+
5. Emit ONE <code>runMeta.primarySignal</code> driving census badge · heatmap · funnel.
|
|
2755
|
+
</div>`;
|
|
2756
|
+
|
|
2757
|
+
if (!rules || rules.length === 0) return methodologyNote;
|
|
2758
|
+
const cards = rules
|
|
2759
|
+
.map((rule) => {
|
|
2760
|
+
const badge = rule.discoveredByAwareness
|
|
2761
|
+
? `<span class="badge b-info">★ discovered (awareness)</span>`
|
|
2762
|
+
: "";
|
|
2763
|
+
// W17-E (R1 floor visibility): make the evidence-floor outcome an explicit badge,
|
|
2764
|
+
// not text buried in the verdict prose. Derived deterministically from the verdict
|
|
2765
|
+
// the enricher emitted (build-render-input.ts buildSelectionMeta): a corroborated
|
|
2766
|
+
// discovered winner reads "★ PRIMARY (… corroborated)"; an unevidenced discovered
|
|
2767
|
+
// signal reads "… unconfirmed (no mechanical evidence …)" and is capped-at-SECONDARY.
|
|
2768
|
+
const floorBadge = floorVerdictBadge(rule.verdict);
|
|
2769
|
+
// D3: dataset-candidate detail rows — each is rendered ONLY when the enricher
|
|
2770
|
+
// derived it from the linked finding (omitted gracefully otherwise — never faked).
|
|
2771
|
+
const detail = (label: string, value: string | undefined, cls = ""): string =>
|
|
2772
|
+
value
|
|
2773
|
+
? `<div class="sel-detail${cls ? " " + cls : ""}"><span class="sel-k">${escapeHtml(label)}</span>${escapeHtml(value)}</div>`
|
|
2774
|
+
: "";
|
|
2775
|
+
const findingLink = rule.linkedFindingId
|
|
2776
|
+
? `<div class="sel-detail"><span class="sel-k">finding</span><code>${escapeHtml(rule.linkedFindingId)}</code></div>`
|
|
2777
|
+
: "";
|
|
2778
|
+
const candidateDetails = [
|
|
2779
|
+
detail("scenario", rule.scenario),
|
|
2780
|
+
detail("use-case / edge-case", rule.useCase),
|
|
2781
|
+
detail("why it failed", rule.whyFailed),
|
|
2782
|
+
detail("why high-value candidate", rule.whyHighValue, "sel-value"),
|
|
2783
|
+
detail("would prevent regression", rule.prevents, "sel-prevents"),
|
|
2784
|
+
findingLink,
|
|
2785
|
+
].join("");
|
|
2786
|
+
return `<div class="sel-card">
|
|
2787
|
+
<div class="sel-card-head"><strong>${escapeHtml(rule.signal)}</strong> ${badge}${floorBadge}</div>
|
|
2788
|
+
<div class="sel-card-body">
|
|
2789
|
+
<span class="sel-k">score</span> <code>${escapeHtml(rule.score)}</code>
|
|
2790
|
+
<span class="sel-k">verdict</span> ${escapeHtml(rule.verdict)}
|
|
2791
|
+
</div>
|
|
2792
|
+
${candidateDetails}
|
|
2793
|
+
</div>`;
|
|
2794
|
+
})
|
|
2795
|
+
.join("\n ");
|
|
2796
|
+
return `<h3>Selection rules — per-signal score + verdict (PR-049)</h3>
|
|
2797
|
+
${methodologyNote}
|
|
2798
|
+
<div class="sel-cards">
|
|
2799
|
+
${cards}
|
|
2800
|
+
</div>`;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
/**
|
|
2804
|
+
* W17-E (R7 — residual surfacing): render the "suspected primary — unconfirmed"
|
|
2805
|
+
* note from runMeta.suspectedPrimaryUnconfirmed.
|
|
2806
|
+
*
|
|
2807
|
+
* WHY: when the top DISCOVERED signal is capped at SECONDARY for lack of mechanical
|
|
2808
|
+
* evidence (R1 evidence floor) and primary falls back to a CHEAP signal, the report
|
|
2809
|
+
* must NOT silently present the cheap fallback as the confident primary. This note
|
|
2810
|
+
* makes the residual visible: it names the suspected signal, shows its HONEST sampled
|
|
2811
|
+
* prevalence ("seen in k/n sampled" — never a fabricated corpus rate), and states WHY
|
|
2812
|
+
* it could not be confirmed (the floor reason). Rendered as a `.warn` callout so it
|
|
2813
|
+
* reads as a caveat, not a finding.
|
|
2814
|
+
*
|
|
2815
|
+
* Returns "" when the field is absent — i.e. the discovered signal PASSED the floor
|
|
2816
|
+
* (nothing unconfirmed) or none was discovered. Deterministic; pure string.
|
|
2817
|
+
*/
|
|
2818
|
+
export function renderSuspectedPrimaryNote(runMeta?: RunMeta): string {
|
|
2819
|
+
const sp = runMeta?.suspectedPrimaryUnconfirmed;
|
|
2820
|
+
if (!sp) return "";
|
|
2821
|
+
return `<div class="warn suspected-primary-unconfirmed" data-suspected="${escapeHtml(sp.signal)}">
|
|
2822
|
+
<strong>⚠ Suspected primary — unconfirmed:</strong>
|
|
2823
|
+
<code>${escapeHtml(sp.signal)}</code> was the highest-impact <em>discovered</em> signal
|
|
2824
|
+
(seen in ${sp.seenCount}/${sp.sampledCount} sampled) but is <strong>capped at SECONDARY</strong>
|
|
2825
|
+
— ${escapeHtml(sp.reason)}. The primary shown below is a cheaper fallback signal, NOT a
|
|
2826
|
+
confident root cause. Treat the suspected signal as a lead requiring mechanical corroboration.
|
|
2827
|
+
</div>`;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/**
|
|
2831
|
+
* Wave-6 R2.4: signal-selection trace — a mermaid decision tree reading
|
|
2832
|
+
* runMeta.signalSelectionTrace. Renders with PARTIAL data (tolerates a short or
|
|
2833
|
+
* empty trace by falling back to a stub diagram). Returns "" when absent.
|
|
2834
|
+
*/
|
|
2835
|
+
export function renderSignalSelectionTrace(trace?: string): string {
|
|
2836
|
+
if (trace === undefined) return "";
|
|
2837
|
+
const body = trace.trim().length > 0
|
|
2838
|
+
? trace
|
|
2839
|
+
: "graph TD\n A[insufficient trace data] --> B[partial render]";
|
|
2840
|
+
return `<h3>Signal-selection trace</h3>
|
|
2841
|
+
<div class="mermaid">
|
|
2842
|
+
${body}
|
|
2843
|
+
</div>`;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
/**
|
|
2847
|
+
* REQ-050: Detect an awareness "not run / not applicable" decision in the run's
|
|
2848
|
+
* decision log. The orchestrator records a skip/exemption decision (e.g. library
|
|
2849
|
+
* priors exist, or the run shape makes the 5-trace mini-sample inapplicable) in
|
|
2850
|
+
* runMeta.decisions. Its presence is the signal that an ABSENT awareness witness is
|
|
2851
|
+
* deliberate (case a) rather than a threading regression (case b).
|
|
2852
|
+
*
|
|
2853
|
+
* Match heuristic: a decision whose step marks an exemption/skip (or whose choice
|
|
2854
|
+
* text names a skip) AND whose text mentions the awareness layer. Returns the
|
|
2855
|
+
* matched decision (for its rationale) or undefined.
|
|
2856
|
+
*/
|
|
2857
|
+
export function findAwarenessSkipDecision(
|
|
2858
|
+
runMeta?: RunMeta
|
|
2859
|
+
): { step: string; choice: string; rationale: string } | undefined {
|
|
2860
|
+
const decisions = runMeta?.decisions;
|
|
2861
|
+
if (!decisions || decisions.length === 0) return undefined;
|
|
2862
|
+
return decisions.find((d) => {
|
|
2863
|
+
const haystack = `${d.step} ${d.choice} ${d.rationale}`.toLowerCase();
|
|
2864
|
+
const mentionsAwareness = haystack.includes("awareness");
|
|
2865
|
+
const isSkip =
|
|
2866
|
+
haystack.includes("skip") ||
|
|
2867
|
+
haystack.includes("exempt") ||
|
|
2868
|
+
haystack.includes("not applicable") ||
|
|
2869
|
+
haystack.includes("not run") ||
|
|
2870
|
+
haystack.includes("n/a");
|
|
2871
|
+
return mentionsAwareness && isSkip;
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
/**
|
|
2876
|
+
* REQ-050: Render the HONEST "awareness not run / not applicable" empty-state for the
|
|
2877
|
+
* Methodology Step 1.5 slot. Surfaces the recorded rationale so the reader can tell
|
|
2878
|
+
* this from the (b) "ran-but-not-threaded" regression marker. INTERNAL-audience only
|
|
2879
|
+
* (the caller guards on audience; the whole Methodology tab is stripped for clients).
|
|
2880
|
+
*/
|
|
2881
|
+
export function renderAwarenessNotRun(decision: {
|
|
2882
|
+
step: string;
|
|
2883
|
+
choice: string;
|
|
2884
|
+
rationale: string;
|
|
2885
|
+
}): string {
|
|
2886
|
+
return `<h3>Step 1.5 — Awareness layer (measurement-gap check)</h3>
|
|
2887
|
+
<div class="warn awareness-not-run" data-awareness="not-run">⊘ Awareness layer not run / not applicable for this run — this is a recorded decision, not a missing widget. Reason: ${escapeHtml(decision.rationale || decision.choice)}.</div>`;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
/**
|
|
2891
|
+
* Wave-6 R2.2: Render Methodology Step 1.5 — the awareness-layer mini-sample +
|
|
2892
|
+
* blind-spots table. When the awareness layer SKIPPED (library priors exist), a
|
|
2893
|
+
* placeholder is rendered instead. Returns "" when no awareness data is present
|
|
2894
|
+
* at all (older runs / backward-compat).
|
|
2895
|
+
*/
|
|
2896
|
+
export function renderAwarenessStep(runMeta?: RunMeta): string {
|
|
2897
|
+
const sample = runMeta?.awarenessSample;
|
|
2898
|
+
const blindSpots = runMeta?.blindSpots;
|
|
2899
|
+
|
|
2900
|
+
// No awareness data at all → omit the step (backward-compat).
|
|
2901
|
+
if (!sample && (!blindSpots || blindSpots.length === 0)) return "";
|
|
2902
|
+
|
|
2903
|
+
// SKIP placeholder: a blindSpots table with the "(all)" skipped marker and no sample.
|
|
2904
|
+
if (!sample && blindSpots && blindSpots.length > 0 && blindSpots.every((b) => b.checkedBy === "—")) {
|
|
2905
|
+
return `<h3>Step 1.5 — Awareness layer (measurement-gap check)</h3>
|
|
2906
|
+
<p class="sub">Library priors exist; awareness layer skipped. The discovered-signal priors from prior runs are reused instead of a fresh 5-trace mini-sample. (R2.2)</p>`;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// SD-7: guard the .map at the call site. The documented shape is
|
|
2910
|
+
// `{ traces: string[]; findings; firedAt }`, but a malformed runtime witness
|
|
2911
|
+
// (e.g. traces threaded as undefined/null by a producer regression) would crash
|
|
2912
|
+
// the whole render with `sample.traces.map is not a function`. Render a SKIP
|
|
2913
|
+
// marker for the trace list instead of throwing — the rest of Step 1.5 (firedAt,
|
|
2914
|
+
// blind-spots) still renders.
|
|
2915
|
+
const sampleHtml = sample
|
|
2916
|
+
? `<p class="sub">A 5-trace LLM mini-sample fired BEFORE primary-signal selection to surface signals Tier-0 cannot MEASURE (the measurement-layer fix). Traces: ${
|
|
2917
|
+
Array.isArray(sample.traces)
|
|
2918
|
+
? sample.traces.map((t) => `<code>${escapeHtml(t)}</code>`).join(", ")
|
|
2919
|
+
: `<em>— trace list unavailable (malformed awareness witness)</em>`
|
|
2920
|
+
}. Fired at ${escapeHtml(sample.firedAt)}.</p>`
|
|
2921
|
+
: "";
|
|
2922
|
+
|
|
2923
|
+
const blindSpotsTable = blindSpots && blindSpots.length > 0
|
|
2924
|
+
? `<table>
|
|
2925
|
+
<thead><tr><th>Signal</th><th>Measurable?</th><th>Checked by</th><th>Result</th></tr></thead>
|
|
2926
|
+
<tbody>
|
|
2927
|
+
${blindSpots
|
|
2928
|
+
.map(
|
|
2929
|
+
(b) =>
|
|
2930
|
+
`<tr><td>${escapeHtml(b.signal)}</td><td>${escapeHtml(b.measurable)}</td><td>${escapeHtml(b.checkedBy)}</td><td>${escapeHtml(b.result)}</td></tr>`
|
|
2931
|
+
)
|
|
2932
|
+
.join("\n ")}
|
|
2933
|
+
</tbody>
|
|
2934
|
+
</table>`
|
|
2935
|
+
: "";
|
|
2936
|
+
|
|
2937
|
+
return `<h3>Step 1.5 — Awareness layer (measurement-gap check)</h3>
|
|
2938
|
+
${sampleHtml}
|
|
2939
|
+
${blindSpotsTable}`;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// ── renderTrajectoryTab (legacy I-041 — retained, no longer in default nav) ──
|
|
2943
|
+
|
|
2944
|
+
/**
|
|
2945
|
+
* I-041 (retained): Session-trajectory tab. The gold-standard nav does not
|
|
2946
|
+
* surface a separate Trajectory tab (its sequence lives in Methodology), but the
|
|
2947
|
+
* function is retained for callers/tests that render it directly.
|
|
2948
|
+
*/
|
|
2949
|
+
export function renderTrajectoryTab(input: RenderInput): string {
|
|
2950
|
+
const traj = input.sessionTrajectory;
|
|
2951
|
+
const dagHtml = traj?.mermaidDag
|
|
2952
|
+
? `<h3>Session Flow (DAG)</h3>
|
|
2953
|
+
<div class="mermaid">${escapeHtml(traj.mermaidDag)}</div>`
|
|
2954
|
+
: `<h3>Session Flow (DAG)</h3><p class="sub">No trajectory DAG available.</p>`;
|
|
2955
|
+
const timelineHtml =
|
|
2956
|
+
traj?.steps && traj.steps.length > 0
|
|
2957
|
+
? `<h3 style="margin-top:18px;">Timeline</h3>
|
|
2958
|
+
<div style="font-size:12px;">
|
|
2959
|
+
${traj.steps
|
|
2960
|
+
.map(
|
|
2961
|
+
(s) =>
|
|
2962
|
+
`<div style="display:flex;gap:10px;align-items:flex-start;padding:6px 0;border-bottom:1px solid var(--border);">
|
|
2963
|
+
<span style="min-width:28px;font-family:var(--fm);color:var(--dim);font-size:11px;">${s.step}</span>
|
|
2964
|
+
<span style="min-width:80px;color:var(--p);font-family:var(--fm);font-size:11px;font-weight:600;">${escapeHtml(s.type)}</span>
|
|
2965
|
+
<span style="flex:1;color:var(--muted);">${escapeHtml(s.content.slice(0, 140))}${s.content.length > 140 ? "…" : ""}</span>
|
|
2966
|
+
${s.timestamp ? `<span style="color:var(--dim);font-size:11px;font-family:var(--fm);white-space:nowrap;">${escapeHtml(s.timestamp)}</span>` : ""}
|
|
2967
|
+
</div>`
|
|
2968
|
+
)
|
|
2969
|
+
.join("\n")}
|
|
2970
|
+
</div>`
|
|
2971
|
+
: `<p class="sub" style="margin-top:12px;">No step-by-step timeline available.</p>`;
|
|
2972
|
+
return `
|
|
2973
|
+
<section class="panel" id="ttraj">
|
|
2974
|
+
<h2>Session Trajectory <span class="badge b-info">INTERNAL</span></h2>
|
|
2975
|
+
${dagHtml}
|
|
2976
|
+
${timelineHtml}
|
|
2977
|
+
</section>`;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// ── renderDiscoveredChecks ────────────────────────────────────────────────────
|
|
2981
|
+
|
|
2982
|
+
function renderDiscoveredChecks(checks: DiscoveredCheck[]): string {
|
|
2983
|
+
return `
|
|
2984
|
+
<div class="discovered-checks">
|
|
2985
|
+
${checks
|
|
2986
|
+
.map(
|
|
2987
|
+
(c) => `
|
|
2988
|
+
<div class="check" id="${escapeHtml(c.checkId)}">
|
|
2989
|
+
<strong>${escapeHtml(c.name)}</strong>
|
|
2990
|
+
<p>${escapeHtml(c.description)}</p>
|
|
2991
|
+
<small>Affects: ${c.affectedTraceIds.map((id) => escapeHtml(id)).join(", ")}</small>
|
|
2992
|
+
</div>`
|
|
2993
|
+
)
|
|
2994
|
+
.join("\n")}
|
|
2995
|
+
</div>`;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// ── Wave-4: Contract-aware rendering (opt-in structured-report mode) ──────────
|
|
2999
|
+
|
|
3000
|
+
/**
|
|
3001
|
+
* Detects whether a target declares a self-diagnosis contract.
|
|
3002
|
+
* Looks for <target-root>/self-diagnosis-contract.yaml.
|
|
3003
|
+
* Returns parsed + validated contract OR null if absent.
|
|
3004
|
+
*
|
|
3005
|
+
* Capability: opt-in structured-report mode. Targets that don't declare
|
|
3006
|
+
* a contract get open-ended reports unchanged (regression invariant).
|
|
3007
|
+
*/
|
|
3008
|
+
export async function loadTargetContract(
|
|
3009
|
+
targetRoot: string
|
|
3010
|
+
): Promise<SelfDiagnosisContract | null> {
|
|
3011
|
+
const contractPath = join(targetRoot, "self-diagnosis-contract.yaml");
|
|
3012
|
+
if (!existsSync(contractPath)) return null;
|
|
3013
|
+
const raw = readFileSync(contractPath, "utf8");
|
|
3014
|
+
const parsed: unknown = parseYaml(raw);
|
|
3015
|
+
if (!Value.Check(SelfDiagnosisContractSchema, parsed)) {
|
|
3016
|
+
const errors = [...Value.Errors(SelfDiagnosisContractSchema, parsed)];
|
|
3017
|
+
const detail = errors.slice(0, 3).map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
3018
|
+
throw new Error(`Invalid self-diagnosis-contract.yaml at ${contractPath}: ${detail}`);
|
|
3019
|
+
}
|
|
3020
|
+
return parsed as SelfDiagnosisContract;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
/**
|
|
3024
|
+
* Open-ended report path (existing behaviour — unchanged).
|
|
3025
|
+
* Targets without a self-diagnosis-contract.yaml always use this path.
|
|
3026
|
+
* Regression invariant: output must be identical to renderReport for same inputs.
|
|
3027
|
+
*/
|
|
3028
|
+
export function renderOpenEndedReport(template: string, input: RenderInput): string {
|
|
3029
|
+
return renderReport(template, input);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// ── Structured render input ──────────────────────────────────────────────────
|
|
3033
|
+
|
|
3034
|
+
export interface StructuredRenderInput extends RenderInput {
|
|
3035
|
+
/** Parsed contract from the target's self-diagnosis-contract.yaml */
|
|
3036
|
+
contract: SelfDiagnosisContract;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
/**
|
|
3040
|
+
* Structured report path (Wave-4 new).
|
|
3041
|
+
* When a target declares a self-diagnosis-contract.yaml, diagnostics emits a
|
|
3042
|
+
* 10-category structured report against the declared success criteria.
|
|
3043
|
+
* Findings with `criterion` populated are matched to their category section.
|
|
3044
|
+
* Criteria with no matching finding default to "pending".
|
|
3045
|
+
*/
|
|
3046
|
+
export function renderStructuredReport(input: StructuredRenderInput): string {
|
|
3047
|
+
const { contract, findings } = input;
|
|
3048
|
+
|
|
3049
|
+
// Build a lookup: criterion id → finding (for criterion-aware findings only)
|
|
3050
|
+
const criterionFindingMap = new Map<string, Finding>();
|
|
3051
|
+
for (const f of findings) {
|
|
3052
|
+
if (f.criterion) {
|
|
3053
|
+
criterionFindingMap.set(f.criterion.id, f);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// Status pill rendering
|
|
3058
|
+
const statusPill = (status: string): string => {
|
|
3059
|
+
const colorMap: Record<string, string> = {
|
|
3060
|
+
pass: "var(--g)",
|
|
3061
|
+
fail: "var(--r)",
|
|
3062
|
+
"not-applicable": "var(--dim)",
|
|
3063
|
+
pending: "var(--y)",
|
|
3064
|
+
};
|
|
3065
|
+
const color = colorMap[status] ?? "var(--muted)";
|
|
3066
|
+
return `<span style="font-family:var(--fm);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;padding:2px 8px;border-radius:4px;background:${color};color:var(--bg);">${escapeHtml(status)}</span>`;
|
|
3067
|
+
};
|
|
3068
|
+
|
|
3069
|
+
// Build 10-category sections
|
|
3070
|
+
const categorySections = contract.success_criteria
|
|
3071
|
+
.map((entry) => {
|
|
3072
|
+
const criteriaRows = entry.criteria
|
|
3073
|
+
.map((criterion) => {
|
|
3074
|
+
const matchedFinding = criterionFindingMap.get(criterion.id);
|
|
3075
|
+
const status = matchedFinding?.criterion?.status ?? "pending";
|
|
3076
|
+
const evidenceRef = matchedFinding?.criterion?.evidenceRef;
|
|
3077
|
+
const evidenceHtml = evidenceRef
|
|
3078
|
+
? `<span style="font-size:11px;color:var(--dim);font-family:var(--fm);">[${escapeHtml(evidenceRef.kind)}] ${escapeHtml(evidenceRef.value)}</span>`
|
|
3079
|
+
: "";
|
|
3080
|
+
return `<tr style="border-bottom:1px solid var(--border);">
|
|
3081
|
+
<td style="padding:8px 12px;font-family:var(--fm);font-size:11px;color:var(--muted);white-space:nowrap;">${escapeHtml(criterion.id)}</td>
|
|
3082
|
+
<td style="padding:8px 12px;font-size:13px;color:var(--text);">${escapeHtml(criterion.statement)}</td>
|
|
3083
|
+
<td style="padding:8px 12px;">${statusPill(status)}</td>
|
|
3084
|
+
<td style="padding:8px 12px;">${evidenceHtml}</td>
|
|
3085
|
+
</tr>`;
|
|
3086
|
+
})
|
|
3087
|
+
.join("\n");
|
|
3088
|
+
|
|
3089
|
+
const notesHtml = entry.notes
|
|
3090
|
+
? `<div style="font-size:11px;color:var(--dim);font-family:var(--fm);padding:6px 0 10px;">${escapeHtml(entry.notes)}</div>`
|
|
3091
|
+
: "";
|
|
3092
|
+
|
|
3093
|
+
return `<section style="margin-bottom:24px;">
|
|
3094
|
+
<h3 style="font-family:var(--fm);font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:var(--p);border-bottom:1px solid var(--border);padding-bottom:6px;margin-bottom:6px;">${escapeHtml(entry.category)}</h3>
|
|
3095
|
+
${notesHtml}
|
|
3096
|
+
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
|
3097
|
+
<thead><tr style="border-bottom:2px solid var(--bstr);">
|
|
3098
|
+
<th style="padding:6px 12px;text-align:left;font-family:var(--fm);color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.06em;">ID</th>
|
|
3099
|
+
<th style="padding:6px 12px;text-align:left;font-family:var(--fm);color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.06em;">Criterion</th>
|
|
3100
|
+
<th style="padding:6px 12px;text-align:left;font-family:var(--fm);color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.06em;">Status</th>
|
|
3101
|
+
<th style="padding:6px 12px;text-align:left;font-family:var(--fm);color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.06em;">Evidence</th>
|
|
3102
|
+
</tr></thead>
|
|
3103
|
+
<tbody>${criteriaRows}</tbody>
|
|
3104
|
+
</table>
|
|
3105
|
+
</section>`;
|
|
3106
|
+
})
|
|
3107
|
+
.join("\n");
|
|
3108
|
+
|
|
3109
|
+
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
3110
|
+
const skillName = escapeHtml(contract.skill.name);
|
|
3111
|
+
const skillVersion = escapeHtml(contract.skill.version);
|
|
3112
|
+
const skillClass = escapeHtml(contract.skill.class);
|
|
3113
|
+
const categoryCount = contract.success_criteria.length;
|
|
3114
|
+
const totalCriteria = contract.success_criteria.reduce(
|
|
3115
|
+
(sum, e) => sum + e.criteria.length,
|
|
3116
|
+
0
|
|
3117
|
+
);
|
|
3118
|
+
|
|
3119
|
+
return `<!DOCTYPE html>
|
|
3120
|
+
<html lang="en" data-theme="dark">
|
|
3121
|
+
<head>
|
|
3122
|
+
<meta charset="UTF-8">
|
|
3123
|
+
<title>MUTAGENT — Structured Diagnostic Report · ${skillName}</title>
|
|
3124
|
+
<style>
|
|
3125
|
+
:root{--bg:#0a0a12;--bg2:#14141d;--surf:#1a1a25;--surf-e:#22222f;--text:#eef1f6;--muted:#a6a2b4;--dim:#6a6678;--border:rgba(255,255,255,0.09);--bstr:rgba(255,255,255,0.16);--p:#b794f4;--p-strong:#7E47D7;--c:#45b8cc;--g:#43c39a;--y:#e8a64d;--r:#e06666;--fm:'IBM Plex Mono',ui-monospace,monospace;}
|
|
3126
|
+
*,*::before,*::after{box-sizing:border-box;border-radius:0!important;} body{margin:0;font-family:'Space Grotesk',system-ui,sans-serif;color:var(--text);background:var(--bg);font-size:14px;line-height:1.55;}
|
|
3127
|
+
header{background:var(--bg2);border-bottom:1px solid var(--bstr);padding:20px 32px;}
|
|
3128
|
+
.logo{font-weight:700;font-size:20px;letter-spacing:0.18em;background:linear-gradient(135deg,var(--p) 0%,var(--c) 100%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
|
|
3129
|
+
main{padding:24px 32px;}
|
|
3130
|
+
h2{font-size:16px;color:var(--p);margin-bottom:16px;}
|
|
3131
|
+
</style>
|
|
3132
|
+
</head>
|
|
3133
|
+
<body>
|
|
3134
|
+
<header>
|
|
3135
|
+
<div class="logo">MUTAGENT</div>
|
|
3136
|
+
<div style="font-size:12px;color:var(--muted);margin-top:4px;">Structured Diagnostic Report · ${skillName} v${skillVersion} · <span style="color:var(--dim);">${skillClass}</span></div>
|
|
3137
|
+
<div style="font-size:11px;color:var(--dim);font-family:var(--fm);margin-top:4px;">session=${escapeHtml(safeSessionId(input.sessionId))} · diagnosed=${escapeHtml(input.diagnosedAt)} · generated=${escapeHtml(generatedAt)}</div>
|
|
3138
|
+
<div style="font-size:11px;color:var(--dim);font-family:var(--fm);">${categoryCount} categories · ${totalCriteria} criteria · ${findings.length} finding(s)</div>
|
|
3139
|
+
</header>
|
|
3140
|
+
<main>
|
|
3141
|
+
<h2>Success Criteria — 10-Category Structured Report</h2>
|
|
3142
|
+
${categorySections}
|
|
3143
|
+
</main>
|
|
3144
|
+
</body>
|
|
3145
|
+
</html>`;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// ── escapeHtml ───────────────────────────────────────────────────────────────
|
|
3149
|
+
|
|
3150
|
+
function escapeHtml(str: string | null | undefined): string {
|
|
3151
|
+
// W12-03: coerce defensively — optional parsed fields (e.g. parsedInvocation.residual)
|
|
3152
|
+
// reach here as undefined on neutral-survey runs; an unguarded .replace would throw
|
|
3153
|
+
// TypeError and crash the whole report (the product surface must never crash on an
|
|
3154
|
+
// absent optional field).
|
|
3155
|
+
const safe = String(str ?? "");
|
|
3156
|
+
return safe
|
|
3157
|
+
.replace(/&/g, "&")
|
|
3158
|
+
.replace(/</g, "<")
|
|
3159
|
+
.replace(/>/g, ">")
|
|
3160
|
+
.replace(/"/g, """)
|
|
3161
|
+
.replace(/'/g, "'");
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
// ── defaultTemplatePath ──────────────────────────────────────────────────────
|
|
3165
|
+
|
|
3166
|
+
/**
|
|
3167
|
+
* Resolve the default template path: assets/templates/report.html.tpl
|
|
3168
|
+
* relative to this script's own directory (report/), so it works regardless
|
|
3169
|
+
* of CWD. The canonical path is <skill-root>/assets/templates/report.html.tpl.
|
|
3170
|
+
* R-SELF-02-a / R-003-A: when --template flag is omitted, default to this file.
|
|
3171
|
+
* Falls back to default.html if report.html.tpl does not exist (pre-P4 compatibility).
|
|
3172
|
+
*/
|
|
3173
|
+
function defaultTemplatePath(): string {
|
|
3174
|
+
const scriptDir = import.meta.dirname ?? pathDirname(import.meta.url.replace("file://", ""));
|
|
3175
|
+
const tplPath = join(scriptDir, "..", "..", "assets", "templates", "report.html.tpl");
|
|
3176
|
+
if (existsSync(tplPath)) return tplPath;
|
|
3177
|
+
return join(scriptDir, "..", "..", "assets", "templates", "default.html");
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
// ── parseCliArgs ─────────────────────────────────────────────────────────────
|
|
3181
|
+
|
|
3182
|
+
/**
|
|
3183
|
+
* R-003-A: Parse flag-based CLI arguments.
|
|
3184
|
+
* Supports: --findings <path> --output <path> [--template <path>] [--audience client|internal]
|
|
3185
|
+
* Returns null if required flags are missing.
|
|
3186
|
+
*/
|
|
3187
|
+
function parseCliArgs(argv: string[]): {
|
|
3188
|
+
findingsPath: string;
|
|
3189
|
+
outputPath: string;
|
|
3190
|
+
templatePath: string | null;
|
|
3191
|
+
audience: Audience;
|
|
3192
|
+
} | null {
|
|
3193
|
+
let findingsPath: string | null = null;
|
|
3194
|
+
let outputPath: string | null = null;
|
|
3195
|
+
let templatePath: string | null = null;
|
|
3196
|
+
let audience: Audience = "client"; // W13: default to CLIENT (leak-safe for published runs); internal is opt-in (--audience internal); self-diag forces internal (PR-022)
|
|
3197
|
+
|
|
3198
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3199
|
+
if (argv[i] === "--findings" && argv[i + 1]) {
|
|
3200
|
+
findingsPath = argv[i + 1];
|
|
3201
|
+
i++;
|
|
3202
|
+
} else if (argv[i] === "--output" && argv[i + 1]) {
|
|
3203
|
+
outputPath = argv[i + 1];
|
|
3204
|
+
i++;
|
|
3205
|
+
} else if (argv[i] === "--template" && argv[i + 1]) {
|
|
3206
|
+
templatePath = argv[i + 1];
|
|
3207
|
+
i++;
|
|
3208
|
+
} else if (argv[i] === "--audience" && argv[i + 1]) {
|
|
3209
|
+
const raw = argv[i + 1];
|
|
3210
|
+
audience = raw === "client" ? "client" : "internal";
|
|
3211
|
+
i++;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
if (!findingsPath || !outputPath) return null;
|
|
3216
|
+
return { findingsPath, outputPath, templatePath, audience };
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// ── CLI entrypoint ───────────────────────────────────────────────────────────
|
|
3220
|
+
|
|
3221
|
+
if (import.meta.main) {
|
|
3222
|
+
const args = process.argv.slice(2);
|
|
3223
|
+
const parsed = parseCliArgs(args);
|
|
3224
|
+
|
|
3225
|
+
if (!parsed) {
|
|
3226
|
+
process.stderr.write(
|
|
3227
|
+
"Usage: bun scripts/report/render.ts --findings <findings.json> --output <report.html> [options]\n" +
|
|
3228
|
+
"Options:\n" +
|
|
3229
|
+
" --template <path> Template file (default: assets/templates/report.html.tpl)\n" +
|
|
3230
|
+
" --audience client|internal Audience mode (default: internal)\n" +
|
|
3231
|
+
" client: NODE-STRIP removes class=\"internal\" nodes (Methodology, Trajectory tabs)\n" +
|
|
3232
|
+
" internal: Full report with all internal tabs and banners\n"
|
|
3233
|
+
);
|
|
3234
|
+
process.exit(1);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
const { findingsPath, outputPath, templatePath: templatePathArg, audience } = parsed;
|
|
3238
|
+
const templatePath = templatePathArg ?? defaultTemplatePath();
|
|
3239
|
+
|
|
3240
|
+
try {
|
|
3241
|
+
const rawInput: RenderInput = JSON.parse(readFileSync(resolve(findingsPath), "utf8"));
|
|
3242
|
+
// FU-INT-1: CLI --audience flag overrides any audience in the JSON
|
|
3243
|
+
const input: RenderInput = { ...rawInput, audience };
|
|
3244
|
+
const template = readFileSync(resolve(templatePath), "utf8");
|
|
3245
|
+
const html = renderReport(template, input);
|
|
3246
|
+
writeFileSync(resolve(outputPath), html, "utf8");
|
|
3247
|
+
process.stdout.write(`Report written to: ${outputPath}\n`);
|
|
3248
|
+
process.exit(0);
|
|
3249
|
+
} catch (err) {
|
|
3250
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
3251
|
+
process.stderr.write(`Error rendering ${findingsPath}:\n${e.stack ?? e.message}\n`);
|
|
3252
|
+
process.exit(1);
|
|
3253
|
+
}
|
|
3254
|
+
}
|