@rsaglobaltech/specgate 0.23.0 → 0.25.0
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/CHANGELOG.md +85 -0
- package/dist/packages/core/src/domain/Clarification.js +173 -0
- package/dist/packages/core/src/domain/Clarification.js.map +1 -0
- package/dist/packages/core/src/domain/DoneVerification.js +21 -2
- package/dist/packages/core/src/domain/DoneVerification.js.map +1 -1
- package/dist/packages/core/src/domain/DraftChecklist.js +16 -16
- package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessRun.js.map +1 -1
- package/dist/packages/core/test/unit/clarification.test.js +161 -0
- package/dist/packages/core/test/unit/clarification.test.js.map +1 -0
- package/dist/packages/core/test/unit/done-verification.test.js +18 -0
- package/dist/packages/core/test/unit/done-verification.test.js.map +1 -1
- package/dist/scripts/answer.js +10 -0
- package/dist/scripts/answer.js.map +1 -0
- package/dist/scripts/ask.js +10 -0
- package/dist/scripts/ask.js.map +1 -0
- package/dist/scripts/brief_add.js +10 -0
- package/dist/scripts/brief_add.js.map +1 -0
- package/dist/scripts/cli/commands/change/ChangeCommand.js +87 -1
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +128 -8
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +6 -0
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ClarifyCommands.js +275 -0
- package/dist/scripts/cli/commands/spec/ClarifyCommands.js.map +1 -0
- package/dist/scripts/cli/commands/spec/DoneCommand.js +2 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StudioCommand.js +31 -0
- package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
- package/dist/scripts/harness/events.js +57 -0
- package/dist/scripts/harness/events.js.map +1 -1
- package/dist/scripts/harness/prompt.js +2 -0
- package/dist/scripts/harness/prompt.js.map +1 -1
- package/dist/scripts/lib/brief-add.js +197 -0
- package/dist/scripts/lib/brief-add.js.map +1 -0
- package/dist/scripts/lib/change-impact.js +156 -0
- package/dist/scripts/lib/change-impact.js.map +1 -0
- package/dist/scripts/lib/clarification.js +300 -0
- package/dist/scripts/lib/clarification.js.map +1 -0
- package/dist/scripts/lib/harness-events.js +32 -1
- package/dist/scripts/lib/harness-events.js.map +1 -1
- package/dist/scripts/lib/harness-guard.js +186 -0
- package/dist/scripts/lib/harness-guard.js.map +1 -0
- package/dist/scripts/lib/in-flight.js +17 -4
- package/dist/scripts/lib/in-flight.js.map +1 -1
- package/dist/scripts/lib/live-detail.js +346 -0
- package/dist/scripts/lib/live-detail.js.map +1 -0
- package/dist/scripts/lib/live-model.js +125 -9
- package/dist/scripts/lib/live-model.js.map +1 -1
- package/dist/scripts/lib/live-page.js +465 -119
- package/dist/scripts/lib/live-page.js.map +1 -1
- package/dist/scripts/lib/surface.js +52 -0
- package/dist/scripts/lib/surface.js.map +1 -1
- package/dist/scripts/questions.js +10 -0
- package/dist/scripts/questions.js.map +1 -0
- package/package.json +1 -1
|
@@ -6,12 +6,21 @@ exports.livePage = livePage;
|
|
|
6
6
|
* resources, filled from `/live.json` and kept current by `/live/stream`.
|
|
7
7
|
* Every value from an event is set with textContent, never as HTML.
|
|
8
8
|
*
|
|
9
|
-
* REQ-179: laid out the way CI runs are read
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* REQ-179: laid out the way CI runs are read — a sidebar of requirements by
|
|
10
|
+
* module, the run summed up, its requirements as a graph of jobs, and the
|
|
11
|
+
* selected one's steps with the agent's stream as the log of the step in
|
|
12
|
+
* progress.
|
|
13
|
+
*
|
|
14
|
+
* REQ-182: the sidebar is the run's work queue (now, needs attention, up next,
|
|
15
|
+
* finished recently) with the agent's model, context and tokens; an Agent tab
|
|
16
|
+
* shows the selected attempt's session and the prompt it was given.
|
|
17
|
+
*
|
|
18
|
+
* REQ-181: the header stays in view while the page scrolls; modules fold, in
|
|
19
|
+
* the sidebar as an accordion the viewer opens and closes, and in the graph
|
|
20
|
+
* once all their requirements have passed; the selected requirement opens into
|
|
21
|
+
* tabs (Steps, Requirement, Tests and files, Attempts) read from
|
|
22
|
+
* `/live/req/<id>.json`; and a second view shows the traceability matrix as it
|
|
23
|
+
* stands, from `/live/matrix.json`.
|
|
15
24
|
*
|
|
16
25
|
* The script lives inside this template literal: it uses no backticks, no
|
|
17
26
|
* dollar-brace, and no backslash escapes.
|
|
@@ -25,192 +34,371 @@ function livePage(projectName) {
|
|
|
25
34
|
<style>
|
|
26
35
|
:root{--bg:#f6f8fa;--panel:#ffffff;--line:#d1d9e0;--soft:#eff2f5;--ink:#1f2328;--muted:#59636e;--faint:#8c959f;
|
|
27
36
|
--pass:#1a7f37;--fail:#cf222e;--run:#9a6700;--run-bg:#fff8c5;--accent:#0969da;--sel:#ddf4ff;
|
|
28
|
-
--log:#1f2328;--log-ink:#d1d9e0;--log-muted:#8c959f;
|
|
37
|
+
--log:#1f2328;--log-ink:#d1d9e0;--log-muted:#8c959f;--top:56px;
|
|
29
38
|
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
|
|
30
39
|
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;color-scheme:light}
|
|
31
40
|
@media (prefers-color-scheme: dark){:root{--bg:#0d1117;--panel:#151b23;--line:#3d444d;--soft:#212830;--ink:#f0f6fc;
|
|
32
41
|
--muted:#9198a1;--faint:#656c76;--pass:#3fb950;--fail:#f85149;--run:#d29922;--run-bg:#2c2513;--accent:#4493f8;
|
|
33
42
|
--sel:#16263d;--log:#010409;--log-ink:#d1d7e0;--log-muted:#656c76;color-scheme:dark}}
|
|
34
43
|
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 var(--sans)}
|
|
35
|
-
button{font:inherit;color:inherit}
|
|
36
|
-
.top{height:
|
|
37
|
-
|
|
44
|
+
button{font:inherit;color:inherit} a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline}
|
|
45
|
+
.top{position:sticky;top:0;z-index:20;height:var(--top);display:flex;align-items:center;gap:10px;padding:0 24px;
|
|
46
|
+
background:var(--panel);border-bottom:1px solid var(--line)}
|
|
47
|
+
.top svg{width:22px;height:22px;flex:none} .sep{color:var(--faint)}
|
|
48
|
+
.views{display:flex;gap:4px;margin-left:24px}
|
|
49
|
+
.views button{border:1px solid transparent;background:none;border-radius:6px;padding:4px 12px;cursor:pointer;color:var(--muted)}
|
|
50
|
+
.views button[aria-pressed="true"]{border-color:var(--line);background:var(--soft);color:var(--ink);font-weight:600}
|
|
38
51
|
.pill{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
|
39
52
|
.pill i{width:8px;height:8px;border-radius:50%;background:var(--faint)} .pill.on i{background:var(--pass)}
|
|
40
|
-
.layout{display:flex;
|
|
41
|
-
aside{width:300px;flex:none;
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
.layout{display:flex;align-items:flex-start}
|
|
54
|
+
aside{position:sticky;top:var(--top);width:300px;flex:none;height:calc(100vh - var(--top));overflow:auto;
|
|
55
|
+
background:var(--panel);border-right:1px solid var(--line);padding:12px}
|
|
56
|
+
aside details{border-radius:6px} aside summary{list-style:none;display:flex;align-items:center;gap:8px;padding:7px 10px;
|
|
57
|
+
border-radius:6px;cursor:pointer;font-weight:600;font-size:13px}
|
|
58
|
+
aside summary::-webkit-details-marker{display:none} aside summary:hover{background:var(--soft)}
|
|
59
|
+
aside summary .chev{width:12px;height:12px;transition:transform .15s} aside details[open]>summary .chev{transform:rotate(90deg)}
|
|
60
|
+
aside summary .count{margin-left:auto;font-weight:400;color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
|
|
61
|
+
.job{display:flex;width:100%;align-items:center;gap:8px;padding:5px 10px 5px 30px;border:0;border-radius:6px;background:none;text-align:left;cursor:pointer;font-size:13px}
|
|
44
62
|
.job:hover{background:var(--soft)} .job.sel{background:var(--sel);font-weight:600}
|
|
45
|
-
.job .name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
63
|
+
.job .name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .job .name>span{display:block;overflow:hidden;text-overflow:ellipsis}
|
|
46
64
|
.job .d{font-size:12px;color:var(--muted);font-weight:400;font-variant-numeric:tabular-nums}
|
|
47
|
-
.summary-btn{margin-bottom:
|
|
48
|
-
|
|
65
|
+
.summary-btn{padding-left:10px;margin-bottom:6px;font-weight:600}
|
|
66
|
+
.qsec{margin:10px 0 14px} .qsec h2{display:flex;align-items:center;gap:8px;margin:0 0 4px;padding:0 10px;font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
|
|
67
|
+
.qsec h2 .count{margin-left:auto;font-weight:400;font-variant-numeric:tabular-nums} .qsec .job{padding-left:10px;align-items:flex-start}
|
|
68
|
+
.qsec .job .ic{margin-top:3px} .job small{display:block;font-size:11.5px;color:var(--muted);font-weight:400}
|
|
69
|
+
.qsec p{margin:2px 10px 6px;font-size:12.5px} .muted{color:var(--muted)}
|
|
70
|
+
.now{border:1px solid var(--run);border-radius:6px;padding:4px 0 8px;background:var(--panel)}
|
|
71
|
+
.kv{display:grid;grid-template-columns:62px minmax(0,1fr);gap:4px 10px;margin:4px 10px 0;font-size:12.5px} .kv dt{color:var(--muted)} .kv dd{margin:0;min-width:0;overflow-wrap:anywhere}
|
|
72
|
+
.ctx{display:flex;flex-direction:column;gap:3px} .ctx .d{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
|
73
|
+
details.prompt{margin-top:10px} details.prompt summary{cursor:pointer;color:var(--accent);font-size:13px}
|
|
74
|
+
details.prompt pre{max-height:420px;overflow:auto;background:var(--log);color:var(--log-ink);padding:12px;border-radius:6px;font:12px/1.55 var(--mono);white-space:pre-wrap}
|
|
75
|
+
main{flex:1;min-width:0;padding:0 28px 28px}
|
|
76
|
+
.stick{position:sticky;top:var(--top);z-index:10;background:var(--bg);padding:20px 0 12px;display:flex;flex-direction:column;gap:12px}
|
|
49
77
|
.head{display:flex;align-items:center;gap:12px;flex-wrap:wrap} .head h1{margin:0;font-size:20px;font-weight:600}
|
|
50
78
|
.badge{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
|
|
51
79
|
.badge.running{background:var(--run-bg);color:var(--run)} .badge.pass{background:rgba(31,136,61,.12);color:var(--pass)}
|
|
52
80
|
.badge.fail{background:rgba(207,34,46,.12);color:var(--fail)} .badge i{width:8px;height:8px;border-radius:50%;background:currentColor}
|
|
53
81
|
.box{background:var(--panel);border:1px solid var(--line);border-radius:6px}
|
|
54
|
-
#progress{display:flex;flex-wrap:wrap;gap:
|
|
82
|
+
#progress{display:flex;flex-wrap:wrap;gap:8px 40px;padding:12px 18px}
|
|
55
83
|
#progress div span{display:block;font-size:12px;color:var(--muted)} #progress div b{font-weight:400;font-variant-numeric:tabular-nums}
|
|
56
|
-
.
|
|
84
|
+
.bar{display:flex;height:6px;border-radius:999px;overflow:hidden;background:var(--soft)} .bar i{display:block}
|
|
85
|
+
.gap{display:flex;flex-direction:column;gap:16px}
|
|
86
|
+
.mm-row{display:grid;grid-template-columns:28px 150px minmax(0,1fr) 70px 90px;gap:12px;align-items:center;padding:11px 16px;border-bottom:1px solid var(--soft)}
|
|
87
|
+
.mm-row:last-of-type{border-bottom:0} .mm-head{font-size:12px;font-weight:600;color:var(--muted);border-bottom-color:var(--line)}
|
|
88
|
+
.mm-row b{font-weight:600} .mm-row .num{text-align:right;font-variant-numeric:tabular-nums} .mm-row .muted{color:var(--muted)}
|
|
89
|
+
.mm-row.has-fail{background:rgba(207,34,46,.05)} .mm-row.has-run{background:var(--run-bg)} .mm-row.queued{color:var(--muted)}
|
|
90
|
+
.mm-bar{display:flex;gap:3px}
|
|
91
|
+
.seg{flex:1;min-width:6px;height:16px;border:0;border-radius:3px;padding:0;cursor:pointer;background:var(--soft)}
|
|
92
|
+
.seg.pass{background:var(--pass)} .seg.fail{background:var(--fail)} .seg.queued{background:var(--soft);box-shadow:inset 0 0 0 1px var(--line)}
|
|
93
|
+
.seg.running{background:repeating-linear-gradient(135deg,#bf8700 0 5px,#d4a72c 5px 10px)}
|
|
94
|
+
.seg.blocked{background:none;box-shadow:inset 0 0 0 1.5px var(--faint)} .seg.question{background:var(--run)}
|
|
95
|
+
.seg.on{outline:2px solid var(--accent);outline-offset:2px} .seg:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
|
|
96
|
+
.mm-legend{display:flex;gap:16px;justify-content:flex-end;padding:10px 16px;font-size:12px;color:var(--muted);border-top:1px solid var(--line)}
|
|
97
|
+
.mm-legend span{display:inline-flex;align-items:center;gap:6px} .mm-legend .seg{width:14px;height:10px;flex:none;cursor:default}
|
|
57
98
|
#focus{display:flex;flex-direction:column;overflow:hidden}
|
|
58
|
-
#focus .fh{padding:12px 16px
|
|
99
|
+
#focus .fh{padding:12px 16px 0;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
|
|
59
100
|
#focus .fh b{font-weight:600} #focus .fh span{font-size:12px;color:var(--muted)}
|
|
101
|
+
.tabs{display:flex;gap:2px;padding:8px 12px 0;border-bottom:1px solid var(--line);overflow-x:auto}
|
|
102
|
+
.tabs button{border:0;background:none;padding:8px 12px;cursor:pointer;color:var(--muted);border-bottom:2px solid transparent;white-space:nowrap}
|
|
103
|
+
.tabs button[aria-selected="true"]{color:var(--ink);font-weight:600;border-bottom-color:#fd8c73}
|
|
104
|
+
.pane{padding:0}
|
|
60
105
|
#pipeline{list-style:none;margin:0;padding:0}
|
|
61
106
|
#pipeline>li{display:flex;align-items:center;gap:10px;padding:8px 16px;border-bottom:1px solid var(--soft);font-size:13px}
|
|
62
|
-
#pipeline
|
|
63
|
-
#pipeline .
|
|
64
|
-
#pipeline li.running{background:var(--run-bg)} #pipeline li.running .d{color:var(--run)}
|
|
65
|
-
#pipeline li.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
#pipeline .t{flex:1;min-width:0;overflow-wrap:anywhere} #pipeline .mono{font-family:var(--mono);font-size:12.5px}
|
|
108
|
+
#pipeline .d{color:var(--muted);font-variant-numeric:tabular-nums}
|
|
109
|
+
#pipeline li.running{background:var(--run-bg)} #pipeline li.running .d{color:var(--run)} #pipeline li.idle{color:var(--faint)}
|
|
110
|
+
#pipeline li.logrow{display:block;padding:0;background:var(--log)}
|
|
111
|
+
.feed{list-style:none;margin:0;padding:10px 16px 10px 42px;max-height:340px;overflow:auto;color:var(--log-ink);font:12px/1.65 var(--mono)}
|
|
112
|
+
.feed li{display:grid;grid-template-columns:64px 56px minmax(0,1fr);gap:10px}
|
|
113
|
+
.feed .ts,.feed .k{color:var(--log-muted)} .feed .ok{color:#3fb950} .feed .bad{color:#f85149}
|
|
114
|
+
.feed .say{font-family:var(--sans);font-size:12.5px} .feed .x{overflow-wrap:anywhere}
|
|
115
|
+
.sect{padding:14px 16px;border-bottom:1px solid var(--soft)} .sect:last-child{border-bottom:0}
|
|
116
|
+
.sect h3{margin:0 0 8px;font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
|
|
117
|
+
.stmt{font-size:14.5px;line-height:1.55} .meta{display:flex;flex-wrap:wrap;gap:6px 18px;font-size:12.5px;color:var(--muted)}
|
|
118
|
+
.scn{border:1px solid var(--line);border-radius:6px;padding:10px 12px;margin-bottom:8px}
|
|
119
|
+
.scn b{font-weight:600} .scn ol{margin:6px 0 0;padding-left:18px;font-size:13px}
|
|
120
|
+
.scn .kw{font-weight:600;color:var(--accent)}
|
|
121
|
+
.list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px;font:12.5px var(--mono)}
|
|
122
|
+
.list li{display:flex;gap:10px;align-items:baseline} .add{color:var(--pass)} .rem{color:var(--fail)}
|
|
123
|
+
table{width:100%;border-collapse:collapse;font-size:12.5px}
|
|
124
|
+
th{text-align:left;font-weight:600;color:var(--muted);padding:7px 10px;border-bottom:1px solid var(--line);white-space:nowrap}
|
|
125
|
+
td{padding:7px 10px;border-bottom:1px solid var(--soft);vertical-align:top}
|
|
126
|
+
td.mono{font-family:var(--mono);white-space:nowrap} td.tests{font-family:var(--mono);font-size:11.5px;overflow-wrap:anywhere}
|
|
127
|
+
.st{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
|
|
128
|
+
.st.Implemented,.st.Verified,.st.Released{color:var(--pass)} .st.Draft{color:var(--muted)} .st.running{color:var(--run)}
|
|
129
|
+
.mtools{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
|
|
130
|
+
.mtools input{height:32px;padding:0 10px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);min-width:260px}
|
|
131
|
+
.msum{font-size:12.5px;color:var(--muted)}
|
|
132
|
+
.mgroup>summary{list-style:none;display:flex;gap:10px;align-items:center;padding:10px 14px;cursor:pointer;font-weight:600}
|
|
133
|
+
.mgroup>summary::-webkit-details-marker{display:none} .mgroup>summary .count{font-weight:400;color:var(--muted);font-size:12.5px}
|
|
70
134
|
.ic{flex:none;width:16px;height:16px;display:inline-block}
|
|
71
|
-
.spin{animation:spin 1s linear infinite;transform-origin:50% 50%}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@media (max-width:900px){.layout{flex-direction:column} aside{width:auto;border-right:0;border-bottom:1px solid var(--line)
|
|
75
|
-
@media (prefers-reduced-motion: reduce){*{animation:none!important}}
|
|
135
|
+
.spin{animation:spin 1s linear infinite;transform-origin:50% 50%} @keyframes spin{to{transform:rotate(360deg)}}
|
|
136
|
+
.empty{padding:40px 16px;text-align:center;color:var(--muted)}
|
|
137
|
+
[hidden]{display:none!important}
|
|
138
|
+
@media (max-width:900px){.layout{flex-direction:column} aside{position:static;width:auto;height:auto;max-height:300px;border-right:0;border-bottom:1px solid var(--line)} main{padding:0 14px 20px}}
|
|
139
|
+
@media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}}
|
|
76
140
|
</style></head>
|
|
77
141
|
<body>
|
|
78
142
|
<div class="top">
|
|
79
143
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></svg>
|
|
80
|
-
<
|
|
144
|
+
<a href="#">${title}</a><span class="sep">/</span><b>Harness live</b>
|
|
145
|
+
<div class="views" role="group" aria-label="View">
|
|
146
|
+
<button type="button" id="vRun" aria-pressed="true">Run</button>
|
|
147
|
+
<button type="button" id="vMatrix" aria-pressed="false">Traceability</button>
|
|
148
|
+
</div>
|
|
81
149
|
<span id="conn" class="pill"><i></i><span id="connText">connecting</span></span>
|
|
82
150
|
</div>
|
|
83
151
|
<div class="layout">
|
|
84
|
-
<aside aria-label="
|
|
152
|
+
<aside aria-label="Work queue"><div id="jobs"></div></aside>
|
|
85
153
|
<main>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
154
|
+
<section id="runView">
|
|
155
|
+
<div class="stick">
|
|
156
|
+
<div class="head"><span id="runBadge" class="badge running"><i></i><span id="runState">Waiting</span></span><h1 id="runTitle">Harness run</h1></div>
|
|
157
|
+
<div id="progress" class="box" role="status"></div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="gap">
|
|
160
|
+
<div class="box" id="chain" role="group" aria-label="The run by module: one segment per requirement"></div>
|
|
161
|
+
<section id="focus" class="box" aria-live="polite">
|
|
162
|
+
<div class="fh"><b id="fName">—</b><span id="fMeta"></span></div>
|
|
163
|
+
<div class="tabs" role="tablist" aria-label="About this requirement">
|
|
164
|
+
<button type="button" role="tab" data-tab="steps" aria-selected="true">Steps</button>
|
|
165
|
+
<button type="button" role="tab" data-tab="req" aria-selected="false">Requirement</button>
|
|
166
|
+
<button type="button" role="tab" data-tab="files" aria-selected="false">Tests and files</button>
|
|
167
|
+
<button type="button" role="tab" data-tab="attempts" aria-selected="false">Attempts</button>
|
|
168
|
+
<button type="button" role="tab" data-tab="agent" aria-selected="false">Agent</button>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="pane" data-pane="steps"><ol id="pipeline" aria-label="Steps: Agent, Specification, Architecture, Tests, Merge"></ol></div>
|
|
171
|
+
<div class="pane" data-pane="req" hidden></div>
|
|
172
|
+
<div class="pane" data-pane="files" hidden></div>
|
|
173
|
+
<div class="pane" data-pane="attempts" hidden></div>
|
|
174
|
+
<div class="pane" data-pane="agent" hidden></div>
|
|
175
|
+
</section>
|
|
176
|
+
</div>
|
|
177
|
+
</section>
|
|
178
|
+
<section id="matrixView" hidden>
|
|
179
|
+
<div class="stick">
|
|
180
|
+
<div class="head"><h1>Traceability matrix</h1><span class="msum" id="mSum"></span></div>
|
|
181
|
+
<div class="mtools"><input id="mFilter" type="search" placeholder="Filter by REQ, SCN, title, test or status" aria-label="Filter the matrix"/></div>
|
|
182
|
+
</div>
|
|
183
|
+
<div id="matrix" class="gap"></div>
|
|
92
184
|
</section>
|
|
93
185
|
</main>
|
|
94
186
|
</div>
|
|
95
187
|
<script>
|
|
96
188
|
(function(){
|
|
97
189
|
var NS = "http:" + "//www.w3.org/2000/svg";
|
|
98
|
-
var model = {requirements:[]}, pinned = null,
|
|
190
|
+
var model = {requirements:[]}, projectDir = "", pinned = null, tab = "steps", stepsKey = "";
|
|
191
|
+
var expanded = {}, details = {}, matrix = [], matrixDirty = true, view = "run";
|
|
192
|
+
var open = load("sg-live-open") || {};
|
|
193
|
+
function load(k){ try { return JSON.parse(localStorage.getItem(k) || "null"); } catch (e) { return null; } }
|
|
194
|
+
function save(k, v){ try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }
|
|
99
195
|
function $(id){ return document.getElementById(id); }
|
|
100
196
|
function el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; }
|
|
101
197
|
function s(tag, attrs, parent){ var e = document.createElementNS(NS, tag); for (var k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; }
|
|
102
198
|
function now(){ return Date.now() / 1000; }
|
|
199
|
+
function two(n){ return (n < 10 ? "0" : "") + n; }
|
|
103
200
|
function dur(sec){ sec = Math.max(0, Math.round(sec)); var h = Math.floor(sec / 3600), m = Math.floor(sec % 3600 / 60), x = sec % 60;
|
|
104
201
|
return h ? h + "h " + m + "m" : m ? m + "m " + x + "s" : x + "s"; }
|
|
105
|
-
function two(n){ return (n < 10 ? "0" : "") + n; }
|
|
106
202
|
function stamp(t){ if (!t) return ""; var d = new Date(t * 1000); return two(d.getHours()) + ":" + two(d.getMinutes()) + ":" + two(d.getSeconds()); }
|
|
107
203
|
function elapsed(r){ return r.state === "running" ? Math.max(r.elapsed, now() - r.started) : r.elapsed; }
|
|
108
204
|
function clip(t, n){ t = String(t || ""); return t.length > n ? t.slice(0, n - 1) + "…" : t; }
|
|
109
|
-
|
|
110
|
-
/** A
|
|
205
|
+
function cap(g){ return g ? g.charAt(0).toUpperCase() + g.slice(1) : "Requirements"; }
|
|
206
|
+
/** A link that opens the file in VS Code, at its line when known. */
|
|
207
|
+
function fileLink(rel, line){
|
|
208
|
+
var a = el("a", "", rel + (line ? ":" + line : ""));
|
|
209
|
+
if (projectDir) a.setAttribute("href", "vscode://file/" + (projectDir.split(String.fromCharCode(92)).join("/") + "/" + rel) + (line ? ":" + line : ""));
|
|
210
|
+
a.title = "Open in VS Code"; return a;
|
|
211
|
+
}
|
|
111
212
|
function icon(state, size){
|
|
112
213
|
var v = s("svg", {viewBox:"0 0 16 16", width:size || 16, height:size || 16, "class":"ic", "aria-hidden":"true"});
|
|
113
214
|
if (state === "pass") { s("circle", {cx:8, cy:8, r:7, fill:"var(--pass)"}, v); s("path", {d:"M4.5 8.2l2.3 2.3 4.7-4.9", fill:"none", stroke:"#fff", "stroke-width":"1.8", "stroke-linecap":"round", "stroke-linejoin":"round"}, v); }
|
|
114
215
|
else if (state === "fail") { s("circle", {cx:8, cy:8, r:7, fill:"var(--fail)"}, v); s("path", {d:"M5.5 5.5l5 5M10.5 5.5l-5 5", stroke:"#fff", "stroke-width":"1.8", "stroke-linecap":"round"}, v); }
|
|
115
216
|
else if (state === "running") { s("circle", {cx:8, cy:8, r:6, fill:"none", stroke:"var(--run)", "stroke-width":"2", opacity:".25"}, v);
|
|
116
217
|
s("path", {d:"M8 2a6 6 0 0 1 6 6", fill:"none", stroke:"var(--run)", "stroke-width":"2", "stroke-linecap":"round", "class":"spin"}, v); }
|
|
218
|
+
else if (state === "question") { s("circle", {cx:8, cy:8, r:7, fill:"var(--run)"}, v); var tq = s("text", {x:8, y:11.5, "text-anchor":"middle", "font-size":"10", "font-weight":"700", fill:"#fff"}, v); tq.textContent = "?"; }
|
|
117
219
|
else s("circle", {cx:8, cy:8, r:6.25, fill:"none", stroke:"var(--faint)", "stroke-width":"1.5"}, v);
|
|
118
220
|
return v;
|
|
119
221
|
}
|
|
120
|
-
function selected(
|
|
121
|
-
if (pinned) { var p =
|
|
122
|
-
|
|
222
|
+
function selected(list){
|
|
223
|
+
if (pinned) { var p = list.filter(function(r){ return r.req === pinned; })[0]; if (p) return p;
|
|
224
|
+
var m = matrix.filter(function(x){ return x.requirement === pinned; })[0];
|
|
225
|
+
if (m) return {req:m.requirement, title:m.title, group:m.group, state:"queued", attempt:0, steps:[], started:now(), updated:now(), elapsed:0, agent:null, usage:null, prompt:null}; }
|
|
226
|
+
return list.filter(function(r){ return r.state === "running"; })[0] || list[0] || null;
|
|
123
227
|
}
|
|
124
228
|
function ordered(reqs){ return reqs.slice().sort(function(a, b){ return a.started - b.started; }); }
|
|
125
229
|
function groups(list){
|
|
126
230
|
var out = [], at = {};
|
|
127
231
|
list.forEach(function(r){ var g = r.group || ""; if (!(g in at)) { at[g] = out.length; out.push({name:g, reqs:[]}); } out[at[g]].reqs.push(r); });
|
|
232
|
+
out.forEach(function(g){ g.pass = g.reqs.filter(function(r){ return r.state === "pass"; }).length;
|
|
233
|
+
g.fail = g.reqs.filter(function(r){ return r.state === "fail"; }).length; g.done = g.pass === g.reqs.length;
|
|
234
|
+
g.time = g.reqs.reduce(function(a, r){ return a + elapsed(r); }, 0); });
|
|
128
235
|
return out;
|
|
129
236
|
}
|
|
130
|
-
function pick(id){ pinned = pinned === id ? null : id;
|
|
237
|
+
function pick(id){ pinned = pinned === id ? null : id; stepsKey = ""; draw(); }
|
|
238
|
+
|
|
239
|
+
function kt(n){ n = Number(n) || 0; return n >= 1e6 ? (n / 1e6).toFixed(2) + "M" : n >= 1e4 ? Math.round(n / 1e3) + "k" : n >= 1e3 ? (n / 1e3).toFixed(1) + "k" : String(n); }
|
|
240
|
+
function usd(n){ return n === null || n === undefined ? "—" : "$" + Number(n).toFixed(2); }
|
|
241
|
+
function shortModel(m){ return String(m || "").replace(/^claude-/, ""); }
|
|
242
|
+
/** How full the context is, as a bar: green, amber past 60%, red past 85%. */
|
|
243
|
+
function ctxBar(u){
|
|
244
|
+
var w = el("div", "ctx"), pct = u && u.window ? Math.min(100, u.context / u.window * 100) : 0;
|
|
245
|
+
var bar = el("div", "bar"), i = el("i"); i.style.width = pct + "%";
|
|
246
|
+
i.style.background = pct > 85 ? "var(--fail)" : pct > 60 ? "var(--run)" : "var(--pass)"; bar.appendChild(i);
|
|
247
|
+
w.append(bar, el("span", "d", u && u.window ? kt(u.context) + " of " + kt(u.window) + " · " + Math.round(pct) + "%" : "—"));
|
|
248
|
+
return w;
|
|
249
|
+
}
|
|
250
|
+
function jobBtn(r, sel, note, state){
|
|
251
|
+
var b = el("button", "job" + (sel && sel.req === r.req ? " sel" : "")); b.type = "button";
|
|
252
|
+
b.setAttribute("aria-current", sel && sel.req === r.req ? "true" : "false");
|
|
253
|
+
var txt = el("span", "name"); txt.append(el("span", "", r.req + " " + (r.title || "")));
|
|
254
|
+
if (note) txt.appendChild(el("small", "", note));
|
|
255
|
+
b.append(icon(state || r.state), txt);
|
|
256
|
+
b.addEventListener("click", function(){ pick(r.req); });
|
|
257
|
+
return b;
|
|
258
|
+
}
|
|
259
|
+
function block(title, count){
|
|
260
|
+
var d = el("section", "qsec"), h = el("h2"); h.append(el("span", "", title));
|
|
261
|
+
if (count !== undefined) h.appendChild(el("span", "count", String(count)));
|
|
262
|
+
d.appendChild(h); return d;
|
|
263
|
+
}
|
|
131
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The sidebar is the run's work queue (REQ-182), not a second map of it: what
|
|
267
|
+
* runs now and with which model and context, what needs a person, what comes
|
|
268
|
+
* next, and what finished last. The map is the matrix in the centre.
|
|
269
|
+
*/
|
|
132
270
|
function sidebar(list, sel){
|
|
133
|
-
var box = $("jobs"); box.replaceChildren();
|
|
271
|
+
var box = $("jobs"), keepScroll = box.parentNode.scrollTop; box.replaceChildren();
|
|
134
272
|
var sum = el("button", "job summary-btn" + (pinned ? "" : " sel"), "Summary"); sum.type = "button";
|
|
135
|
-
sum.addEventListener("click", function(){ pinned = null;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
273
|
+
sum.addEventListener("click", function(){ pinned = null; stepsKey = ""; draw(); }); box.appendChild(sum);
|
|
274
|
+
var since = model.queueStarted || 0, byReq = {}, titles = {};
|
|
275
|
+
list.forEach(function(r){ byReq[r.req] = r; });
|
|
276
|
+
matrix.forEach(function(m){ titles[m.requirement] = m.title; });
|
|
277
|
+
|
|
278
|
+
var cur = list.filter(function(r){ return r.state === "running"; })[0];
|
|
279
|
+
var now0 = block("Now");
|
|
280
|
+
if (cur) {
|
|
281
|
+
var card = el("div", "now"); card.appendChild(jobBtn(cur, sel, "attempt " + cur.attempt + " · " + cur.phase + " · " + dur(elapsed(cur))));
|
|
282
|
+
var kv = el("dl", "kv");
|
|
283
|
+
var add = function(k, v){ var dd = el("dd"); if (typeof v === "string") dd.textContent = v; else dd.appendChild(v); kv.append(el("dt", "", k), dd); };
|
|
284
|
+
add("Model", cur.agent ? shortModel(cur.agent.model) : "not reported");
|
|
285
|
+
add("Context", ctxBar(cur.usage));
|
|
286
|
+
add("Output", cur.usage ? kt(cur.usage.output) + " tokens" : "—");
|
|
287
|
+
if (cur.prompt) add("Prompt", "~" + kt(cur.prompt.tokens) + " tokens · " + cur.prompt.lines + " lines");
|
|
288
|
+
card.appendChild(kv); now0.appendChild(card);
|
|
289
|
+
} else now0.appendChild(el("p", "muted", "Nothing running."));
|
|
290
|
+
box.appendChild(now0);
|
|
291
|
+
|
|
292
|
+
// What the agent asked waits for a person first (REQ-189).
|
|
293
|
+
var need = list.filter(function(r){ return r.state === "question" && r.question; }).map(function(r){
|
|
294
|
+
return {r:r, note:"asks: " + clip(r.question.text, 90) + " · " + r.question.answer, st:"question"}; });
|
|
295
|
+
need = need.concat(list.filter(function(r){ return r.state === "fail"; }).map(function(r){
|
|
296
|
+
var g = r.steps.filter(function(e){ return e.type === "gate" && e.result !== "pass"; })[0];
|
|
297
|
+
return {r:r, note:"failed · attempt " + r.attempt + (g ? " · " + clip(String(g.stage).split(":")[0], 40) : ""), st:"fail"}; }));
|
|
298
|
+
var seenB = {};
|
|
299
|
+
matrix.forEach(function(m){ if (m.status === "Needs Clarification" && !byReq[m.requirement] && !seenB[m.requirement]) { seenB[m.requirement] = 1;
|
|
300
|
+
need.push({r:{req:m.requirement, title:m.title, state:"idle"}, note:"blocked · needs clarification", st:"idle"}); } });
|
|
301
|
+
var n1 = block("Needs attention", need.length);
|
|
302
|
+
need.forEach(function(x){ n1.appendChild(jobBtn(x.r, sel, x.note, x.st)); });
|
|
303
|
+
if (!need.length) n1.appendChild(el("p", "muted", "Nothing failed or blocked."));
|
|
304
|
+
box.appendChild(n1);
|
|
305
|
+
|
|
306
|
+
var done = list.filter(function(r){ return r.state === "pass" && r.started >= since; });
|
|
307
|
+
var next = (model.queue || []).filter(function(id){ var r = byReq[id]; return !(r && r.started >= since); });
|
|
308
|
+
var times = done.map(function(r){ return r.elapsed; }).sort(function(a, b){ return a - b; });
|
|
309
|
+
var median = times.length ? times[Math.floor(times.length / 2)] : 0;
|
|
310
|
+
var n2 = block("Up next", next.length);
|
|
311
|
+
if (next.length && median) n2.appendChild(el("p", "muted", "about " + dur(next.length * median) + " at " + dur(median) + " each"));
|
|
312
|
+
next.slice(0, 6).forEach(function(id){ n2.appendChild(jobBtn({req:id, title:titles[id] || "", state:"idle"}, sel, "", "idle")); });
|
|
313
|
+
if (next.length > 6) n2.appendChild(el("p", "muted", "+ " + (next.length - 6) + " more"));
|
|
314
|
+
if (!next.length) n2.appendChild(el("p", "muted", (model.queue || []).length ? "The queue is done." : "No queue recorded for this run."));
|
|
315
|
+
box.appendChild(n2);
|
|
316
|
+
|
|
317
|
+
var recent = list.filter(function(r){ return r.state === "pass"; }).sort(function(a, b){ return b.updated - a.updated; }).slice(0, 5);
|
|
318
|
+
var n3 = block("Finished recently");
|
|
319
|
+
recent.forEach(function(r){ n3.appendChild(jobBtn(r, sel, dur(r.elapsed) + (r.usage && r.usage.cost !== null ? " · " + usd(r.usage.cost) : ""), "pass")); });
|
|
320
|
+
if (!recent.length) n3.appendChild(el("p", "muted", "None yet."));
|
|
321
|
+
box.appendChild(n3);
|
|
322
|
+
box.parentNode.scrollTop = keepScroll;
|
|
146
323
|
}
|
|
147
324
|
|
|
148
325
|
function summary(list){
|
|
149
326
|
var p = 0, f = 0, run = 0, first = 0, last = 0;
|
|
150
|
-
list.forEach(function(r){ if (r.state === "pass") p++; else if (r.state === "fail") f++; else run++;
|
|
327
|
+
list.forEach(function(r){ if (r.state === "pass") p++; else if (r.state === "fail" || r.state === "question") f++; else run++;
|
|
151
328
|
if (!first || r.started < first) first = r.started; last = Math.max(last, r.state === "running" ? now() : r.updated); });
|
|
152
329
|
var state = run ? "running" : f ? "fail" : list.length ? "pass" : "running";
|
|
153
330
|
$("runBadge").className = "badge " + state;
|
|
154
331
|
$("runState").textContent = run ? "In progress" : f ? "Failed" : list.length ? "Completed" : "Waiting";
|
|
155
|
-
var
|
|
156
|
-
$("runTitle").textContent =
|
|
332
|
+
var cur = list.filter(function(r){ return r.state === "running"; })[0];
|
|
333
|
+
$("runTitle").textContent = cur ? cap(cur.group) + " · " + cur.req + " " + (cur.title || "") : groups(list).map(function(g){ return cap(g.name); }).join(", ") || "Harness run";
|
|
157
334
|
var box = $("progress"); box.replaceChildren();
|
|
158
335
|
[["Started", first ? stamp(first) : "—"], ["Duration", first ? dur(last - first) : "—"],
|
|
159
336
|
["Requirements", p + " passed · " + run + " running · " + f + " failed"],
|
|
160
|
-
["Agent time", dur(list.reduce(function(a, r){ return a + elapsed(r); }, 0))]].
|
|
337
|
+
["Agent time", dur(list.reduce(function(a, r){ return a + elapsed(r); }, 0))]].concat(model.totals && (model.totals.input || model.totals.output) ? [
|
|
338
|
+
["Tokens", kt(model.totals.input + model.totals.cacheRead + model.totals.cacheWrite) + " in · " + kt(model.totals.output) + " out"],
|
|
339
|
+
["Cost", usd(model.totals.cost)]] : []).forEach(function(x){
|
|
161
340
|
var d = el("div"); d.append(el("span", "", x[0]), el("b", "", x[1])); box.appendChild(d);
|
|
162
341
|
});
|
|
342
|
+
var bar = el("div", "bar"); bar.style.flexBasis = "100%"; var total = Math.max(1, list.length);
|
|
343
|
+
[["var(--pass)", p], ["var(--run)", run], ["var(--fail)", f]].forEach(function(x){ var i = el("i"); i.style.width = (x[1] / total * 100) + "%"; i.style.background = x[0]; bar.appendChild(i); });
|
|
344
|
+
box.appendChild(bar);
|
|
163
345
|
}
|
|
164
346
|
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
var
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
var
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
347
|
+
/**
|
|
348
|
+
* The run as a matrix of modules (REQ-181): one row per module, a segment
|
|
349
|
+
* per requirement in id order, coloured by what happened to it in this run,
|
|
350
|
+
* or by its status when the run has not reached it; the passed count and
|
|
351
|
+
* the agent time. A segment opens its requirement below.
|
|
352
|
+
*/
|
|
353
|
+
function modules(list, sel){
|
|
354
|
+
var box = $("chain"); box.replaceChildren();
|
|
355
|
+
var byReq = {}; list.forEach(function(r){ byReq[r.req] = r; });
|
|
356
|
+
var reqs = [], seen = {};
|
|
357
|
+
matrix.forEach(function(m){ if (seen[m.requirement] || m.requirement === "REQ-001") return; seen[m.requirement] = 1;
|
|
358
|
+
reqs.push({req:m.requirement, title:m.title, group:m.group, status:m.status}); });
|
|
359
|
+
list.forEach(function(r){ if (!seen[r.req]) { seen[r.req] = 1; reqs.push({req:r.req, title:r.title, group:r.group, status:"Draft"}); } });
|
|
360
|
+
if (!reqs.length) { box.appendChild(el("div", "empty", "No harness events yet. Run specgate harness run: each requirement appears here as it starts.")); return; }
|
|
361
|
+
var order = [], rowsBy = {};
|
|
362
|
+
reqs.sort(function(x, y){ return x.req.localeCompare(y.req); }).forEach(function(q){
|
|
363
|
+
var g = q.group || "spec"; if (!rowsBy[g]) { rowsBy[g] = []; order.push(g); } rowsBy[g].push(q); });
|
|
364
|
+
var rank = function(g){ var rs = rowsBy[g], ev = rs.map(function(q){ return byReq[q.req]; }).filter(Boolean);
|
|
365
|
+
return ev.length ? Math.min.apply(null, ev.map(function(r){ return r.started; })) : Infinity; };
|
|
366
|
+
order.sort(function(x, y){ return rank(x) - rank(y); });
|
|
367
|
+
var head = el("div", "mm-row mm-head");
|
|
368
|
+
["", "Module", "Requirements", "Passed", "Time"].forEach(function(h, i){ var c = el("span", i >= 3 ? "num" : "", h); head.appendChild(c); });
|
|
369
|
+
box.appendChild(head);
|
|
370
|
+
order.forEach(function(g){
|
|
371
|
+
var rs = rowsBy[g], pass = 0, fail = 0, run = 0, time = 0;
|
|
372
|
+
var row = el("div", "mm-row"), bar = el("span", "mm-bar");
|
|
373
|
+
rs.forEach(function(q){
|
|
374
|
+
var ev = byReq[q.req], st = ev ? ev.state : (q.status === "Implemented" || q.status === "Verified" || q.status === "Released") ? "pass"
|
|
375
|
+
: q.status === "Needs Clarification" ? "blocked" : "queued";
|
|
376
|
+
if (st === "pass") pass++; else if (st === "fail" || st === "question") fail++; else if (st === "running") run++;
|
|
377
|
+
if (ev) time += elapsed(ev);
|
|
378
|
+
var seg = el("button", "seg " + st + (sel && sel.req === q.req ? " on" : "")); seg.type = "button";
|
|
379
|
+
seg.title = q.req + " — " + (q.title || "") + " · " + (ev ? st + " · " + dur(elapsed(ev)) : st);
|
|
380
|
+
seg.setAttribute("aria-label", seg.title);
|
|
381
|
+
seg.addEventListener("click", function(){ pick(q.req); });
|
|
382
|
+
bar.appendChild(seg);
|
|
383
|
+
});
|
|
384
|
+
var state = fail ? "fail" : run ? "running" : pass === rs.length ? "pass" : "idle";
|
|
385
|
+
if (fail) row.classList.add("has-fail"); else if (run) row.classList.add("has-run"); else if (state === "idle" && !pass) row.classList.add("queued");
|
|
386
|
+
row.append(icon(state), el("b", "", cap(g === "spec" ? "" : g)), bar, el("span", "num", pass + "/" + rs.length), el("span", "num muted", time ? dur(time) : pass === rs.length ? "earlier" : "queued"));
|
|
387
|
+
box.appendChild(row);
|
|
201
388
|
});
|
|
389
|
+
var legend = el("div", "mm-legend");
|
|
390
|
+
[["pass", "passed"], ["running", "running"], ["fail", "failed"], ["question", "asks"], ["blocked", "blocked"], ["queued", "queued"]].forEach(function(x){
|
|
391
|
+
var s0 = el("span"), sw = el("i", "seg " + x[0]); s0.append(sw, document.createTextNode(x[1])); legend.appendChild(s0); });
|
|
392
|
+
box.appendChild(legend);
|
|
202
393
|
}
|
|
203
394
|
|
|
204
|
-
/** The selected requirement's steps: the agent, then each stage of the gate as it ran, the rest still to come. */
|
|
205
395
|
function steps(r){
|
|
206
396
|
var ol = $("pipeline");
|
|
207
|
-
if (!r) {
|
|
208
|
-
$("fName").textContent = r.req + " · " + (r.title || "");
|
|
209
|
-
$("fMeta").textContent = "attempt " + r.attempt + (r.group ? " · " + r.group : "") + " · harness/" + r.req;
|
|
397
|
+
if (!r) { ol.replaceChildren(el("li", "empty", "Each requirement opens here as the harness works on it.")); return; }
|
|
210
398
|
var events = r.steps.slice().reverse(), firstGate = events.filter(function(e){ return e.type === "gate"; })[0];
|
|
211
399
|
var rows = [], seen = {Specification:false, Architecture:false, Tests:false, Merge:false};
|
|
212
400
|
var agentEnd = firstGate ? Number(firstGate.t) : (r.state === "running" ? now() : r.updated);
|
|
213
|
-
rows.push({name:"Agent",
|
|
401
|
+
rows.push({name:"Agent", state: firstGate ? "pass" : r.state === "running" ? "running" : r.state, d: dur(agentEnd - r.started), log: !firstGate});
|
|
214
402
|
var prevT = agentEnd;
|
|
215
403
|
events.forEach(function(e){
|
|
216
404
|
if (e.type !== "gate" && e.type !== "merge") return;
|
|
@@ -223,9 +411,9 @@ function livePage(projectName) {
|
|
|
223
411
|
var lastGate = events.filter(function(e){ return e.type === "gate"; }).slice(-1)[0];
|
|
224
412
|
if (r.state === "running" && firstGate && !(lastGate && lastGate.result !== "pass")) {
|
|
225
413
|
var next = ["Specification","Architecture","Tests","Merge"].filter(function(k){ return !seen[k]; })[0];
|
|
226
|
-
if (next) { rows.push({name:
|
|
414
|
+
if (next) { rows.push({name:next, state:"running", d: dur(now() - prevT), log:true}); seen[next] = true; }
|
|
227
415
|
}
|
|
228
|
-
["Specification","Architecture","Tests","Merge"].forEach(function(k){ if (!seen[k] && r.state === "running") rows.push({name:k,
|
|
416
|
+
["Specification","Architecture","Tests","Merge"].forEach(function(k){ if (!seen[k] && r.state === "running") rows.push({name:k, state:"idle", d:""}); });
|
|
229
417
|
ol.replaceChildren();
|
|
230
418
|
rows.forEach(function(x){
|
|
231
419
|
var li = el("li", x.state === "running" ? "running" : x.state === "idle" ? "idle" : "");
|
|
@@ -236,14 +424,14 @@ function livePage(projectName) {
|
|
|
236
424
|
if (!rows.some(function(x){ return x.log; }) && r.steps.length) { var lr2 = el("li", "logrow"); lr2.appendChild(log(r)); ol.appendChild(lr2); }
|
|
237
425
|
}
|
|
238
426
|
function log(r){
|
|
239
|
-
var ul = el("ol"); ul.id = "feed";
|
|
427
|
+
var ul = el("ol", "feed"); ul.id = "feed";
|
|
240
428
|
r.steps.slice(0, 40).slice().reverse().forEach(function(e){
|
|
241
429
|
var li = el("li"), t = Number(e.ended || e.t) || 0, kind = e.type === "step" ? String(e.kind || "") : String(e.type || "");
|
|
242
430
|
var what = e.type === "message" ? e.text : e.type === "gate" ? (e.result === "pass" ? "passed " : "failed ") + e.stage
|
|
243
431
|
: e.type === "merge" ? "merged into " + (e.into || "HEAD") : (e.target || e.tool || "");
|
|
244
432
|
if (e.added !== undefined || e.removed !== undefined) what += " +" + (Number(e.added) || 0) + " −" + (Number(e.removed) || 0);
|
|
245
|
-
|
|
246
|
-
|
|
433
|
+
li.append(el("span", "ts", stamp(t)), el("span", "k", e.type === "message" ? "agent" : kind),
|
|
434
|
+
el("span", "x" + (e.type === "message" ? " say" : e.type === "gate" ? (e.result === "pass" ? " ok" : " bad") : ""), clip(what, 400)));
|
|
247
435
|
ul.appendChild(li);
|
|
248
436
|
});
|
|
249
437
|
if (!ul.childNodes.length) ul.appendChild(el("li", "", "The agent's steps appear here as it works."));
|
|
@@ -251,27 +439,185 @@ function livePage(projectName) {
|
|
|
251
439
|
return ul;
|
|
252
440
|
}
|
|
253
441
|
|
|
442
|
+
/** The requirement's details, fetched once per change of its events. */
|
|
443
|
+
function detail(r, then){
|
|
444
|
+
var key = r.req + ":" + r.steps.length + ":" + r.state;
|
|
445
|
+
if (details[r.req] && details[r.req].key === key) return then(details[r.req].d);
|
|
446
|
+
fetch("live/req/" + encodeURIComponent(r.req) + ".json").then(function(x){ return x.json(); })
|
|
447
|
+
.then(function(d){ details[r.req] = {key:key, d:d}; then(d); }).catch(function(){});
|
|
448
|
+
}
|
|
449
|
+
function sect(title){ var d = el("div", "sect"); d.appendChild(el("h3", "", title)); return d; }
|
|
450
|
+
function paneReq(d){
|
|
451
|
+
var p = document.querySelector('[data-pane="req"]'); p.replaceChildren();
|
|
452
|
+
var a = sect("Requirement");
|
|
453
|
+
a.appendChild(el("div", "stmt", d.statement || "No requirement text found in the specification."));
|
|
454
|
+
var m = el("div", "meta"); m.style.marginTop = "8px";
|
|
455
|
+
m.append(el("span", "", "Status: " + d.status), el("span", "", "Module: " + (d.group || "—")));
|
|
456
|
+
if (d.source) { var sp = el("span", "", "Spec: "); sp.appendChild(fileLink(d.source.file, d.source.line)); m.appendChild(sp); }
|
|
457
|
+
a.appendChild(m); p.appendChild(a);
|
|
458
|
+
var b = sect("Scenarios · " + d.scenarios.length);
|
|
459
|
+
d.scenarios.forEach(function(sc){
|
|
460
|
+
var box = el("div", "scn"), h = el("div"); h.append(el("b", "", sc.id), document.createTextNode(" — " + sc.title)); box.appendChild(h);
|
|
461
|
+
var ol = el("ol"); sc.steps.forEach(function(st){ var li = el("li"), sp2 = st.indexOf(" "), kw = sp2 > 0 ? st.slice(0, sp2) : st;
|
|
462
|
+
li.append(el("span", "kw", kw), document.createTextNode(sp2 > 0 ? st.slice(sp2) : "")); ol.appendChild(li); });
|
|
463
|
+
box.appendChild(ol); b.appendChild(box);
|
|
464
|
+
});
|
|
465
|
+
if (!d.scenarios.length) b.appendChild(el("div", "meta", "No scenario."));
|
|
466
|
+
p.appendChild(b);
|
|
467
|
+
if (d.assumptions.length) {
|
|
468
|
+
var c = sect("Assumptions · " + d.assumptions.length), t = el("table"), hr = el("tr");
|
|
469
|
+
["#", "Assumption", "Status"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
|
|
470
|
+
d.assumptions.forEach(function(x){ var tr = el("tr"); tr.append(el("td", "mono", x.id), el("td", "", x.text), el("td", "", x.status)); t.appendChild(tr); });
|
|
471
|
+
c.appendChild(t); p.appendChild(c);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function paneFiles(d){
|
|
475
|
+
var p = document.querySelector('[data-pane="files"]'); p.replaceChildren();
|
|
476
|
+
var a = sect("Tests that name it · " + d.tests.length), ul = el("ul", "list");
|
|
477
|
+
d.tests.forEach(function(f){ var li = el("li"); li.appendChild(fileLink(f)); ul.appendChild(li); });
|
|
478
|
+
if (!d.tests.length) ul.appendChild(el("li", "", "No test names it yet."));
|
|
479
|
+
a.appendChild(ul); p.appendChild(a);
|
|
480
|
+
var b = sect("Feature"), ul2 = el("ul", "list");
|
|
481
|
+
d.features.forEach(function(f){ var li = el("li"); li.appendChild(fileLink(f)); ul2.appendChild(li); });
|
|
482
|
+
if (!d.features.length) ul2.appendChild(el("li", "", "Written when its change is archived."));
|
|
483
|
+
b.appendChild(ul2); p.appendChild(b);
|
|
484
|
+
var c = sect("Files the agent changed (last attempt) · " + d.files.length), ul3 = el("ul", "list");
|
|
485
|
+
d.files.forEach(function(f){ var li = el("li"); li.append(fileLink(f.path), el("span", "add", "+" + f.added), el("span", "rem", "−" + f.removed)); ul3.appendChild(li); });
|
|
486
|
+
if (!d.files.length) ul3.appendChild(el("li", "", "No file changed yet."));
|
|
487
|
+
c.appendChild(ul3); p.appendChild(c);
|
|
488
|
+
}
|
|
489
|
+
function paneAttempts(d){
|
|
490
|
+
var p = document.querySelector('[data-pane="attempts"]'); p.replaceChildren();
|
|
491
|
+
var a = sect("Attempts · " + d.attempts.length + " · branch harness/" + d.id);
|
|
492
|
+
var t = el("table"), hr = el("tr"); ["#", "Result", "Started", "Duration", "Failed at", "Gate"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
|
|
493
|
+
d.attempts.forEach(function(x){
|
|
494
|
+
var tr = el("tr"), res = el("td"), st = el("span", "st");
|
|
495
|
+
st.append(icon(x.result), document.createTextNode(x.result)); res.appendChild(st);
|
|
496
|
+
tr.append(el("td", "mono", String(x.attempt)), res, el("td", "mono", stamp(x.started)), el("td", "", dur(x.duration)),
|
|
497
|
+
el("td", "", x.failedAt || "—"), el("td", "tests", x.gates.map(function(g){ return (g.result === "pass" ? "✓ " : "✗ ") + g.stage; }).join(" · ")));
|
|
498
|
+
t.appendChild(tr);
|
|
499
|
+
});
|
|
500
|
+
if (!d.attempts.length) { var tr0 = el("tr"), td0 = el("td", "", "No attempt recorded."); td0.colSpan = 6; tr0.appendChild(td0); t.appendChild(tr0); }
|
|
501
|
+
a.appendChild(t); p.appendChild(a);
|
|
502
|
+
}
|
|
503
|
+
/** The agent's session for the selected attempt: model, context, tokens, cost and the prompt it was given (REQ-182). */
|
|
504
|
+
function paneAgent(r){
|
|
505
|
+
var p = document.querySelector('[data-pane="agent"]'); p.replaceChildren();
|
|
506
|
+
if (!r.agent && !r.usage && !r.prompt) {
|
|
507
|
+
p.appendChild(el("div", "empty", "No session reported for this attempt. Agents piped through specgate harness events --from claude report their model, tokens and prompt."));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
var a = sect("Session"), m = el("div", "meta");
|
|
511
|
+
m.append(el("span", "", "Model: " + (r.agent ? r.agent.model || "—" : "—")));
|
|
512
|
+
if (r.agent && r.agent.version) m.appendChild(el("span", "", "Claude Code " + r.agent.version));
|
|
513
|
+
if (r.agent && r.agent.permission) m.appendChild(el("span", "", "Permissions: " + r.agent.permission));
|
|
514
|
+
if (r.agent && r.agent.tools) m.appendChild(el("span", "", r.agent.tools + " tools"));
|
|
515
|
+
if (r.usage && r.usage.turns !== null) m.appendChild(el("span", "", r.usage.turns + " turns"));
|
|
516
|
+
a.appendChild(m); p.appendChild(a);
|
|
517
|
+
if (r.usage) {
|
|
518
|
+
var c = sect("Context window"); c.appendChild(ctxBar(r.usage));
|
|
519
|
+
c.appendChild(el("div", "meta", "Peak " + kt(r.usage.peak) + (r.usage.window ? " · " + Math.round(r.usage.peak / r.usage.window * 100) + "% of the window" : "")));
|
|
520
|
+
p.appendChild(c);
|
|
521
|
+
var t = sect("Tokens"), tb = el("table"), hr = el("tr");
|
|
522
|
+
["Input", "Cache read", "Cache write", "Output", "Cost"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); tb.appendChild(hr);
|
|
523
|
+
var tr = el("tr"), fin = r.usage.input || r.usage.cacheRead || r.usage.cacheWrite;
|
|
524
|
+
[fin ? kt(r.usage.input) : "—", fin ? kt(r.usage.cacheRead) : "—", fin ? kt(r.usage.cacheWrite) : "—", kt(r.usage.output), usd(r.usage.cost)].forEach(function(x){ tr.appendChild(el("td", "mono", x)); });
|
|
525
|
+
tb.appendChild(tr); t.appendChild(tb);
|
|
526
|
+
if (!fin) t.appendChild(el("div", "meta", "Totals and cost arrive when the session ends."));
|
|
527
|
+
p.appendChild(t);
|
|
528
|
+
}
|
|
529
|
+
if (r.prompt) {
|
|
530
|
+
var q = sect("Prompt"), qm = el("div", "meta");
|
|
531
|
+
qm.append(el("span", "", "For: " + r.prompt.role), el("span", "", r.prompt.lines + " lines"), el("span", "", kt(r.prompt.chars) + " characters"), el("span", "", "~" + kt(r.prompt.tokens) + " tokens (estimated)"));
|
|
532
|
+
var url = "live/req/" + encodeURIComponent(r.req) + "/prompt/" + r.attempt + ".txt", open1 = el("a", "", "Open in a new tab"); open1.href = url; open1.target = "_blank"; open1.rel = "noopener";
|
|
533
|
+
qm.appendChild(open1); q.appendChild(qm);
|
|
534
|
+
var dt = el("details", "prompt"), ds = el("summary", "", "Show the prompt"), pre = el("pre");
|
|
535
|
+
dt.append(ds, pre);
|
|
536
|
+
dt.addEventListener("toggle", function(){ if (dt.open && !pre.textContent) fetch(url).then(function(x){ return x.text(); }).then(function(x){ pre.textContent = x; }).catch(function(){ pre.textContent = "Not kept."; }); });
|
|
537
|
+
q.appendChild(dt); p.appendChild(q);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function focus(sel){
|
|
541
|
+
$("fName").textContent = sel ? sel.req + " · " + (sel.title || "") : "—";
|
|
542
|
+
$("fMeta").textContent = sel ? "attempt " + sel.attempt + (sel.group ? " · " + sel.group : "") + " · harness/" + sel.req + (sel.agent ? " · " + shortModel(sel.agent.model) : "") : "";
|
|
543
|
+
Array.prototype.forEach.call(document.querySelectorAll(".tabs button"), function(b){ b.setAttribute("aria-selected", String(b.dataset.tab === tab)); });
|
|
544
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-pane]"), function(p){ p.hidden = p.dataset.pane !== tab; });
|
|
545
|
+
if (!sel) return steps(null);
|
|
546
|
+
var key = sel.req + ":" + sel.attempt + ":" + sel.steps.length + ":" + sel.state + ":" + tab + ":" + (sel.usage ? sel.usage.context + "/" + sel.usage.output : "");
|
|
547
|
+
if (key === stepsKey) return;
|
|
548
|
+
stepsKey = key;
|
|
549
|
+
if (tab === "steps") steps(sel);
|
|
550
|
+
else if (tab === "agent") paneAgent(sel);
|
|
551
|
+
else detail(sel, function(d){ if (tab === "req") paneReq(d); else if (tab === "files") paneFiles(d); else paneAttempts(d); });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** The traceability matrix, one accordion per module, filtered as the viewer types. */
|
|
555
|
+
function drawMatrix(){
|
|
556
|
+
var q = String($("mFilter").value || "").toLowerCase(), box = $("matrix"); box.replaceChildren();
|
|
557
|
+
var rows = matrix.filter(function(r){ return !q || [r.requirement, r.scenario, r.title, r.status, r.group, r.feature].concat(r.tests).join(" ").toLowerCase().indexOf(q) >= 0; });
|
|
558
|
+
var reqs = {}, impl = {}; matrix.forEach(function(r){ reqs[r.requirement] = 1; if (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") impl[r.requirement] = 1; });
|
|
559
|
+
var tested = matrix.filter(function(r){ return r.tests.length; }).length;
|
|
560
|
+
$("mSum").textContent = Object.keys(impl).length + " of " + Object.keys(reqs).length + " requirements delivered · " + tested + " of " + matrix.length + " scenario rows have a test";
|
|
561
|
+
var running = {}; (model.requirements || []).forEach(function(r){ if (r.state === "running") running[r.req] = 1; });
|
|
562
|
+
var byGroup = {}, order = [];
|
|
563
|
+
rows.forEach(function(r){ var g = r.group || "spec.md"; if (!byGroup[g]) { byGroup[g] = []; order.push(g); } byGroup[g].push(r); });
|
|
564
|
+
order.forEach(function(g){
|
|
565
|
+
var list = byGroup[g], d = el("details", "box mgroup"), mk = "m:" + g; d.open = mk in open ? open[mk] : true;
|
|
566
|
+
var done = {}; list.forEach(function(r){ if (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") done[r.requirement] = 1; });
|
|
567
|
+
var all = {}; list.forEach(function(r){ all[r.requirement] = 1; });
|
|
568
|
+
var sm = el("summary"); sm.append(el("span", "", cap(g)), el("span", "count", Object.keys(done).length + " / " + Object.keys(all).length + " delivered")); d.appendChild(sm);
|
|
569
|
+
d.addEventListener("toggle", function(){ open[mk] = d.open; save("sg-live-open", open); });
|
|
570
|
+
var wrap = el("div"); wrap.style.overflowX = "auto";
|
|
571
|
+
var t = el("table"), hr = el("tr"); ["Requirement", "Title", "Scenario", "Feature", "Tests", "Status"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
|
|
572
|
+
list.forEach(function(r){
|
|
573
|
+
var tr = el("tr"), reqTd = el("td", "mono"), a = el("a", "", r.requirement); a.href = "#"; a.addEventListener("click", function(ev){ ev.preventDefault(); setView("run"); pinned = r.requirement; tab = "req"; stepsKey = ""; draw(); });
|
|
574
|
+
reqTd.appendChild(a);
|
|
575
|
+
var feat = el("td", "tests"); if (r.feature) feat.appendChild(fileLink(r.feature)); else feat.textContent = "—";
|
|
576
|
+
var tests = el("td", "tests"); if (r.tests.length) r.tests.forEach(function(f, i){ if (i) tests.appendChild(document.createElement("br")); tests.appendChild(fileLink(f)); }); else tests.textContent = "—";
|
|
577
|
+
var stTd = el("td"), state = running[r.requirement] ? "running" : r.status, sp = el("span", "st " + state.split(" ").join(""));
|
|
578
|
+
sp.append(icon(running[r.requirement] ? "running" : (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") ? "pass" : "idle", 14),
|
|
579
|
+
document.createTextNode(running[r.requirement] ? "in progress" : r.status + (r.source === "change" ? " · in change" : "")));
|
|
580
|
+
stTd.appendChild(sp);
|
|
581
|
+
tr.append(reqTd, el("td", "", r.title), el("td", "mono", r.scenario), feat, tests, stTd); t.appendChild(tr);
|
|
582
|
+
});
|
|
583
|
+
wrap.appendChild(t); d.appendChild(wrap); box.appendChild(d);
|
|
584
|
+
});
|
|
585
|
+
if (!order.length) box.appendChild(el("div", "empty box", matrix.length ? "No row matches the filter." : "Reading the matrix…"));
|
|
586
|
+
}
|
|
587
|
+
function refreshMatrix(){
|
|
588
|
+
if (!matrixDirty) return;
|
|
589
|
+
matrixDirty = false;
|
|
590
|
+
fetch("live/matrix.json").then(function(x){ return x.json(); }).then(function(m){ matrix = m.rows || []; if (view === "matrix") drawMatrix(); else draw(); }).catch(function(){ matrixDirty = true; });
|
|
591
|
+
}
|
|
592
|
+
function setView(v){
|
|
593
|
+
view = v; $("vRun").setAttribute("aria-pressed", String(v === "run")); $("vMatrix").setAttribute("aria-pressed", String(v === "matrix"));
|
|
594
|
+
$("runView").hidden = v !== "run"; $("matrixView").hidden = v !== "matrix"; if (v === "matrix") { drawMatrix(); refreshMatrix(); } else { stepsKey = ""; draw(); }
|
|
595
|
+
}
|
|
596
|
+
|
|
254
597
|
function draw(){
|
|
255
598
|
var list = ordered(model.requirements || []), sel = selected(list);
|
|
256
|
-
sidebar(list, sel);
|
|
257
|
-
|
|
258
|
-
if (key !== logKey) { logKey = key; steps(sel); }
|
|
599
|
+
sidebar(list, sel);
|
|
600
|
+
if (view === "run") { summary(list); modules(list, sel); focus(sel); }
|
|
259
601
|
}
|
|
260
602
|
function tick(){
|
|
261
603
|
var list = ordered(model.requirements || []), sel = selected(list);
|
|
262
|
-
summary(list);
|
|
263
|
-
var run = document.querySelector("#pipeline li.running .d");
|
|
264
|
-
if (run && sel && sel.state === "running") draw();
|
|
604
|
+
if (view === "run") { summary(list); if (sel && sel.state === "running" && tab === "steps") { stepsKey = ""; focus(sel); } }
|
|
265
605
|
}
|
|
266
|
-
function accept(m){ model = m && m.requirements ? m : {requirements:[]}; draw(); }
|
|
606
|
+
function accept(m){ model = m && m.requirements ? m : {requirements:[]}; if (m && m.projectDir) projectDir = m.projectDir; matrixDirty = true; draw(); refreshMatrix(); }
|
|
267
607
|
|
|
608
|
+
Array.prototype.forEach.call(document.querySelectorAll(".tabs button"), function(b){
|
|
609
|
+
b.addEventListener("click", function(){ tab = b.dataset.tab; stepsKey = ""; draw(); });
|
|
610
|
+
});
|
|
611
|
+
$("vRun").addEventListener("click", function(){ setView("run"); });
|
|
612
|
+
$("vMatrix").addEventListener("click", function(){ setView("matrix"); });
|
|
613
|
+
$("mFilter").addEventListener("input", drawMatrix);
|
|
268
614
|
fetch("live.json").then(function(x){ return x.json(); }).then(accept).catch(function(){});
|
|
269
615
|
var es = new EventSource("live/stream");
|
|
270
616
|
es.addEventListener("live", function(e){ try { accept(JSON.parse(e.data)); } catch (err) {} });
|
|
271
617
|
es.onopen = function(){ $("conn").classList.add("on"); $("connText").textContent = "Live"; };
|
|
272
618
|
es.onerror = function(){ $("conn").classList.remove("on"); $("connText").textContent = "Reconnecting"; };
|
|
273
619
|
setInterval(tick, 1000);
|
|
274
|
-
var t; window.addEventListener("resize", function(){ clearTimeout(t); t = setTimeout(function(){
|
|
620
|
+
var t; window.addEventListener("resize", function(){ clearTimeout(t); t = setTimeout(function(){ stepsKey = ""; draw(); }, 120); });
|
|
275
621
|
})();
|
|
276
622
|
</script>
|
|
277
623
|
</body></html>`;
|