@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,305 @@
|
|
|
1
|
+
import type { Store } from '../store.ts'
|
|
2
|
+
import type { Html } from './html.ts'
|
|
3
|
+
import type { Context, Page } from './layout.ts'
|
|
4
|
+
import { frozenRevision, planCandidate } from '../plans.ts'
|
|
5
|
+
import { planDetail } from '../views.ts'
|
|
6
|
+
import { html, short } from './html.ts'
|
|
7
|
+
import { banner, heading } from './layout.ts'
|
|
8
|
+
|
|
9
|
+
type Candidate = ReturnType<typeof planCandidate>
|
|
10
|
+
type Requirement = Candidate['requirements'][number]
|
|
11
|
+
type Test = Candidate['tests'][number]
|
|
12
|
+
|
|
13
|
+
export type EditState = {
|
|
14
|
+
mode: 'form' | 'json'
|
|
15
|
+
candidate: Candidate
|
|
16
|
+
json: string
|
|
17
|
+
actor: string
|
|
18
|
+
error: string | null
|
|
19
|
+
stale: Html | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function words(values: string) {
|
|
23
|
+
return values
|
|
24
|
+
.split(/[\s,]+/)
|
|
25
|
+
.map((value) => value.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function lines(values: string) {
|
|
30
|
+
return values
|
|
31
|
+
.split('\n')
|
|
32
|
+
.map((value) => value.trim())
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function candidateFromForm(base: Candidate, form: URLSearchParams): Candidate {
|
|
37
|
+
const text = (name: string, fallback: string) => (form.get(name) ?? fallback).trim()
|
|
38
|
+
const covered = new Set(form.getAll('cover'))
|
|
39
|
+
return {
|
|
40
|
+
requirements: base.requirements.map((requirement, i) => ({
|
|
41
|
+
id: requirement.id,
|
|
42
|
+
text: text(`r${i}.text`, requirement.text),
|
|
43
|
+
source: text(`r${i}.source`, requirement.source),
|
|
44
|
+
})),
|
|
45
|
+
tests: base.tests.map((test, i) => ({
|
|
46
|
+
...test,
|
|
47
|
+
title: text(`t${i}.title`, test.title),
|
|
48
|
+
capability: text(`t${i}.capability`, test.capability),
|
|
49
|
+
operations: words(form.get(`t${i}.operations`) ?? test.operations.join(', ')),
|
|
50
|
+
dependsOn: words(form.get(`t${i}.dependsOn`) ?? test.dependsOn.join(', ')),
|
|
51
|
+
preconditions: lines(form.get(`t${i}.preconditions`) ?? test.preconditions.join('\n')),
|
|
52
|
+
action: text(`t${i}.action`, test.action),
|
|
53
|
+
requirementIds: base.requirements
|
|
54
|
+
.filter((requirement) => covered.has(`${test.id} ${requirement.id}`))
|
|
55
|
+
.map((requirement) => requirement.id),
|
|
56
|
+
assertions: test.assertions.map((assertion, j) => ({
|
|
57
|
+
id: assertion.id,
|
|
58
|
+
check: text(`t${i}.a${j}.check`, assertion.check),
|
|
59
|
+
checkKind: form.get(`t${i}.a${j}.kind`) === 'agent_observation' ? 'agent_observation' : 'deterministic',
|
|
60
|
+
})),
|
|
61
|
+
})),
|
|
62
|
+
gaps: base.gaps,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function candidateJson(candidate: unknown) {
|
|
67
|
+
return JSON.stringify(candidate, null, 2)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function field(label: string, name: string, value: string, help = '') {
|
|
71
|
+
return html`<div class="field-group">
|
|
72
|
+
<label class="lab" for="${name}">${label}</label>
|
|
73
|
+
<input id="${name}" name="${name}" value="${value}" />
|
|
74
|
+
${help ? html`<p class="help">${help}</p>` : ''}
|
|
75
|
+
</div>`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function area(label: string, name: string, value: string, rows: number, help = '') {
|
|
79
|
+
return html`<div class="field-group">
|
|
80
|
+
<label class="lab" for="${name}">${label}</label>
|
|
81
|
+
<textarea id="${name}" name="${name}" rows="${rows}">${value}</textarea>
|
|
82
|
+
${help ? html`<p class="help">${help}</p>` : ''}
|
|
83
|
+
</div>`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function requirementFields(requirement: Requirement, i: number) {
|
|
87
|
+
return html`<fieldset class="edit-card" id="edit-${requirement.id}">
|
|
88
|
+
<legend class="mono">${requirement.id}</legend>
|
|
89
|
+
${area('Text', `r${i}.text`, requirement.text, 2)}
|
|
90
|
+
${field('Source', `r${i}.source`, requirement.source, 'Where the requirement comes from.')}
|
|
91
|
+
</fieldset>`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function testFields(test: Test, i: number) {
|
|
95
|
+
return html`<fieldset class="edit-card" id="edit-${test.id}">
|
|
96
|
+
<legend><span class="mono">${test.id}</span> ${test.title}</legend>
|
|
97
|
+
${field('Title', `t${i}.title`, test.title)}
|
|
98
|
+
<div class="edit-row">
|
|
99
|
+
${field('Capability', `t${i}.capability`, test.capability)}
|
|
100
|
+
${field('Operations', `t${i}.operations`, test.operations.join(', '), 'Separate with commas.')}
|
|
101
|
+
</div>
|
|
102
|
+
${field(
|
|
103
|
+
'Depends on',
|
|
104
|
+
`t${i}.dependsOn`,
|
|
105
|
+
test.dependsOn.join(', '),
|
|
106
|
+
'Test IDs, separated with commas. Use plan:T1 for a test in another plan.'
|
|
107
|
+
)}
|
|
108
|
+
${area('Preconditions', `t${i}.preconditions`, test.preconditions.join('\n'), 2, 'One per line.')}
|
|
109
|
+
${area('Action', `t${i}.action`, test.action, 2)}
|
|
110
|
+
<div class="field-group">
|
|
111
|
+
<p class="lab">Assertions</p>
|
|
112
|
+
${
|
|
113
|
+
test.assertions.length
|
|
114
|
+
? html`<ul class="edit-asrs">
|
|
115
|
+
${test.assertions.map(
|
|
116
|
+
(assertion, j) =>
|
|
117
|
+
html`<li>
|
|
118
|
+
<label class="aid" for="t${i}.a${j}.check">${assertion.id}</label>
|
|
119
|
+
<textarea id="t${i}.a${j}.check" name="t${i}.a${j}.check" rows="1">
|
|
120
|
+
${assertion.check}</textarea>
|
|
121
|
+
<label class="sr-only" for="t${i}.a${j}.kind"
|
|
122
|
+
>Check kind of ${assertion.id}</label
|
|
123
|
+
>
|
|
124
|
+
<select id="t${i}.a${j}.kind" name="t${i}.a${j}.kind">
|
|
125
|
+
${['deterministic', 'agent_observation'].map(
|
|
126
|
+
(kind) =>
|
|
127
|
+
html`<option
|
|
128
|
+
value="${kind}"
|
|
129
|
+
${kind === assertion.checkKind ? html`selected` : ''}
|
|
130
|
+
>
|
|
131
|
+
${kind.replace('_', ' ')}
|
|
132
|
+
</option>`
|
|
133
|
+
)}
|
|
134
|
+
</select>
|
|
135
|
+
</li>`
|
|
136
|
+
)}
|
|
137
|
+
</ul>`
|
|
138
|
+
: html`<p class="help">No assertions. Add them in the JSON editor.</p>`
|
|
139
|
+
}
|
|
140
|
+
</div>
|
|
141
|
+
</fieldset>`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function coverageMatrix(candidate: Candidate) {
|
|
145
|
+
return html`<div class="scroll">
|
|
146
|
+
<table class="matrix">
|
|
147
|
+
<thead>
|
|
148
|
+
<tr>
|
|
149
|
+
<th scope="col">Requirement</th>
|
|
150
|
+
${candidate.tests.map((test) => html`<th scope="col" class="mono">${test.id}</th>`)}
|
|
151
|
+
</tr>
|
|
152
|
+
</thead>
|
|
153
|
+
<tbody>
|
|
154
|
+
${candidate.requirements.map(
|
|
155
|
+
(requirement) =>
|
|
156
|
+
html`<tr>
|
|
157
|
+
<th scope="row">
|
|
158
|
+
<span class="mono">${requirement.id}</span>
|
|
159
|
+
<span class="muted small matrix-text">${requirement.text}</span>
|
|
160
|
+
</th>
|
|
161
|
+
${candidate.tests.map(
|
|
162
|
+
(test) =>
|
|
163
|
+
html`<td>
|
|
164
|
+
<input
|
|
165
|
+
type="checkbox"
|
|
166
|
+
name="cover"
|
|
167
|
+
value="${test.id} ${requirement.id}"
|
|
168
|
+
aria-label="${test.id} covers ${requirement.id}"
|
|
169
|
+
${test.requirementIds.includes(requirement.id) ? html`checked` : ''}
|
|
170
|
+
/>
|
|
171
|
+
</td>`
|
|
172
|
+
)}
|
|
173
|
+
</tr>`
|
|
174
|
+
)}
|
|
175
|
+
</tbody>
|
|
176
|
+
</table>
|
|
177
|
+
</div>`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hidden(context: Context, sha256: string, mode: string) {
|
|
181
|
+
return html`<input type="hidden" name="csrf" value="${context.csrf}" />
|
|
182
|
+
<input type="hidden" name="sha" value="${sha256}" />
|
|
183
|
+
<input type="hidden" name="mode" value="${mode}" />`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function actorLine(context: Context, state: EditState, path: string, mode: string) {
|
|
187
|
+
if (context.actor)
|
|
188
|
+
return html`<p class="help">
|
|
189
|
+
Saving as <strong>${context.actor}</strong>.
|
|
190
|
+
<a href="/actor?back=${encodeURIComponent(path)}">Change name</a>
|
|
191
|
+
</p>`
|
|
192
|
+
return html`<div class="field-group">
|
|
193
|
+
<label class="lab" for="actor-${mode}">Your name</label>
|
|
194
|
+
<input id="actor-${mode}" name="actor" required autocomplete="name" value="${state.actor}" />
|
|
195
|
+
<p class="help">Checkmate records it with the edit.</p>
|
|
196
|
+
</div>`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function editPage(
|
|
200
|
+
store: Store,
|
|
201
|
+
context: Context,
|
|
202
|
+
planId: string,
|
|
203
|
+
revision: number,
|
|
204
|
+
state: EditState | null = null
|
|
205
|
+
): Page {
|
|
206
|
+
const detail = planDetail(store, planId, revision)
|
|
207
|
+
const subject = `${planId}@${revision}`
|
|
208
|
+
const path = `/plans/${subject}/edit`
|
|
209
|
+
const title = `Edit ${subject}`
|
|
210
|
+
if (detail.revision !== detail.latestRevision)
|
|
211
|
+
return {
|
|
212
|
+
title,
|
|
213
|
+
section: 'plans',
|
|
214
|
+
subjects: [`plan:${planId}`],
|
|
215
|
+
body: html`${heading(title, detail.plan.objective)}
|
|
216
|
+
${banner(
|
|
217
|
+
'info',
|
|
218
|
+
html`Only the latest revision can change.
|
|
219
|
+
<a href="/plans/${planId}@${detail.latestRevision}/edit"
|
|
220
|
+
>Edit ${planId}@${detail.latestRevision}</a
|
|
221
|
+
>`
|
|
222
|
+
)}`,
|
|
223
|
+
}
|
|
224
|
+
const frozen = frozenRevision(store, planId, revision)
|
|
225
|
+
const needsApproval = detail.approval.policy !== 'none'
|
|
226
|
+
const next = revision + 1
|
|
227
|
+
const save = frozen ? `Save as ${planId}@${next}` : 'Save changes'
|
|
228
|
+
const why = frozen === 'run' ? `A run uses ${subject}.` : `${subject} is approved.`
|
|
229
|
+
const notice = frozen
|
|
230
|
+
? banner(
|
|
231
|
+
'warn',
|
|
232
|
+
html`<strong
|
|
233
|
+
>${why} Saving creates revision ${next}${needsApproval ? ', which needs approval' : ''}.</strong
|
|
234
|
+
>
|
|
235
|
+
${subject} stays as it is.`
|
|
236
|
+
)
|
|
237
|
+
: banner(
|
|
238
|
+
'info',
|
|
239
|
+
html`<strong>Edits change this revision in place.</strong> ${subject} is not approved and no run uses
|
|
240
|
+
it.`
|
|
241
|
+
)
|
|
242
|
+
const shown = state ?? {
|
|
243
|
+
mode: 'form' as const,
|
|
244
|
+
candidate: planCandidate(detail.plan),
|
|
245
|
+
json: candidateJson(planCandidate(detail.plan)),
|
|
246
|
+
actor: '',
|
|
247
|
+
error: null,
|
|
248
|
+
stale: null,
|
|
249
|
+
}
|
|
250
|
+
const messages = html`${shown.stale ? banner('warn', shown.stale) : ''}
|
|
251
|
+
${shown.error ? html`<p class="form-error" role="alert">${shown.error}</p>` : ''}`
|
|
252
|
+
const buttons = (mode: string) =>
|
|
253
|
+
html`<div class="btns">
|
|
254
|
+
<button class="btn primary">${save}</button>
|
|
255
|
+
<a class="btn" href="/plans/${subject}">Cancel</a>
|
|
256
|
+
</div>
|
|
257
|
+
${actorLine(context, shown, path, mode)}`
|
|
258
|
+
const body = html`${heading(
|
|
259
|
+
title,
|
|
260
|
+
detail.plan.objective,
|
|
261
|
+
html`<div class="meta">
|
|
262
|
+
<a class="pill" href="/plans/${subject}">${subject}</a>
|
|
263
|
+
<span class="pill" title="${detail.sha256}"
|
|
264
|
+
>sha256 <span class="mono">${short(detail.sha256)}</span></span
|
|
265
|
+
>
|
|
266
|
+
</div>`
|
|
267
|
+
)}
|
|
268
|
+
<div data-region="edit" data-stale-message="${planId} changed. Review it before you save.">
|
|
269
|
+
<div data-stale-slot>${notice} ${messages}</div>
|
|
270
|
+
<form method="post" action="${path}" class="edit-form" data-edit-form>
|
|
271
|
+
${hidden(context, detail.sha256, 'form')}
|
|
272
|
+
<section aria-labelledby="edit-requirements">
|
|
273
|
+
<h2 id="edit-requirements">
|
|
274
|
+
Requirements <span class="n">${shown.candidate.requirements.length}</span>
|
|
275
|
+
</h2>
|
|
276
|
+
<div class="stack">${shown.candidate.requirements.map(requirementFields)}</div>
|
|
277
|
+
</section>
|
|
278
|
+
<section aria-labelledby="edit-tests">
|
|
279
|
+
<h2 id="edit-tests">Tests <span class="n">${shown.candidate.tests.length}</span></h2>
|
|
280
|
+
<div class="stack">${shown.candidate.tests.map(testFields)}</div>
|
|
281
|
+
</section>
|
|
282
|
+
<section aria-labelledby="edit-coverage">
|
|
283
|
+
<h2 id="edit-coverage">Coverage</h2>
|
|
284
|
+
<p class="help">Mark the requirements that each test covers. Each test covers one or more.</p>
|
|
285
|
+
${coverageMatrix(shown.candidate)}
|
|
286
|
+
</section>
|
|
287
|
+
<div class="edit-save">${buttons('form')}</div>
|
|
288
|
+
</form>
|
|
289
|
+
<details class="edit-json" ${shown.mode === 'json' ? html`open` : ''}>
|
|
290
|
+
<summary>Edit as JSON</summary>
|
|
291
|
+
<form method="post" action="${path}" class="edit-form" data-edit-form>
|
|
292
|
+
${hidden(context, detail.sha256, 'json')}
|
|
293
|
+
<p class="help">
|
|
294
|
+
Add or remove requirements, tests, assertions, and gaps. The JSON has the fields of a plan
|
|
295
|
+
candidate:
|
|
296
|
+
<code>requirements</code>, <code>tests</code>, and <code>gaps</code>.
|
|
297
|
+
</p>
|
|
298
|
+
<label class="sr-only" for="json">Plan candidate JSON</label>
|
|
299
|
+
<textarea id="json" name="json" rows="24" class="code" spellcheck="false">${shown.json}</textarea>
|
|
300
|
+
${buttons('json')}
|
|
301
|
+
</form>
|
|
302
|
+
</details>
|
|
303
|
+
</div>`
|
|
304
|
+
return { title, section: 'plans', subjects: [`plan:${planId}`], body }
|
|
305
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Html } from './html.ts'
|
|
2
|
+
import type { Context } from './layout.ts'
|
|
3
|
+
import { html, short } from './html.ts'
|
|
4
|
+
import { banner } from './layout.ts'
|
|
5
|
+
|
|
6
|
+
export type FormState = {
|
|
7
|
+
values: Record<string, string>
|
|
8
|
+
error: string | null
|
|
9
|
+
stale: Html | null
|
|
10
|
+
confirm: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Verdict = {
|
|
14
|
+
action: string
|
|
15
|
+
subject: string
|
|
16
|
+
question: string
|
|
17
|
+
sha256: string
|
|
18
|
+
digestLabel: string
|
|
19
|
+
approve: { value: string; label: string; confirm: string; summary: string }
|
|
20
|
+
reject: { value: string; label: string; confirm: string; summary: string }
|
|
21
|
+
staleMessage: string
|
|
22
|
+
intro?: Html | ''
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function emptyState(): FormState {
|
|
26
|
+
return { values: {}, error: null, stale: null, confirm: false }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hidden(context: Context, sha256: string) {
|
|
30
|
+
return html`<input type="hidden" name="csrf" value="${context.csrf}" />
|
|
31
|
+
<input type="hidden" name="sha" value="${sha256}" />`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function actorField(context: Context, state: FormState) {
|
|
35
|
+
if (context.actor) return ''
|
|
36
|
+
return html`<div class="field-group">
|
|
37
|
+
<label class="lab" for="actor">Your name</label>
|
|
38
|
+
<input id="actor" name="actor" required autocomplete="name" value="${state.values.actor ?? ''}" />
|
|
39
|
+
<p class="help">Checkmate asks once, keeps it in a cookie, and records it with each decision.</p>
|
|
40
|
+
</div>`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function messages(state: FormState) {
|
|
44
|
+
return html`${state.stale ? banner('warn', state.stale) : ''}
|
|
45
|
+
${state.error ? html`<p class="form-error" role="alert">${state.error}</p>` : ''}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rationaleField(state: FormState, placeholder: string) {
|
|
49
|
+
return html`<div class="field-group">
|
|
50
|
+
<label class="lab" for="rationale">Rationale</label>
|
|
51
|
+
<textarea id="rationale" name="rationale" rows="4" required data-rationale placeholder="${placeholder}">
|
|
52
|
+
${state.values.rationale ?? ''}</textarea>
|
|
53
|
+
</div>`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function confirmStep(context: Context, verdict: Verdict, state: FormState) {
|
|
57
|
+
const choice = state.values.verdict === verdict.reject.value ? verdict.reject : verdict.approve
|
|
58
|
+
return html`<form method="post" action="${verdict.action}" class="decision-form">
|
|
59
|
+
${hidden(context, verdict.sha256)}
|
|
60
|
+
<input type="hidden" name="verdict" value="${choice.value}" />
|
|
61
|
+
<input type="hidden" name="rationale" value="${state.values.rationale ?? ''}" />
|
|
62
|
+
${context.actor ? '' : html`<input type="hidden" name="actor" value="${state.values.actor ?? ''}" />`}
|
|
63
|
+
<div class="confirm-box">
|
|
64
|
+
<p class="lab">Confirm</p>
|
|
65
|
+
<p>${choice.summary}</p>
|
|
66
|
+
<blockquote>${state.values.rationale ?? ''}</blockquote>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="btns">
|
|
69
|
+
<button class="btn primary" name="confirmed" value="1">${choice.confirm}</button>
|
|
70
|
+
<button class="btn" name="back" value="1" formnovalidate>Back</button>
|
|
71
|
+
</div>
|
|
72
|
+
</form>`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function chooseStep(context: Context, verdict: Verdict, state: FormState) {
|
|
76
|
+
const button = (choice: Verdict['approve'], primary: boolean) =>
|
|
77
|
+
html`<button
|
|
78
|
+
class="btn${primary ? ' primary' : ''}"
|
|
79
|
+
name="verdict"
|
|
80
|
+
value="${choice.value}"
|
|
81
|
+
data-needs-rationale
|
|
82
|
+
data-confirm-label="${choice.confirm}"
|
|
83
|
+
data-summary="${choice.summary}"
|
|
84
|
+
>
|
|
85
|
+
${choice.label}
|
|
86
|
+
</button>`
|
|
87
|
+
return html`<form method="post" action="${verdict.action}" class="decision-form" data-verdict-form>
|
|
88
|
+
${hidden(context, verdict.sha256)} ${actorField(context, state)}
|
|
89
|
+
${rationaleField(state, 'Why do you decide this?')}
|
|
90
|
+
<div class="confirm-box" data-confirm hidden>
|
|
91
|
+
<p class="lab">Confirm</p>
|
|
92
|
+
<p data-confirm-summary></p>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="btns" data-choose>${button(verdict.approve, true)} ${button(verdict.reject, false)}</div>
|
|
95
|
+
<div class="btns" data-confirm-buttons hidden>
|
|
96
|
+
<button class="btn primary" name="confirmed" value="1" data-confirm-submit>Confirm</button>
|
|
97
|
+
<button class="btn" type="button" data-confirm-back>Back</button>
|
|
98
|
+
</div>
|
|
99
|
+
</form>`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function verdictPanel(context: Context, verdict: Verdict, state: FormState) {
|
|
103
|
+
return html`<section
|
|
104
|
+
class="panel decision"
|
|
105
|
+
id="decide"
|
|
106
|
+
aria-labelledby="decide-title"
|
|
107
|
+
data-region="decision"
|
|
108
|
+
data-stale-message="${verdict.staleMessage}"
|
|
109
|
+
>
|
|
110
|
+
<h2 class="lab" id="decide-title">Decision</h2>
|
|
111
|
+
<p class="subj">${verdict.question}</p>
|
|
112
|
+
<p class="help">
|
|
113
|
+
${verdict.digestLabel} sha256 <code title="${verdict.sha256}">${short(verdict.sha256)}</code>. Your decision
|
|
114
|
+
is recorded in the log.
|
|
115
|
+
</p>
|
|
116
|
+
<div data-stale-slot>${messages(state)}</div>
|
|
117
|
+
${verdict.intro ?? ''}
|
|
118
|
+
${state.confirm ? confirmStep(context, verdict, state) : chooseStep(context, verdict, state)}
|
|
119
|
+
</section>`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const kinds = [
|
|
123
|
+
{
|
|
124
|
+
value: 'clarify',
|
|
125
|
+
label: 'Clarify',
|
|
126
|
+
help: 'Clarify gives the expected behaviour. The agent updates the affected assertions in the next revision.',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
value: 'accept_assumption',
|
|
130
|
+
label: 'Accept assumption',
|
|
131
|
+
help: 'Accept assumption records your answer as an assumption. Tests run on it, and reports list it.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
value: 'defer',
|
|
135
|
+
label: 'Defer',
|
|
136
|
+
help: 'Defer moves the gap out of this scope. The affected tests become deferred and do not run.',
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
export type GapForm = {
|
|
141
|
+
action: string
|
|
142
|
+
cancel: string
|
|
143
|
+
sha256: string
|
|
144
|
+
title: Html
|
|
145
|
+
question: string
|
|
146
|
+
staleMessage: string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function gapPanel(context: Context, form: GapForm, state: FormState) {
|
|
150
|
+
const selected = state.values.kind ?? 'clarify'
|
|
151
|
+
return html`<section
|
|
152
|
+
class="panel decision"
|
|
153
|
+
id="answer"
|
|
154
|
+
aria-labelledby="answer-title"
|
|
155
|
+
data-region="decision"
|
|
156
|
+
data-stale-message="${form.staleMessage}"
|
|
157
|
+
>
|
|
158
|
+
<h2 class="lab" id="answer-title">Answer gap</h2>
|
|
159
|
+
<p class="subj">${form.title}</p>
|
|
160
|
+
<p class="help">${form.question}</p>
|
|
161
|
+
<div data-stale-slot>${messages(state)}</div>
|
|
162
|
+
<form method="post" action="${form.action}" class="decision-form gap-form" data-gap-form>
|
|
163
|
+
${hidden(context, form.sha256)}
|
|
164
|
+
<fieldset class="seg-field">
|
|
165
|
+
<legend class="lab">Kind of answer</legend>
|
|
166
|
+
<div class="seg">
|
|
167
|
+
${kinds.map(
|
|
168
|
+
(kind) =>
|
|
169
|
+
html`<input
|
|
170
|
+
type="radio"
|
|
171
|
+
id="kind-${kind.value}"
|
|
172
|
+
name="kind"
|
|
173
|
+
value="${kind.value}"
|
|
174
|
+
${selected === kind.value ? html`checked` : ''}
|
|
175
|
+
/><label for="kind-${kind.value}">${kind.label}</label>`
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</fieldset>
|
|
179
|
+
${kinds.map((kind) => html`<p class="help kind-help" data-kind="${kind.value}">${kind.help}</p>`)}
|
|
180
|
+
<div class="field-group">
|
|
181
|
+
<label class="lab" for="value">Answer</label>
|
|
182
|
+
<textarea id="value" name="value" rows="3" required>${state.values.value ?? ''}</textarea>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="field-group source-field">
|
|
185
|
+
<label class="lab" for="source">Source <span class="req">required for Clarify</span></label>
|
|
186
|
+
<input
|
|
187
|
+
id="source"
|
|
188
|
+
name="source"
|
|
189
|
+
value="${state.values.source ?? ''}"
|
|
190
|
+
placeholder="oracle://refund-policy#window"
|
|
191
|
+
data-source
|
|
192
|
+
/>
|
|
193
|
+
<p class="help">Where the answer comes from: a document, a ticket, or a person.</p>
|
|
194
|
+
</div>
|
|
195
|
+
${rationaleField(state, 'Why is this the right answer?')} ${actorField(context, state)}
|
|
196
|
+
<div class="btns">
|
|
197
|
+
<button class="btn primary" data-needs-rationale>Record answer</button>
|
|
198
|
+
<a class="btn" href="${form.cancel}">Cancel</a>
|
|
199
|
+
</div>
|
|
200
|
+
</form>
|
|
201
|
+
</section>`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function recordPanel(title: string, content: Html) {
|
|
205
|
+
return html`<section class="panel decision" aria-labelledby="decide-title" data-region="decision">
|
|
206
|
+
<h2 class="lab" id="decide-title">${title}</h2>
|
|
207
|
+
${content}
|
|
208
|
+
</section>`
|
|
209
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export type Html = { readonly html: string }
|
|
2
|
+
|
|
3
|
+
const entities: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
|
4
|
+
|
|
5
|
+
export function escape(text: string) {
|
|
6
|
+
return text.replace(/[&<>"']/g, (character) => entities[character])
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isHtml(value: unknown): value is Html {
|
|
10
|
+
return typeof value === 'object' && value !== null && 'html' in value
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function piece(value: unknown): string {
|
|
14
|
+
if (value === null || value === undefined || value === false) return ''
|
|
15
|
+
if (Array.isArray(value)) return value.map(piece).join('')
|
|
16
|
+
if (isHtml(value)) return value.html
|
|
17
|
+
return escape(String(value))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function html(strings: TemplateStringsArray, ...values: unknown[]): Html {
|
|
21
|
+
let text = strings[0]
|
|
22
|
+
for (let i = 0; i < values.length; i++) text += piece(values[i]) + strings[i + 1]
|
|
23
|
+
return { html: text }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function join(items: Html[], separator: Html | string) {
|
|
27
|
+
return html`${items.flatMap((item, i) => (i ? [separator, item] : [item]))}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Tone = 'pass' | 'fail' | 'block' | 'warn' | 'unsure' | 'acc' | 'muted'
|
|
31
|
+
|
|
32
|
+
const statusLook: Record<string, [string, Tone]> = {
|
|
33
|
+
passed: ['✓', 'pass'],
|
|
34
|
+
failed: ['✗', 'fail'],
|
|
35
|
+
inconclusive: ['?', 'unsure'],
|
|
36
|
+
not_checked: ['!', 'warn'],
|
|
37
|
+
blocked: ['–', 'block'],
|
|
38
|
+
not_run: ['–', 'block'],
|
|
39
|
+
error: ['!', 'warn'],
|
|
40
|
+
ready: ['✓', 'pass'],
|
|
41
|
+
deferred: ['–', 'block'],
|
|
42
|
+
planned: ['✓', 'pass'],
|
|
43
|
+
tested: ['✓', 'pass'],
|
|
44
|
+
uncovered: ['–', 'block'],
|
|
45
|
+
complete: ['✓', 'pass'],
|
|
46
|
+
partial: ['◐', 'warn'],
|
|
47
|
+
not_executed: ['–', 'block'],
|
|
48
|
+
completed: ['✓', 'pass'],
|
|
49
|
+
interrupted: ['!', 'warn'],
|
|
50
|
+
operational_error: ['!', 'fail'],
|
|
51
|
+
not_applicable: ['–', 'block'],
|
|
52
|
+
approved: ['✓', 'pass'],
|
|
53
|
+
rejected: ['✗', 'fail'],
|
|
54
|
+
pending: ['○', 'acc'],
|
|
55
|
+
not_required: ['–', 'block'],
|
|
56
|
+
started: ['●', 'acc'],
|
|
57
|
+
finalized: ['✓', 'pass'],
|
|
58
|
+
open: ['○', 'warn'],
|
|
59
|
+
resolved: ['✓', 'pass'],
|
|
60
|
+
blocking: ['●', 'fail'],
|
|
61
|
+
non_blocking: ['○', 'warn'],
|
|
62
|
+
clarify: ['✓', 'acc'],
|
|
63
|
+
accept_assumption: ['≈', 'acc'],
|
|
64
|
+
defer: ['–', 'block'],
|
|
65
|
+
defect: ['✗', 'fail'],
|
|
66
|
+
potential_issue: ['?', 'warn'],
|
|
67
|
+
low: ['·', 'block'],
|
|
68
|
+
medium: ['!', 'warn'],
|
|
69
|
+
high: ['!', 'fail'],
|
|
70
|
+
critical: ['!!', 'fail'],
|
|
71
|
+
rejected_operation: ['✗', 'fail'],
|
|
72
|
+
needs_decision: ['?', 'warn'],
|
|
73
|
+
invalid_request: ['✗', 'fail'],
|
|
74
|
+
needs_plan: ['○', 'acc'],
|
|
75
|
+
run_in_progress: ['●', 'acc'],
|
|
76
|
+
needs_revision: ['○', 'acc'],
|
|
77
|
+
plan_rejected: ['✗', 'fail'],
|
|
78
|
+
needs_answers: ['?', 'warn'],
|
|
79
|
+
needs_approval: ['○', 'acc'],
|
|
80
|
+
ready_to_run: ['○', 'acc'],
|
|
81
|
+
nothing_to_run: ['–', 'block'],
|
|
82
|
+
defects_open: ['✗', 'fail'],
|
|
83
|
+
needs_retest: ['!', 'warn'],
|
|
84
|
+
awaits_acceptance: ['○', 'acc'],
|
|
85
|
+
accepted: ['✓', 'pass'],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function words(status: string) {
|
|
89
|
+
return status.replace(/_/g, ' ')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function pill(status: string, label: string = words(status)) {
|
|
93
|
+
const [icon, tone] = statusLook[status] ?? ['·', 'muted']
|
|
94
|
+
return html`<span class="st ${tone}"><span aria-hidden="true">${icon}</span> ${label}</span>`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function icon(status: string | null) {
|
|
98
|
+
const [symbol, tone] = status ? (statusLook[status] ?? ['·', 'muted']) : ['·', 'muted']
|
|
99
|
+
return html`<span class="ic ${tone}" role="img" aria-label="${status ? words(status) : 'no result yet'}"
|
|
100
|
+
>${symbol}</span
|
|
101
|
+
>`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function short(sha256: string) {
|
|
105
|
+
return `${sha256.slice(0, 4)}…${sha256.slice(-4)}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function age(iso: string, now = Date.now()) {
|
|
109
|
+
const seconds = Math.max(0, Math.round((now - Date.parse(iso)) / 1000))
|
|
110
|
+
if (seconds < 45) return 'now'
|
|
111
|
+
const minutes = Math.round(seconds / 60)
|
|
112
|
+
if (minutes < 60) return `${minutes} min`
|
|
113
|
+
const hours = Math.round(minutes / 60)
|
|
114
|
+
if (hours < 48) return `${hours} h`
|
|
115
|
+
return `${Math.round(hours / 24)} d`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function clock(iso: string) {
|
|
119
|
+
const date = new Date(iso)
|
|
120
|
+
const pad = (value: number) => String(value).padStart(2, '0')
|
|
121
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function time(iso: string, text = age(iso)) {
|
|
125
|
+
return html`<time datetime="${iso}" title="${clock(iso)}">${text}</time>`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function plural(count: number, one: string, many = `${one}s`) {
|
|
129
|
+
return `${count} ${count === 1 ? one : many}`
|
|
130
|
+
}
|