@xoxoai/checkmate 0.6.7 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -257
- package/dist/checkmate.js +13375 -0
- package/package.json +30 -94
- package/plugin/.claude-plugin/plugin.json +10 -0
- package/plugin/bin/checkmate.ts +3 -0
- package/plugin/contracts/finding.schema.json +78 -0
- package/plugin/contracts/gap.schema.json +56 -0
- package/plugin/contracts/plan-candidate.schema.json +103 -0
- package/plugin/contracts/plan-delta.schema.json +63 -0
- package/plugin/contracts/plan-patch.schema.json +116 -0
- package/plugin/contracts/plan-request.schema.json +108 -0
- package/plugin/contracts/plan.schema.json +313 -0
- package/plugin/contracts/profile.schema.json +154 -0
- package/plugin/contracts/response.schema.json +25 -0
- package/plugin/contracts/result.schema.json +410 -0
- package/plugin/dist/checkmate.js +13012 -0
- package/plugin/package.json +4 -0
- package/plugin/references/browser.md +35 -0
- package/plugin/references/http.md +30 -0
- package/plugin/references/workflow.md +122 -0
- package/plugin/skills/execute/SKILL.md +92 -0
- package/plugin/skills/plan/SKILL.md +94 -0
- package/plugin/skills/report/SKILL.md +28 -0
- package/plugin/skills/revise/SKILL.md +43 -0
- package/plugin/skills/setup/SKILL.md +82 -0
- package/plugin/skills/status/SKILL.md +20 -0
- package/plugin/skills/ui/SKILL.md +26 -0
- package/plugin/src/actor.ts +21 -0
- package/plugin/src/cli.ts +1133 -0
- package/plugin/src/contracts.ts +342 -0
- package/plugin/src/index.ts +52 -0
- package/plugin/src/plans.ts +1028 -0
- package/plugin/src/render.ts +544 -0
- package/plugin/src/runs.ts +868 -0
- package/plugin/src/store.ts +384 -0
- package/plugin/src/targets.ts +205 -0
- package/plugin/src/types.ts +319 -0
- package/plugin/src/ui/assets/client.js +501 -0
- package/plugin/src/ui/assets/favicon.svg +1 -0
- package/plugin/src/ui/assets/fonts/inter-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/inter.woff2 +0 -0
- package/plugin/src/ui/assets/fonts/jetbrains-mono-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/jetbrains-mono.woff2 +0 -0
- package/plugin/src/ui/assets/fonts/space-grotesk-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/space-grotesk.woff2 +0 -0
- package/plugin/src/ui/assets/style.css +1926 -0
- package/plugin/src/ui/edit.ts +305 -0
- package/plugin/src/ui/forms.ts +209 -0
- package/plugin/src/ui/html.ts +130 -0
- package/plugin/src/ui/layout.ts +198 -0
- package/plugin/src/ui/lists.ts +645 -0
- package/plugin/src/ui/plan.ts +577 -0
- package/plugin/src/ui/run.ts +489 -0
- package/plugin/src/ui/server.ts +684 -0
- package/plugin/src/validators.d.ts +13 -0
- package/plugin/src/validators.js +1 -0
- package/plugin/src/views.ts +839 -0
- package/bin/checkmate.js +0 -13
- package/dist/ai/client.d.ts +0 -64
- package/dist/ai/client.d.ts.map +0 -1
- package/dist/ai/client.js +0 -313
- package/dist/ai/client.js.map +0 -1
- package/dist/ai/message-handler.d.ts +0 -9
- package/dist/ai/message-handler.d.ts.map +0 -1
- package/dist/ai/message-handler.js +0 -50
- package/dist/ai/message-handler.js.map +0 -1
- package/dist/ai/message-history.d.ts +0 -17
- package/dist/ai/message-history.d.ts.map +0 -1
- package/dist/ai/message-history.js +0 -45
- package/dist/ai/message-history.js.map +0 -1
- package/dist/ai/prompts.d.ts +0 -4
- package/dist/ai/prompts.d.ts.map +0 -1
- package/dist/ai/prompts.js +0 -24
- package/dist/ai/prompts.js.map +0 -1
- package/dist/ai/rate-limit-policy.d.ts +0 -9
- package/dist/ai/rate-limit-policy.d.ts.map +0 -1
- package/dist/ai/rate-limit-policy.js +0 -34
- package/dist/ai/rate-limit-policy.js.map +0 -1
- package/dist/ai/tool-response-handler.d.ts +0 -16
- package/dist/ai/tool-response-handler.d.ts.map +0 -1
- package/dist/ai/tool-response-handler.js +0 -85
- package/dist/ai/tool-response-handler.js.map +0 -1
- package/dist/ai/turn-processor.d.ts +0 -34
- package/dist/ai/turn-processor.d.ts.map +0 -1
- package/dist/ai/turn-processor.js +0 -107
- package/dist/ai/turn-processor.js.map +0 -1
- package/dist/api/allocate-run-identity.d.ts +0 -8
- package/dist/api/allocate-run-identity.d.ts.map +0 -1
- package/dist/api/allocate-run-identity.js +0 -10
- package/dist/api/allocate-run-identity.js.map +0 -1
- package/dist/api/execute-prepared-run.d.ts +0 -22
- package/dist/api/execute-prepared-run.d.ts.map +0 -1
- package/dist/api/execute-prepared-run.js +0 -184
- package/dist/api/execute-prepared-run.js.map +0 -1
- package/dist/api/index.d.ts +0 -25
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -69
- package/dist/api/index.js.map +0 -1
- package/dist/api/prepare-run.d.ts +0 -44
- package/dist/api/prepare-run.d.ts.map +0 -1
- package/dist/api/prepare-run.js +0 -261
- package/dist/api/prepare-run.js.map +0 -1
- package/dist/cli/diagnostic-relay.d.ts +0 -6
- package/dist/cli/diagnostic-relay.d.ts.map +0 -1
- package/dist/cli/diagnostic-relay.js +0 -6
- package/dist/cli/diagnostic-relay.js.map +0 -1
- package/dist/cli/main.d.ts +0 -21
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -166
- package/dist/cli/main.js.map +0 -1
- package/dist/cli/protocol.d.ts +0 -57
- package/dist/cli/protocol.d.ts.map +0 -1
- package/dist/cli/protocol.js +0 -116
- package/dist/cli/protocol.js.map +0 -1
- package/dist/cli/reconcile-result.d.ts +0 -13
- package/dist/cli/reconcile-result.d.ts.map +0 -1
- package/dist/cli/reconcile-result.js +0 -300
- package/dist/cli/reconcile-result.js.map +0 -1
- package/dist/cli/run-parent.d.ts +0 -37
- package/dist/cli/run-parent.d.ts.map +0 -1
- package/dist/cli/run-parent.js +0 -491
- package/dist/cli/run-parent.js.map +0 -1
- package/dist/cli/signals.d.ts +0 -36
- package/dist/cli/signals.d.ts.map +0 -1
- package/dist/cli/signals.js +0 -85
- package/dist/cli/signals.js.map +0 -1
- package/dist/cli/static-commands.d.ts +0 -11
- package/dist/cli/static-commands.d.ts.map +0 -1
- package/dist/cli/static-commands.js +0 -83
- package/dist/cli/static-commands.js.map +0 -1
- package/dist/cli/worker-stream-guard.d.ts +0 -20
- package/dist/cli/worker-stream-guard.d.ts.map +0 -1
- package/dist/cli/worker-stream-guard.js +0 -41
- package/dist/cli/worker-stream-guard.js.map +0 -1
- package/dist/cli/worker.d.ts +0 -3
- package/dist/cli/worker.d.ts.map +0 -1
- package/dist/cli/worker.js +0 -111
- package/dist/cli/worker.js.map +0 -1
- package/dist/config/ingestion.d.ts +0 -58
- package/dist/config/ingestion.d.ts.map +0 -1
- package/dist/config/ingestion.js +0 -283
- package/dist/config/ingestion.js.map +0 -1
- package/dist/config/manifest.d.ts +0 -20
- package/dist/config/manifest.d.ts.map +0 -1
- package/dist/config/manifest.js +0 -91
- package/dist/config/manifest.js.map +0 -1
- package/dist/config/model-egress.d.ts +0 -10
- package/dist/config/model-egress.d.ts.map +0 -1
- package/dist/config/model-egress.js +0 -62
- package/dist/config/model-egress.js.map +0 -1
- package/dist/config/package-resolution.d.ts +0 -4
- package/dist/config/package-resolution.d.ts.map +0 -1
- package/dist/config/package-resolution.js +0 -24
- package/dist/config/package-resolution.js.map +0 -1
- package/dist/config/policy.d.ts +0 -44
- package/dist/config/policy.d.ts.map +0 -1
- package/dist/config/policy.js +0 -141
- package/dist/config/policy.js.map +0 -1
- package/dist/config/record.d.ts +0 -4
- package/dist/config/record.d.ts.map +0 -1
- package/dist/config/record.js +0 -10
- package/dist/config/record.js.map +0 -1
- package/dist/config/secrets.d.ts +0 -11
- package/dist/config/secrets.d.ts.map +0 -1
- package/dist/config/secrets.js +0 -42
- package/dist/config/secrets.js.map +0 -1
- package/dist/contracts/diagnostics.d.ts +0 -4
- package/dist/contracts/diagnostics.d.ts.map +0 -1
- package/dist/contracts/diagnostics.js +0 -31
- package/dist/contracts/diagnostics.js.map +0 -1
- package/dist/contracts/serialize.d.ts +0 -2
- package/dist/contracts/serialize.d.ts.map +0 -1
- package/dist/contracts/serialize.js +0 -90
- package/dist/contracts/serialize.js.map +0 -1
- package/dist/contracts/types.d.ts +0 -316
- package/dist/contracts/types.d.ts.map +0 -1
- package/dist/contracts/types.js +0 -74
- package/dist/contracts/types.js.map +0 -1
- package/dist/contracts/validator.d.ts +0 -8
- package/dist/contracts/validator.d.ts.map +0 -1
- package/dist/contracts/validator.js +0 -71
- package/dist/contracts/validator.js.map +0 -1
- package/dist/driver.d.ts +0 -97
- package/dist/driver.d.ts.map +0 -1
- package/dist/driver.js +0 -24
- package/dist/driver.js.map +0 -1
- package/dist/drivers/descriptor.d.ts +0 -15
- package/dist/drivers/descriptor.d.ts.map +0 -1
- package/dist/drivers/descriptor.js +0 -122
- package/dist/drivers/descriptor.js.map +0 -1
- package/dist/drivers/loader.d.ts +0 -18
- package/dist/drivers/loader.d.ts.map +0 -1
- package/dist/drivers/loader.js +0 -97
- package/dist/drivers/loader.js.map +0 -1
- package/dist/drivers/web/checkmate-driver.json +0 -47
- package/dist/drivers/web/index.d.ts +0 -6
- package/dist/drivers/web/index.d.ts.map +0 -1
- package/dist/drivers/web/index.js +0 -27
- package/dist/drivers/web/index.js.map +0 -1
- package/dist/drivers/web/session.d.ts +0 -25
- package/dist/drivers/web/session.d.ts.map +0 -1
- package/dist/drivers/web/session.js +0 -132
- package/dist/drivers/web/session.js.map +0 -1
- package/dist/drivers/web/tools/network-request-recorder.d.ts +0 -46
- package/dist/drivers/web/tools/network-request-recorder.d.ts.map +0 -1
- package/dist/drivers/web/tools/network-request-recorder.js +0 -164
- package/dist/drivers/web/tools/network-request-recorder.js.map +0 -1
- package/dist/drivers/web/tools/screenshot-service.d.ts +0 -10
- package/dist/drivers/web/tools/screenshot-service.d.ts.map +0 -1
- package/dist/drivers/web/tools/screenshot-service.js +0 -23
- package/dist/drivers/web/tools/screenshot-service.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/index.d.ts +0 -4
- package/dist/drivers/web/tools/snapshot-filter/index.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/index.js +0 -4
- package/dist/drivers/web/tools/snapshot-filter/index.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts +0 -15
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js +0 -99
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts +0 -5
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js +0 -56
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts +0 -3
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js +0 -85
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-service.d.ts +0 -31
- package/dist/drivers/web/tools/snapshot-service.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-service.js +0 -96
- package/dist/drivers/web/tools/snapshot-service.js.map +0 -1
- package/dist/drivers/web/tools/tool.d.ts +0 -76
- package/dist/drivers/web/tools/tool.d.ts.map +0 -1
- package/dist/drivers/web/tools/tool.js +0 -562
- package/dist/drivers/web/tools/tool.js.map +0 -1
- package/dist/drivers/web/tools/transient-state-tracker.d.ts +0 -26
- package/dist/drivers/web/tools/transient-state-tracker.d.ts.map +0 -1
- package/dist/drivers/web/tools/transient-state-tracker.js +0 -301
- package/dist/drivers/web/tools/transient-state-tracker.js.map +0 -1
- package/dist/drivers/web/tools/types.d.ts +0 -11
- package/dist/drivers/web/tools/types.d.ts.map +0 -1
- package/dist/drivers/web/tools/types.js +0 -2
- package/dist/drivers/web/tools/types.js.map +0 -1
- package/dist/evidence/atomic-file.d.ts +0 -23
- package/dist/evidence/atomic-file.d.ts.map +0 -1
- package/dist/evidence/atomic-file.js +0 -104
- package/dist/evidence/atomic-file.js.map +0 -1
- package/dist/evidence/layout.d.ts +0 -30
- package/dist/evidence/layout.d.ts.map +0 -1
- package/dist/evidence/layout.js +0 -152
- package/dist/evidence/layout.js.map +0 -1
- package/dist/evidence/retention.d.ts +0 -5
- package/dist/evidence/retention.d.ts.map +0 -1
- package/dist/evidence/retention.js +0 -10
- package/dist/evidence/retention.js.map +0 -1
- package/dist/evidence/store.d.ts +0 -101
- package/dist/evidence/store.d.ts.map +0 -1
- package/dist/evidence/store.js +0 -375
- package/dist/evidence/store.js.map +0 -1
- package/dist/evidence/terminal-finalizer.d.ts +0 -19
- package/dist/evidence/terminal-finalizer.d.ts.map +0 -1
- package/dist/evidence/terminal-finalizer.js +0 -56
- package/dist/evidence/terminal-finalizer.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -12
- package/dist/index.js.map +0 -1
- package/dist/logging/invocation-logger.d.ts +0 -4
- package/dist/logging/invocation-logger.d.ts.map +0 -1
- package/dist/logging/invocation-logger.js +0 -25
- package/dist/logging/invocation-logger.js.map +0 -1
- package/dist/logging/level-logger.d.ts +0 -11
- package/dist/logging/level-logger.d.ts.map +0 -1
- package/dist/logging/level-logger.js +0 -82
- package/dist/logging/level-logger.js.map +0 -1
- package/dist/logging/types.d.ts +0 -8
- package/dist/logging/types.d.ts.map +0 -1
- package/dist/logging/types.js +0 -7
- package/dist/logging/types.js.map +0 -1
- package/dist/redaction/diagnostic-sanitizer.d.ts +0 -7
- package/dist/redaction/diagnostic-sanitizer.d.ts.map +0 -1
- package/dist/redaction/diagnostic-sanitizer.js +0 -42
- package/dist/redaction/diagnostic-sanitizer.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -40
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -176
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/redaction/scrub.d.ts +0 -29
- package/dist/redaction/scrub.d.ts.map +0 -1
- package/dist/redaction/scrub.js +0 -54
- package/dist/redaction/scrub.js.map +0 -1
- package/dist/runtime/config.d.ts +0 -16
- package/dist/runtime/config.d.ts.map +0 -1
- package/dist/runtime/config.js +0 -2
- package/dist/runtime/config.js.map +0 -1
- package/dist/runtime/driver-boundary.d.ts +0 -16
- package/dist/runtime/driver-boundary.d.ts.map +0 -1
- package/dist/runtime/driver-boundary.js +0 -71
- package/dist/runtime/driver-boundary.js.map +0 -1
- package/dist/runtime/driver-session.d.ts +0 -9
- package/dist/runtime/driver-session.d.ts.map +0 -1
- package/dist/runtime/driver-session.js +0 -64
- package/dist/runtime/driver-session.js.map +0 -1
- package/dist/runtime/internal-step-evidence.d.ts +0 -30
- package/dist/runtime/internal-step-evidence.d.ts.map +0 -1
- package/dist/runtime/internal-step-evidence.js +0 -82
- package/dist/runtime/internal-step-evidence.js.map +0 -1
- package/dist/runtime/result-adapter.d.ts +0 -9
- package/dist/runtime/result-adapter.d.ts.map +0 -1
- package/dist/runtime/result-adapter.js +0 -77
- package/dist/runtime/result-adapter.js.map +0 -1
- package/dist/runtime/runner.d.ts +0 -43
- package/dist/runtime/runner.d.ts.map +0 -1
- package/dist/runtime/runner.js +0 -72
- package/dist/runtime/runner.js.map +0 -1
- package/dist/runtime/scenario-control.d.ts +0 -40
- package/dist/runtime/scenario-control.d.ts.map +0 -1
- package/dist/runtime/scenario-control.js +0 -94
- package/dist/runtime/scenario-control.js.map +0 -1
- package/dist/runtime/scenario-runner.d.ts +0 -41
- package/dist/runtime/scenario-runner.d.ts.map +0 -1
- package/dist/runtime/scenario-runner.js +0 -274
- package/dist/runtime/scenario-runner.js.map +0 -1
- package/dist/runtime/scenario-state.d.ts +0 -55
- package/dist/runtime/scenario-state.d.ts.map +0 -1
- package/dist/runtime/scenario-state.js +0 -128
- package/dist/runtime/scenario-state.js.map +0 -1
- package/dist/runtime/step-execution.d.ts +0 -27
- package/dist/runtime/step-execution.d.ts.map +0 -1
- package/dist/runtime/step-execution.js +0 -143
- package/dist/runtime/step-execution.js.map +0 -1
- package/dist/runtime/text.d.ts +0 -10
- package/dist/runtime/text.d.ts.map +0 -1
- package/dist/runtime/text.js +0 -25
- package/dist/runtime/text.js.map +0 -1
- package/dist/runtime/types.d.ts +0 -53
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -2
- package/dist/runtime/types.js.map +0 -1
- package/dist/runtime/usage-tracker.d.ts +0 -37
- package/dist/runtime/usage-tracker.d.ts.map +0 -1
- package/dist/runtime/usage-tracker.js +0 -110
- package/dist/runtime/usage-tracker.js.map +0 -1
- package/dist/tools/define-agent-tool.d.ts +0 -20
- package/dist/tools/define-agent-tool.d.ts.map +0 -1
- package/dist/tools/define-agent-tool.js +0 -30
- package/dist/tools/define-agent-tool.js.map +0 -1
- package/dist/tools/dispatcher.d.ts +0 -19
- package/dist/tools/dispatcher.d.ts.map +0 -1
- package/dist/tools/dispatcher.js +0 -91
- package/dist/tools/dispatcher.js.map +0 -1
- package/dist/tools/loop-detector.d.ts +0 -27
- package/dist/tools/loop-detector.d.ts.map +0 -1
- package/dist/tools/loop-detector.js +0 -76
- package/dist/tools/loop-detector.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -19
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -54
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/step/result-tool.d.ts +0 -7
- package/dist/tools/step/result-tool.d.ts.map +0 -1
- package/dist/tools/step/result-tool.js +0 -34
- package/dist/tools/step/result-tool.js.map +0 -1
- package/dist/tools/tool-contract.d.ts +0 -3
- package/dist/tools/tool-contract.d.ts.map +0 -1
- package/dist/tools/tool-contract.js +0 -2
- package/dist/tools/tool-contract.js.map +0 -1
- package/dist/tools/types.d.ts +0 -210
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js +0 -4
- package/dist/tools/types.js.map +0 -1
- package/docs/CLI.md +0 -59
- package/docs/CONFIGURATION.md +0 -105
- package/docs/DRIVERS.md +0 -79
- package/docs/EVIDENCE.md +0 -64
- package/schemas/checkmate-config.v1.json +0 -176
- package/schemas/describe-result.v1.json +0 -414
- package/schemas/driver-descriptor.v1.json +0 -66
- package/schemas/run-request.v1.json +0 -67
- package/schemas/run-result.v1.json +0 -333
- package/schemas/validation-result.v1.json +0 -86
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import type { Store } from '../store.ts'
|
|
2
|
+
import type { Plan } from '../types.ts'
|
|
3
|
+
import type { Html } from './html.ts'
|
|
4
|
+
import type { Context, Page } from './layout.ts'
|
|
5
|
+
import type { FormState } from './forms.ts'
|
|
6
|
+
import { planDetail, planHistory, planStatus } from '../views.ts'
|
|
7
|
+
import { planRunGaps } from '../plans.ts'
|
|
8
|
+
import { gapsAffecting } from '../contracts.ts'
|
|
9
|
+
import { html, join, pill, plural, short, time, words } from './html.ts'
|
|
10
|
+
import { banner, downloads, heading } from './layout.ts'
|
|
11
|
+
import { emptyState, gapPanel, recordPanel, verdictPanel } from './forms.ts'
|
|
12
|
+
import { counts, loopState, runStatus } from './lists.ts'
|
|
13
|
+
|
|
14
|
+
type Detail = ReturnType<typeof planDetail>
|
|
15
|
+
type Test = Plan['tests'][number]
|
|
16
|
+
type Gap = Plan['gaps'][number]
|
|
17
|
+
|
|
18
|
+
function testLink(planId: string, revision: number, reference: string) {
|
|
19
|
+
const [owner, id] = reference.includes(':') ? reference.split(':') : [null, reference]
|
|
20
|
+
return owner
|
|
21
|
+
? html`<a class="mono" href="/plans/${owner}#${id}">${reference}</a>`
|
|
22
|
+
: html`<a class="mono" href="/plans/${planId}@${revision}#${id}">${reference}</a>`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ids(values: string[], link: (id: string) => Html) {
|
|
26
|
+
return values.length ? join(values.map(link), ', ') : html`<span class="muted">none</span>`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function facts(rows: Array<[string, Html | string | false]>) {
|
|
30
|
+
const shown = rows.filter(([, value]) => value !== false)
|
|
31
|
+
return html`<dl class="facts">
|
|
32
|
+
${shown.map(
|
|
33
|
+
([label, value]) =>
|
|
34
|
+
html`<div>
|
|
35
|
+
<dt>${label}</dt>
|
|
36
|
+
<dd>${value}</dd>
|
|
37
|
+
</div>`
|
|
38
|
+
)}
|
|
39
|
+
</dl>`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function testCard(detail: Detail, test: Test) {
|
|
43
|
+
const { planId, revision } = detail
|
|
44
|
+
return html`<article class="test" id="${test.id}" data-key="test:${test.id}">
|
|
45
|
+
<header class="test-h">
|
|
46
|
+
${pill(test.readiness)}
|
|
47
|
+
<a class="id" href="#${test.id}">${test.id}</a>
|
|
48
|
+
<h3>${test.title}</h3>
|
|
49
|
+
<span class="count">${test.capability} · ${plural(test.assertions.length, 'assertion')}</span>
|
|
50
|
+
</header>
|
|
51
|
+
<div class="test-b">
|
|
52
|
+
${facts([
|
|
53
|
+
['Requirements', ids(test.requirementIds, (id) => html`<a class="mono" href="#${id}">${id}</a>`)],
|
|
54
|
+
[
|
|
55
|
+
'Depends on',
|
|
56
|
+
test.dependsOn.length > 0 && ids(test.dependsOn, (id) => testLink(planId, revision, id)),
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
'Gaps',
|
|
60
|
+
test.gapIds.length > 0 && ids(test.gapIds, (id) => html`<a class="mono" href="#${id}">${id}</a>`),
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
'Operations',
|
|
64
|
+
test.operations.length > 0 && html`<span class="mono">${test.operations.join(', ')}</span>`,
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
'Preconditions',
|
|
68
|
+
test.preconditions.length > 0 &&
|
|
69
|
+
html`<ul class="plain">
|
|
70
|
+
${test.preconditions.map((item) => html`<li>${item}</li>`)}
|
|
71
|
+
</ul>`,
|
|
72
|
+
],
|
|
73
|
+
['Action', test.action],
|
|
74
|
+
])}
|
|
75
|
+
</div>
|
|
76
|
+
${
|
|
77
|
+
test.assertions.length
|
|
78
|
+
? html`<ul class="checklist" aria-label="Assertions of ${test.id}">
|
|
79
|
+
${test.assertions.map(
|
|
80
|
+
(assertion) =>
|
|
81
|
+
html`<li class="asr" id="${test.id}.${assertion.id}">
|
|
82
|
+
<span class="box" aria-hidden="true"></span>
|
|
83
|
+
<span class="aid">${assertion.id}</span>
|
|
84
|
+
<span class="check">${assertion.check}</span>
|
|
85
|
+
<span class="tag">${words(assertion.checkKind)}</span>
|
|
86
|
+
</li>`
|
|
87
|
+
)}
|
|
88
|
+
</ul>`
|
|
89
|
+
: html`<p class="test-note">No assertions yet. A blocking gap must be answered first.</p>`
|
|
90
|
+
}
|
|
91
|
+
</article>`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function decisionFor(plan: Plan, gap: Gap) {
|
|
95
|
+
return plan.decisions.find((decision) => decision.id === gap.decisionId)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function gapRow(detail: Detail, gap: Gap) {
|
|
99
|
+
const decision = decisionFor(detail.plan, gap)
|
|
100
|
+
const pending = detail.pendingDecisions.find((item) => item.gapId === gap.id)
|
|
101
|
+
const answerable = gap.status === 'open' && !pending && detail.revision === detail.latestRevision
|
|
102
|
+
const affects = [...gap.requirementIds, ...gap.testIds]
|
|
103
|
+
const head = html`<span class="id mono">${gap.id}</span>
|
|
104
|
+
${pill(gap.severity)} ${pill(gap.status)}
|
|
105
|
+
<span class="gap-refs muted"
|
|
106
|
+
>${affects.length ? `affects ${affects.join(', ')}` : 'affects the whole plan'}</span
|
|
107
|
+
>`
|
|
108
|
+
const content = html`<p class="muted">${gap.consequence}</p>
|
|
109
|
+
${
|
|
110
|
+
decision
|
|
111
|
+
? html`<p class="gap-d">
|
|
112
|
+
${pill(decision.kind)} ${decision.value} <span class="muted">· ${decision.actor}</span>
|
|
113
|
+
</p>`
|
|
114
|
+
: ''
|
|
115
|
+
}
|
|
116
|
+
${
|
|
117
|
+
pending
|
|
118
|
+
? html`<p class="gap-d">
|
|
119
|
+
${pill('pending', 'decision pending')} ${words(pending.kind)}: ${pending.value}
|
|
120
|
+
<span class="muted">· ${pending.actor} · applies in the next revision</span>
|
|
121
|
+
</p>`
|
|
122
|
+
: ''
|
|
123
|
+
}
|
|
124
|
+
${
|
|
125
|
+
answerable
|
|
126
|
+
? html`<p>
|
|
127
|
+
<a class="btn small" href="/plans/${detail.planId}@${detail.revision}/gaps/${gap.id}"
|
|
128
|
+
>Answer ${gap.id}</a
|
|
129
|
+
>
|
|
130
|
+
</p>`
|
|
131
|
+
: ''
|
|
132
|
+
}`
|
|
133
|
+
if (gap.status === 'open')
|
|
134
|
+
return html`<li class="gap" id="${gap.id}" data-key="gap:${gap.id}">
|
|
135
|
+
<div class="gap-h">${head}</div>
|
|
136
|
+
<p class="gap-q">${gap.question}</p>
|
|
137
|
+
${content}
|
|
138
|
+
</li>`
|
|
139
|
+
return html`<li class="gap closed" id="${gap.id}" data-key="gap:${gap.id}">
|
|
140
|
+
<details data-open-key="gap:${gap.id}">
|
|
141
|
+
<summary>
|
|
142
|
+
<span class="gap-h">${head}</span>
|
|
143
|
+
<span class="gap-q">${gap.question}</span>
|
|
144
|
+
</summary>
|
|
145
|
+
<div class="gap-b">${content}</div>
|
|
146
|
+
</details>
|
|
147
|
+
</li>`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function gapsList(detail: Detail) {
|
|
151
|
+
const gaps = detail.plan.gaps
|
|
152
|
+
if (!gaps.length) return html`<p class="empty-line">No gaps.</p>`
|
|
153
|
+
const open = gaps.filter((gap) => gap.status === 'open')
|
|
154
|
+
const closed = gaps.filter((gap) => gap.status !== 'open')
|
|
155
|
+
return html`<ul class="gaps">
|
|
156
|
+
${[...open, ...closed].map((gap) => gapRow(detail, gap))}
|
|
157
|
+
</ul>`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runsTable(detail: Detail) {
|
|
161
|
+
if (!detail.runs.length) return html`<p class="empty-line">No run uses this plan yet.</p>`
|
|
162
|
+
return html`<div class="scroll">
|
|
163
|
+
<table>
|
|
164
|
+
<thead>
|
|
165
|
+
<tr>
|
|
166
|
+
<th scope="col">Run</th>
|
|
167
|
+
<th scope="col">Revision</th>
|
|
168
|
+
<th scope="col">Build</th>
|
|
169
|
+
<th scope="col">Tests</th>
|
|
170
|
+
<th scope="col">Completion</th>
|
|
171
|
+
<th scope="col">Acceptance</th>
|
|
172
|
+
<th scope="col">Started</th>
|
|
173
|
+
</tr>
|
|
174
|
+
</thead>
|
|
175
|
+
<tbody>
|
|
176
|
+
${[...detail.runs].reverse().map(
|
|
177
|
+
(run) =>
|
|
178
|
+
html`<tr data-key="run:${run.runId}">
|
|
179
|
+
<td><a class="mono" href="/runs/${run.runId}">${run.runId}</a></td>
|
|
180
|
+
<td>
|
|
181
|
+
<a class="mono" href="/plans/${detail.planId}@${run.revision}">@${run.revision}</a>
|
|
182
|
+
</td>
|
|
183
|
+
<td class="mono">${run.build}</td>
|
|
184
|
+
<td>${counts(run.counts)}</td>
|
|
185
|
+
<td>${run.completion ? pill(run.completion) : html`<span class="muted">running</span>`}</td>
|
|
186
|
+
<td>${runStatus(run)}</td>
|
|
187
|
+
<td class="nowrap">${time(run.startedAt)}</td>
|
|
188
|
+
</tr>`
|
|
189
|
+
)}
|
|
190
|
+
</tbody>
|
|
191
|
+
</table>
|
|
192
|
+
</div>`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function requirementsTable(plan: Plan) {
|
|
196
|
+
return html`<div class="scroll">
|
|
197
|
+
<table>
|
|
198
|
+
<thead>
|
|
199
|
+
<tr>
|
|
200
|
+
<th scope="col">ID</th>
|
|
201
|
+
<th scope="col">Requirement</th>
|
|
202
|
+
<th scope="col">Tests</th>
|
|
203
|
+
<th scope="col">Coverage</th>
|
|
204
|
+
</tr>
|
|
205
|
+
</thead>
|
|
206
|
+
<tbody>
|
|
207
|
+
${plan.requirements.map((requirement) => {
|
|
208
|
+
const coverage = plan.coverage.find((row) => row.requirementId === requirement.id)
|
|
209
|
+
return html`<tr id="${requirement.id}">
|
|
210
|
+
<td class="mono">${requirement.id}</td>
|
|
211
|
+
<td>
|
|
212
|
+
${requirement.text}
|
|
213
|
+
<div class="muted small">Source <code>${requirement.source}</code></div>
|
|
214
|
+
</td>
|
|
215
|
+
<td>${ids(coverage?.testIds ?? [], (id) => html`<a class="mono" href="#${id}">${id}</a>`)}</td>
|
|
216
|
+
<td>${coverage ? pill(coverage.status) : ''}</td>
|
|
217
|
+
</tr>`
|
|
218
|
+
})}
|
|
219
|
+
</tbody>
|
|
220
|
+
</table>
|
|
221
|
+
</div>`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function decisionsList(plan: Plan) {
|
|
225
|
+
if (!plan.decisions.length && !plan.assumptions.length)
|
|
226
|
+
return html`<p class="empty-line">No decision applied yet.</p>`
|
|
227
|
+
return html`<ul class="records">
|
|
228
|
+
${plan.decisions.map(
|
|
229
|
+
(decision) =>
|
|
230
|
+
html`<li>
|
|
231
|
+
<div class="record-h">
|
|
232
|
+
${pill(decision.kind)} <a class="mono" href="#${decision.gapId}">${decision.gapId}</a>
|
|
233
|
+
<span class="muted">by ${decision.actor}</span>
|
|
234
|
+
<span class="mono muted small">${decision.id}</span>
|
|
235
|
+
</div>
|
|
236
|
+
<p>${decision.value}</p>
|
|
237
|
+
<p class="muted small">
|
|
238
|
+
Rationale:
|
|
239
|
+
${decision.rationale}${decision.source ? html` · Source <code>${decision.source}</code>` : ''}
|
|
240
|
+
</p>
|
|
241
|
+
</li>`
|
|
242
|
+
)}
|
|
243
|
+
</ul>
|
|
244
|
+
${
|
|
245
|
+
plan.assumptions.length
|
|
246
|
+
? html`<h3 class="minor">Assumptions</h3>
|
|
247
|
+
<ul class="plain">
|
|
248
|
+
${plan.assumptions.map((item) => html`<li>${item.text}</li>`)}
|
|
249
|
+
</ul>`
|
|
250
|
+
: ''
|
|
251
|
+
}`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function changeLine(line: string) {
|
|
255
|
+
const arrow = line.indexOf(' → ')
|
|
256
|
+
const colon = line.indexOf(': ')
|
|
257
|
+
if (arrow < 0 || colon < 0 || colon > arrow) return html`<li><code>${line}</code></li>`
|
|
258
|
+
return html`<li>
|
|
259
|
+
<code>${line.slice(0, colon)}</code>
|
|
260
|
+
<span class="was">${line.slice(colon + 2, arrow)}</span>
|
|
261
|
+
<span aria-hidden="true" class="arrow">→</span><span class="sr-only">changed to</span>
|
|
262
|
+
<span class="now">${line.slice(arrow + 3)}</span>
|
|
263
|
+
</li>`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const historyShown = 6
|
|
267
|
+
|
|
268
|
+
export function planHistoryList(store: Store, detail: Detail, all: boolean) {
|
|
269
|
+
const revisions = planHistory(store, detail.planId).reverse()
|
|
270
|
+
const shown = all ? revisions : revisions.slice(0, historyShown)
|
|
271
|
+
const hidden = revisions.length - shown.length
|
|
272
|
+
const subject = `${detail.planId}@${detail.revision}`
|
|
273
|
+
return html`<div class="history-wrap" data-history>
|
|
274
|
+
<ol class="history${hidden ? ' clipped' : ''}">
|
|
275
|
+
${shown.map(
|
|
276
|
+
(item, index) =>
|
|
277
|
+
html`<li class="${item.revision === detail.revision ? 'current' : ''}">
|
|
278
|
+
<details ${index === 0 ? html`open` : ''} data-open-key="revision:${item.revision}">
|
|
279
|
+
<summary class="record-h">
|
|
280
|
+
<span class="mono rev">${detail.planId}@${item.revision}</span>
|
|
281
|
+
<span class="muted">by ${item.actor} · ${time(item.at)}</span>
|
|
282
|
+
<code class="muted small" title="${item.sha256}">${short(item.sha256)}</code>
|
|
283
|
+
<span class="muted small"
|
|
284
|
+
>${item.changes.length ? plural(item.changes.length, 'change') : 'first revision'}</span
|
|
285
|
+
>
|
|
286
|
+
</summary>
|
|
287
|
+
<div class="history-b">
|
|
288
|
+
${
|
|
289
|
+
item.changes.length
|
|
290
|
+
? html`<ul class="changes">
|
|
291
|
+
${item.changes.map(changeLine)}
|
|
292
|
+
</ul>`
|
|
293
|
+
: html`<p class="muted small">First revision.</p>`
|
|
294
|
+
}
|
|
295
|
+
<p class="small">
|
|
296
|
+
<a href="/plans/${detail.planId}@${item.revision}"
|
|
297
|
+
>Open ${detail.planId}@${item.revision}</a
|
|
298
|
+
>
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
</details>
|
|
302
|
+
</li>`
|
|
303
|
+
)}
|
|
304
|
+
</ol>
|
|
305
|
+
${
|
|
306
|
+
hidden
|
|
307
|
+
? html`<a
|
|
308
|
+
class="history-more"
|
|
309
|
+
href="/plans/${subject}?history=all#history"
|
|
310
|
+
data-history-more="/plans/${subject}/history"
|
|
311
|
+
><span class="btn small">Show full history · ${plural(hidden, 'earlier revision')}</span></a
|
|
312
|
+
>`
|
|
313
|
+
: ''
|
|
314
|
+
}
|
|
315
|
+
</div>`
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function approvalPanel(context: Context, detail: Detail, state: FormState | null) {
|
|
319
|
+
const subject = `${detail.planId}@${detail.revision}`
|
|
320
|
+
const latest = detail.approval.latest
|
|
321
|
+
if (detail.approval.policy === 'none')
|
|
322
|
+
return recordPanel('Approval', html`<p class="help">The target policy does not require plan approval.</p>`)
|
|
323
|
+
if (latest && latest.sha256 === detail.sha256 && !state)
|
|
324
|
+
return recordPanel(
|
|
325
|
+
'Approval',
|
|
326
|
+
html`<p class="subj">${pill(latest.verdict)} ${subject}</p>
|
|
327
|
+
<p class="help">
|
|
328
|
+
By ${latest.actor} · ${time(latest.at)} · sha256 <code>${short(latest.sha256)}</code>
|
|
329
|
+
</p>
|
|
330
|
+
<blockquote>${latest.rationale}</blockquote>
|
|
331
|
+
${latest.sha256 !== detail.sha256 ? banner('warn', 'This decision was for another digest.') : ''}`
|
|
332
|
+
)
|
|
333
|
+
return verdictPanel(
|
|
334
|
+
context,
|
|
335
|
+
{
|
|
336
|
+
action: `/plans/${subject}`,
|
|
337
|
+
subject,
|
|
338
|
+
question: `Approve ${subject}?`,
|
|
339
|
+
sha256: detail.sha256,
|
|
340
|
+
digestLabel: 'Plan',
|
|
341
|
+
approve: {
|
|
342
|
+
value: 'approve',
|
|
343
|
+
label: `Approve ${subject}`,
|
|
344
|
+
confirm: 'Confirm approval',
|
|
345
|
+
summary: `Approve ${subject}. Runs can then use this revision.`,
|
|
346
|
+
},
|
|
347
|
+
reject: {
|
|
348
|
+
value: 'reject',
|
|
349
|
+
label: 'Reject',
|
|
350
|
+
confirm: 'Confirm rejection',
|
|
351
|
+
summary: `Reject ${subject}. The agent must revise the plan.`,
|
|
352
|
+
},
|
|
353
|
+
staleMessage: `${detail.planId} changed. Review it before you decide.`,
|
|
354
|
+
intro: html`<p class="help">${planSummaryLine(detail.plan)}</p>`,
|
|
355
|
+
},
|
|
356
|
+
state ?? emptyState()
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function planSummaryLine(plan: Plan) {
|
|
361
|
+
const count = (readiness: string) => plan.tests.filter((test) => test.readiness === readiness).length
|
|
362
|
+
const open = plan.gaps.filter((gap) => gap.status === 'open').length
|
|
363
|
+
return `${plural(plan.tests.length, 'test')}: ${count('ready')} ready, ${count('blocked')} blocked, ${count('deferred')} deferred. ${plural(open, 'open gap')}.`
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function newerBanner(detail: Detail) {
|
|
367
|
+
if (detail.revision === detail.latestRevision) return ''
|
|
368
|
+
return banner(
|
|
369
|
+
'info',
|
|
370
|
+
html`Revision ${detail.latestRevision} is available.
|
|
371
|
+
<a href="/plans/${detail.planId}@${detail.latestRevision}"
|
|
372
|
+
>Review ${detail.planId}@${detail.latestRevision}</a
|
|
373
|
+
>`
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function side(content: Html) {
|
|
378
|
+
return html`<aside class="side" aria-label="Decision">
|
|
379
|
+
${content}
|
|
380
|
+
<p class="back"><a href="/">← Back to inbox</a></p>
|
|
381
|
+
</aside>`
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function planPage(
|
|
385
|
+
store: Store,
|
|
386
|
+
context: Context,
|
|
387
|
+
planId: string,
|
|
388
|
+
revision: number,
|
|
389
|
+
state: FormState | null = null,
|
|
390
|
+
allHistory = false
|
|
391
|
+
): Page {
|
|
392
|
+
const detail = planDetail(store, planId, revision)
|
|
393
|
+
const { plan } = detail
|
|
394
|
+
const subject = `${planId}@${revision}`
|
|
395
|
+
const latest = detail.revision === detail.latestRevision
|
|
396
|
+
const meta = html`<div class="meta">
|
|
397
|
+
${loopState(planStatus(store, planId))} ${pill(detail.approval.state)}
|
|
398
|
+
<span class="pill">target <span class="mono">${plan.target.targetId}@${plan.target.revision}</span></span>
|
|
399
|
+
<span class="pill" title="${detail.sha256}">sha256 <span class="mono">${short(detail.sha256)}</span></span>
|
|
400
|
+
<span class="head-actions">
|
|
401
|
+
${latest ? html`<a class="btn small" href="/plans/${subject}/edit">Edit plan</a>` : ''}
|
|
402
|
+
${downloads('Export', `/plans/${subject}/export`, 'The HTML export is print-ready. Open it and print to PDF.')}
|
|
403
|
+
</span>
|
|
404
|
+
</div>`
|
|
405
|
+
const pending = detail.pendingDecisions.length
|
|
406
|
+
? banner(
|
|
407
|
+
'info',
|
|
408
|
+
html`${plural(detail.pendingDecisions.length, 'decision')}
|
|
409
|
+
${detail.pendingDecisions.length === 1 ? 'waits' : 'wait'} for the next revision:
|
|
410
|
+
${detail.pendingDecisions.map((item) => `${item.gapId} ${words(item.kind)}`).join(', ')}.`
|
|
411
|
+
)
|
|
412
|
+
: ''
|
|
413
|
+
const body = html`${heading(subject, plan.objective, meta)}
|
|
414
|
+
<div data-region="banners">${newerBanner(detail)} ${pending}</div>
|
|
415
|
+
<div class="split">
|
|
416
|
+
<div class="main-col">
|
|
417
|
+
<nav class="jump" aria-label="On this page">
|
|
418
|
+
<a href="#tests">Tests</a><a href="#gaps">Gaps</a><a href="#requirements">Requirements</a
|
|
419
|
+
><a href="#runs">Runs</a><a href="#decisions">Decisions</a><a href="#history">History</a>
|
|
420
|
+
</nav>
|
|
421
|
+
<section aria-labelledby="tests" data-region="tests">
|
|
422
|
+
<h2 id="tests">
|
|
423
|
+
Tests <span class="n">${plan.tests.length}</span>
|
|
424
|
+
${latest ? html`<a class="h-link" href="/plans/${subject}/edit#edit-tests">Edit</a>` : ''}
|
|
425
|
+
</h2>
|
|
426
|
+
<div class="stack">${plan.tests.map((test) => testCard(detail, test))}</div>
|
|
427
|
+
</section>
|
|
428
|
+
<section aria-labelledby="gaps" data-region="gaps">
|
|
429
|
+
<h2 id="gaps">Gaps <span class="n">${plan.gaps.length}</span></h2>
|
|
430
|
+
${gapsList(detail)}
|
|
431
|
+
</section>
|
|
432
|
+
<section aria-labelledby="requirements">
|
|
433
|
+
<h2 id="requirements">
|
|
434
|
+
Requirements and coverage
|
|
435
|
+
${latest ? html`<a class="h-link" href="/plans/${subject}/edit#edit-requirements">Edit</a>` : ''}
|
|
436
|
+
</h2>
|
|
437
|
+
${requirementsTable(plan)}
|
|
438
|
+
</section>
|
|
439
|
+
<section aria-labelledby="runs" data-region="plan-runs">
|
|
440
|
+
<h2 id="runs">Runs <span class="n">${detail.runs.length}</span></h2>
|
|
441
|
+
${runsTable(detail)}
|
|
442
|
+
</section>
|
|
443
|
+
<section aria-labelledby="decisions">
|
|
444
|
+
<h2 id="decisions">Decisions</h2>
|
|
445
|
+
${decisionsList(plan)}
|
|
446
|
+
</section>
|
|
447
|
+
<section aria-labelledby="history" data-region="history">
|
|
448
|
+
<h2 id="history">History</h2>
|
|
449
|
+
${planHistoryList(store, detail, allHistory)}
|
|
450
|
+
</section>
|
|
451
|
+
</div>
|
|
452
|
+
${side(approvalPanel(context, detail, state))}
|
|
453
|
+
</div>`
|
|
454
|
+
return {
|
|
455
|
+
title: subject,
|
|
456
|
+
section: 'plans',
|
|
457
|
+
subjects: [`plan:${planId}`, 'inbox', ...detail.runs.map((run) => `run:${run.runId}`)],
|
|
458
|
+
body,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function gapPage(
|
|
463
|
+
store: Store,
|
|
464
|
+
context: Context,
|
|
465
|
+
planId: string,
|
|
466
|
+
revision: number,
|
|
467
|
+
gapId: string,
|
|
468
|
+
state: FormState | null = null
|
|
469
|
+
): Page {
|
|
470
|
+
const detail = planDetail(store, planId, revision)
|
|
471
|
+
const { plan } = detail
|
|
472
|
+
const latest = detail.revision === detail.latestRevision
|
|
473
|
+
const fromRun =
|
|
474
|
+
plan.gaps.some((item) => item.id === gapId) || !latest ? null : unadoptedRunGap(store, planId, gapId)
|
|
475
|
+
const gap = plan.gaps.find((item) => item.id === gapId) ?? fromRun?.gap
|
|
476
|
+
if (!gap) throw new Error(`Unknown gap ${gapId} in ${planId}@${revision}`)
|
|
477
|
+
const subject = `${planId}@${revision}`
|
|
478
|
+
const pending = detail.pendingDecisions.find((item) => item.gapId === gap.id)
|
|
479
|
+
const decision = decisionFor(plan, gap)
|
|
480
|
+
const tests = plan.tests.filter((test) => gapsAffecting([gap], test).length > 0)
|
|
481
|
+
const requirements = plan.requirements.filter((requirement) => gap.requirementIds.includes(requirement.id))
|
|
482
|
+
const runBanner = fromRun
|
|
483
|
+
? banner(
|
|
484
|
+
'info',
|
|
485
|
+
html`This gap comes from run <a class="mono" href="/runs/${fromRun.runId}">${fromRun.runId}</a>. Your
|
|
486
|
+
answer adds it to ${planId}.`
|
|
487
|
+
)
|
|
488
|
+
: ''
|
|
489
|
+
let panel: Html
|
|
490
|
+
if (state || (latest && gap.status === 'open' && !pending))
|
|
491
|
+
panel = gapPanel(
|
|
492
|
+
context,
|
|
493
|
+
{
|
|
494
|
+
action: `/plans/${subject}/gaps/${gap.id}`,
|
|
495
|
+
cancel: `/plans/${subject}#${gap.id}`,
|
|
496
|
+
sha256: detail.sha256,
|
|
497
|
+
title: html`${planId} · ${gap.id} · ${words(gap.severity)}`,
|
|
498
|
+
question: gap.question,
|
|
499
|
+
staleMessage: `${planId} changed. Review it before you answer.`,
|
|
500
|
+
},
|
|
501
|
+
state ?? emptyState()
|
|
502
|
+
)
|
|
503
|
+
else if (pending)
|
|
504
|
+
panel = recordPanel(
|
|
505
|
+
'Answer recorded',
|
|
506
|
+
html`<p class="subj">${pill(pending.kind)} ${gap.id}</p>
|
|
507
|
+
<blockquote>${pending.value}</blockquote>
|
|
508
|
+
<p class="help">By ${pending.actor}. The agent applies it in the next revision of ${planId}.</p>`
|
|
509
|
+
)
|
|
510
|
+
else if (decision)
|
|
511
|
+
panel = recordPanel(
|
|
512
|
+
'Decision',
|
|
513
|
+
html`<p class="subj">${pill(decision.kind)} ${gap.id}</p>
|
|
514
|
+
<blockquote>${decision.value}</blockquote>
|
|
515
|
+
<p class="help">By ${decision.actor}. Rationale: ${decision.rationale}</p>`
|
|
516
|
+
)
|
|
517
|
+
else
|
|
518
|
+
panel = recordPanel(
|
|
519
|
+
'Answer gap',
|
|
520
|
+
html`<p class="help">Answer this gap on the latest revision.</p>
|
|
521
|
+
<p>
|
|
522
|
+
<a class="btn" href="/plans/${planId}@${detail.latestRevision}/gaps/${gap.id}"
|
|
523
|
+
>Open ${planId}@${detail.latestRevision}</a
|
|
524
|
+
>
|
|
525
|
+
</p>`
|
|
526
|
+
)
|
|
527
|
+
const meta = html`<div class="meta">
|
|
528
|
+
${pill(gap.severity)} ${pill(gap.status)} <a class="pill" href="/plans/${subject}">${subject}</a>
|
|
529
|
+
</div>`
|
|
530
|
+
const body = html`${heading(html`${planId} · ${gap.id}`, gap.question, meta)}
|
|
531
|
+
<div data-region="banners">${newerBanner(detail)} ${runBanner}</div>
|
|
532
|
+
<div class="split">
|
|
533
|
+
<div class="main-col">
|
|
534
|
+
<section aria-labelledby="consequence">
|
|
535
|
+
<h2 id="consequence">Consequence</h2>
|
|
536
|
+
<p class="lead">${gap.consequence}</p>
|
|
537
|
+
</section>
|
|
538
|
+
${
|
|
539
|
+
requirements.length
|
|
540
|
+
? html`<section aria-labelledby="gap-requirements">
|
|
541
|
+
<h2 id="gap-requirements">Requirements</h2>
|
|
542
|
+
<ul class="records">
|
|
543
|
+
${requirements.map(
|
|
544
|
+
(requirement) =>
|
|
545
|
+
html`<li>
|
|
546
|
+
<div class="record-h"><span class="mono">${requirement.id}</span></div>
|
|
547
|
+
<p>${requirement.text}</p>
|
|
548
|
+
<p class="muted small">Source <code>${requirement.source}</code></p>
|
|
549
|
+
</li>`
|
|
550
|
+
)}
|
|
551
|
+
</ul>
|
|
552
|
+
</section>`
|
|
553
|
+
: ''
|
|
554
|
+
}
|
|
555
|
+
<section aria-labelledby="gap-tests">
|
|
556
|
+
<h2 id="gap-tests">Affected tests <span class="n">${tests.length}</span></h2>
|
|
557
|
+
${
|
|
558
|
+
tests.length
|
|
559
|
+
? html`<div class="stack">${tests.map((test) => testCard(detail, test))}</div>`
|
|
560
|
+
: html`<p class="empty-line">No test names this gap.</p>`
|
|
561
|
+
}
|
|
562
|
+
</section>
|
|
563
|
+
</div>
|
|
564
|
+
${side(panel)}
|
|
565
|
+
</div>`
|
|
566
|
+
return {
|
|
567
|
+
title: `${planId} ${gap.id}`,
|
|
568
|
+
section: 'inbox',
|
|
569
|
+
subjects: [`plan:${planId}`, 'inbox', ...(fromRun ? [`run:${fromRun.runId}`] : [])],
|
|
570
|
+
body,
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function unadoptedRunGap(store: Store, planId: string, gapId: string) {
|
|
575
|
+
const found = planRunGaps(store, planId).filter((item) => item.gap.id === gapId && !item.adopted)
|
|
576
|
+
return found.length === 1 ? found[0] : null
|
|
577
|
+
}
|