in2-agent-server 0.20260821.185429

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.
Files changed (115) hide show
  1. package/README.md +369 -0
  2. package/agents/plan-requirement.md +32 -0
  3. package/dist/campfire/configItem.js +201 -0
  4. package/dist/campfire/configItemDescription.js +379 -0
  5. package/dist/campfire/deriveFromBundle.js +1013 -0
  6. package/dist/cli.js +465 -0
  7. package/dist/core/betaFeedback.js +493 -0
  8. package/dist/core/chapterInstructions.js +473 -0
  9. package/dist/core/claudeAssets.js +61 -0
  10. package/dist/core/gate.js +201 -0
  11. package/dist/core/ids.js +9 -0
  12. package/dist/core/install.js +60 -0
  13. package/dist/core/operatorQa.js +462 -0
  14. package/dist/core/orgCallMeter.js +178 -0
  15. package/dist/core/parcels.js +1235 -0
  16. package/dist/core/paths.js +105 -0
  17. package/dist/core/phases.js +225 -0
  18. package/dist/core/planFile.js +251 -0
  19. package/dist/core/planFragments.js +119 -0
  20. package/dist/core/readsLog.js +398 -0
  21. package/dist/core/registration.js +24 -0
  22. package/dist/core/result.js +38 -0
  23. package/dist/core/resume.js +233 -0
  24. package/dist/core/revisionChain.js +351 -0
  25. package/dist/core/richText.js +61 -0
  26. package/dist/core/semanticHash.js +89 -0
  27. package/dist/core/sessionOrg.js +60 -0
  28. package/dist/core/sfQuery.js +19 -0
  29. package/dist/core/spawn.js +182 -0
  30. package/dist/core/supervisorContract.js +230 -0
  31. package/dist/core/telemetry.js +1297 -0
  32. package/dist/core/telemetryContext.js +154 -0
  33. package/dist/core/telemetrySpool.js +244 -0
  34. package/dist/core/transcriptUpload.js +516 -0
  35. package/dist/core/update.js +363 -0
  36. package/dist/core/viewer.js +117 -0
  37. package/dist/core/workspace.js +92 -0
  38. package/dist/core/writeGrants.js +93 -0
  39. package/dist/core/writebackManifest.js +92 -0
  40. package/dist/direct/applyLoop.js +2516 -0
  41. package/dist/direct/batchReport.js +510 -0
  42. package/dist/direct/comparatorNoise.js +75 -0
  43. package/dist/direct/containers.js +85 -0
  44. package/dist/direct/contamination.js +221 -0
  45. package/dist/direct/crud.js +56 -0
  46. package/dist/direct/destructive.js +168 -0
  47. package/dist/direct/knownTokens.js +94 -0
  48. package/dist/direct/lint.js +382 -0
  49. package/dist/direct/metadataSchema.generated.json +1 -0
  50. package/dist/direct/metadataSchema.js +179 -0
  51. package/dist/direct/operatorPage.js +349 -0
  52. package/dist/direct/orgRepo.js +1531 -0
  53. package/dist/direct/orgRepoSpawnCount.js +21 -0
  54. package/dist/direct/plannedScope.js +119 -0
  55. package/dist/direct/preImageTargets.js +63 -0
  56. package/dist/direct/refVerify.js +191 -0
  57. package/dist/direct/registry.js +129 -0
  58. package/dist/direct/removals.js +262 -0
  59. package/dist/direct/rollback.js +1513 -0
  60. package/dist/direct/rollbackRun.js +1360 -0
  61. package/dist/direct/sessionProjection.js +126 -0
  62. package/dist/direct/snapshotComparability.js +165 -0
  63. package/dist/direct/sourceTree.js +113 -0
  64. package/dist/direct/unstatedChanges.js +106 -0
  65. package/dist/direct/waveAssembly.js +556 -0
  66. package/dist/direct/waveCounts.js +19 -0
  67. package/dist/direct/waveDerivation.js +152 -0
  68. package/dist/direct/waveGrade.js +185 -0
  69. package/dist/index.js +90 -0
  70. package/dist/orgread/customFieldSnapshot.js +48 -0
  71. package/dist/orgread/fieldMetadata.js +178 -0
  72. package/dist/orgread/genericSnapshot.js +61 -0
  73. package/dist/orgread/layoutSnapshot.js +151 -0
  74. package/dist/orgread/salesforceOrgReader.js +184 -0
  75. package/dist/orgread/snapshotTypes.js +9 -0
  76. package/dist/projection/bridge.js +26 -0
  77. package/dist/projection/diff.js +141 -0
  78. package/dist/projection/mapmd.js +213 -0
  79. package/dist/projection/nameAmbiguity.js +106 -0
  80. package/dist/projection/precedent.js +136 -0
  81. package/dist/projection/project.js +214 -0
  82. package/dist/projection/scope.js +110 -0
  83. package/dist/projection/types.js +5 -0
  84. package/dist/tools/analyze.js +322 -0
  85. package/dist/tools/auth.js +915 -0
  86. package/dist/tools/campfire.js +1742 -0
  87. package/dist/tools/closeBatch.js +154 -0
  88. package/dist/tools/closeText.js +35 -0
  89. package/dist/tools/direct.js +1613 -0
  90. package/dist/tools/feedback.js +318 -0
  91. package/dist/tools/operator.js +287 -0
  92. package/dist/tools/org.js +739 -0
  93. package/dist/tools/phase.js +249 -0
  94. package/dist/tools/projection.js +172 -0
  95. package/dist/tools/session.js +313 -0
  96. package/dist/tools/update.js +82 -0
  97. package/dist/tools/waves.js +793 -0
  98. package/package.json +48 -0
  99. package/reference/campfire-configuration-items-guide.md +710 -0
  100. package/reference/deployability-cribs.md +119 -0
  101. package/reference/operator-voice.md +141 -0
  102. package/reference/org-read-cribs.md +34 -0
  103. package/reference/scoped-dml-interim.md +68 -0
  104. package/reference/supervisor-playbook.md +313 -0
  105. package/reference/templates.md +233 -0
  106. package/skills/in2-delivery/SKILL.md +322 -0
  107. package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
  108. package/skills/in2-delivery/references/deployability-cribs.md +119 -0
  109. package/skills/in2-delivery/references/operator-voice.md +141 -0
  110. package/skills/in2-delivery/references/org-read-cribs.md +34 -0
  111. package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
  112. package/skills/in2-delivery/references/templates.md +233 -0
  113. package/skills/plan-requirement/SKILL.md +188 -0
  114. package/viewer/index.html +899 -0
  115. package/viewer/server.mjs +433 -0
@@ -0,0 +1,899 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>IN2 Delivery Run</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
10
+ <style>
11
+ /* Design system lifted from the original VFP console (legacy-vfp / prototype-vfp-console
12
+ partials/layout.ejs). Dark theme derived from the brand navy/charcoal. */
13
+ :root {
14
+ --cf-navy: #1E1B4B; --cf-navy-light: #312E81; --cf-charcoal: #0F0A2A;
15
+ --cf-primary: #7C3AED; --cf-primary-hover: #6D28D9;
16
+ --cf-primary-light: rgba(124, 58, 237, 0.1); --cf-primary-glow: rgba(124, 58, 237, 0.25);
17
+ --cf-success: #22c55e; --cf-success-light: rgba(34, 197, 94, 0.1); --cf-success-text: #15803d;
18
+ --cf-warning: #eab308; --cf-warning-light: rgba(234, 179, 8, 0.1); --cf-warning-text: #a16207;
19
+ --cf-danger: #ef4444; --cf-danger-light: rgba(239, 68, 68, 0.1); --cf-danger-text: #dc2626;
20
+ --cf-info: #3b82f6; --cf-info-light: rgba(59, 130, 246, 0.1); --cf-info-text: #2563eb;
21
+ --cf-bg: #f8fafc; --cf-surface: #ffffff;
22
+ --cf-border: #e2e8f0; --cf-border-light: #f1f5f9;
23
+ --cf-text: #1E1B4B; --cf-text-secondary: #64748b; --cf-text-muted: #94a3b8;
24
+ --cf-radius: 12px; --cf-radius-sm: 8px;
25
+ --cf-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
26
+ --cf-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.07), 0 2px 4px -2px rgba(0,0,0,0.05);
27
+ --cf-code-bg: #f1f5f9;
28
+ --cf-navbar: var(--cf-charcoal); --cf-navbar-text: #ffffff;
29
+ }
30
+ @media (prefers-color-scheme: dark) {
31
+ :root {
32
+ --cf-bg: #0F0A2A; --cf-surface: #1a1442;
33
+ --cf-border: #312E81; --cf-border-light: #262059;
34
+ --cf-text: #eef2ff; --cf-text-secondary: #a5b4fc; --cf-text-muted: #6f7bb6;
35
+ --cf-primary: #a78bfa; --cf-primary-hover: #c4b5fd;
36
+ --cf-primary-light: rgba(167, 139, 250, 0.14); --cf-primary-glow: rgba(167, 139, 250, 0.3);
37
+ --cf-success-text: #4ade80; --cf-warning-text: #facc15; --cf-danger-text: #f87171; --cf-info-text: #60a5fa;
38
+ --cf-shadow: 0 1px 3px rgba(0,0,0,0.3); --cf-shadow-md: 0 4px 10px rgba(0,0,0,0.35);
39
+ --cf-code-bg: #262059;
40
+ --cf-navbar: #0a0720; --cf-navbar-text: #ffffff;
41
+ }
42
+ }
43
+ * { box-sizing: border-box; }
44
+ body {
45
+ margin: 0; background: var(--cf-bg); color: var(--cf-text);
46
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
47
+ font-size: 13.5px; line-height: 1.5; -webkit-font-smoothing: antialiased;
48
+ }
49
+
50
+ .cf-navbar {
51
+ background: var(--cf-navbar); color: var(--cf-navbar-text);
52
+ padding: 0.6rem 14px; border-bottom: 1px solid rgba(255,255,255,0.06);
53
+ display: flex; align-items: center; gap: 10px;
54
+ }
55
+ .cf-navbar .brand { font-weight: 700; font-size: 0.95rem; letter-spacing: -0.01em; display: flex; align-items: center; gap: 8px; }
56
+ .brand-mark {
57
+ width: 22px; height: 22px; border-radius: 6px; flex: 0 0 auto;
58
+ background: linear-gradient(135deg, var(--cf-primary), var(--cf-navy-light));
59
+ display: inline-flex; align-items: center; justify-content: center;
60
+ font-size: 10px; font-weight: 800; color: #fff;
61
+ }
62
+ #live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--cf-success); flex: 0 0 auto; }
63
+ #live-dot.stale { background: var(--cf-warning); }
64
+ .cf-navbar .org { color: rgba(255,255,255,0.55); font-size: 0.72rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
65
+ .cf-navbar select {
66
+ background: rgba(255,255,255,0.08); color: var(--cf-navbar-text);
67
+ border: 1px solid rgba(255,255,255,0.15); border-radius: var(--cf-radius-sm);
68
+ font: inherit; font-size: 0.72rem; padding: 3px 6px; max-width: 220px;
69
+ }
70
+
71
+ .wrap { max-width: 780px; margin: 0 auto; padding: 12px 14px 44px; }
72
+ .stepper-slot { position: sticky; top: 8px; z-index: 5; margin-bottom: 12px; }
73
+
74
+ /* ── Stepper A: grouped steps + sub-dots ────────────────────────────────── */
75
+ .cf-stepper {
76
+ display: flex; align-items: flex-start; justify-content: center;
77
+ background: var(--cf-surface); border: 1px solid var(--cf-border); border-radius: var(--cf-radius);
78
+ box-shadow: var(--cf-shadow-md); padding: 0.9rem 0.5rem 0.65rem;
79
+ }
80
+ .cf-step { flex: 1; min-width: 0; text-align: center; position: relative; cursor: pointer; }
81
+ .cf-step-line {
82
+ position: absolute; top: 14px; left: calc(-50% + 16px); right: calc(50% + 16px);
83
+ height: 2px; background: var(--cf-border);
84
+ }
85
+ .cf-step:first-child .cf-step-line { display: none; }
86
+ .cf-step.reached .cf-step-line { background: var(--cf-success); }
87
+ .cf-step-circle {
88
+ width: 28px; height: 28px; border-radius: 50%; margin: 0 auto;
89
+ display: flex; align-items: center; justify-content: center;
90
+ font-size: 0.72rem; font-weight: 700; transition: all 0.3s ease; position: relative; z-index: 1;
91
+ background: var(--cf-border-light); color: var(--cf-text-muted); border: 2px solid var(--cf-border);
92
+ }
93
+ .cf-step.done .cf-step-circle { background: var(--cf-success); border-color: var(--cf-success); color: #fff; }
94
+ .cf-step.active .cf-step-circle {
95
+ background: var(--cf-primary); border-color: var(--cf-primary); color: #fff;
96
+ animation: cfGlow 1.8s ease-in-out infinite;
97
+ }
98
+ @keyframes cfGlow {
99
+ 0%,100% { box-shadow: 0 0 0 4px var(--cf-primary-light); }
100
+ 50% { box-shadow: 0 0 0 7px var(--cf-primary-glow); }
101
+ }
102
+ .cf-step-label {
103
+ font-size: 0.66rem; font-weight: 500; color: var(--cf-text-muted); margin-top: 5px;
104
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 2px;
105
+ }
106
+ .cf-step.done .cf-step-label { color: var(--cf-success-text); }
107
+ .cf-step.active .cf-step-label { color: var(--cf-primary); font-weight: 600; }
108
+ .subdots { display: flex; gap: 4px; justify-content: center; margin-top: 4px; min-height: 6px; }
109
+ .subdot { width: 5px; height: 5px; border-radius: 50%; background: var(--cf-border); }
110
+ .subdot.done { background: var(--cf-success); }
111
+ .subdot.now { background: var(--cf-primary); animation: cfGlow 1.6s ease-in-out infinite; }
112
+ .cf-step .pgmark { height: 3px; border-radius: 100px; margin: 5px 8px 0; background: transparent; }
113
+ .cf-step.onpage .pgmark { background: var(--cf-primary); }
114
+
115
+ /* ── Page bar ───────────────────────────────────────────────────────────── */
116
+ .pagebar { display: flex; align-items: center; gap: 8px; margin: 2px 2px 10px; }
117
+ .cf-heading { font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; font-size: 0.95rem; }
118
+ .cf-subtext { color: var(--cf-text-secondary); font-size: 0.75rem; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
119
+ .navbtn {
120
+ border: 1px solid var(--cf-border); background: var(--cf-surface); color: var(--cf-text);
121
+ border-radius: var(--cf-radius-sm); font: inherit; font-size: 0.8rem; font-weight: 600;
122
+ padding: 2px 10px; cursor: pointer; transition: all 0.15s ease; box-shadow: var(--cf-shadow);
123
+ }
124
+ .navbtn:hover:not(:disabled) { border-color: var(--cf-primary); color: var(--cf-primary); }
125
+ .navbtn:disabled { opacity: 0.35; cursor: default; }
126
+ #follow {
127
+ border: none; background: var(--cf-border-light); color: var(--cf-text-muted); border-radius: 100px;
128
+ font: inherit; font-size: 0.65rem; font-weight: 700; padding: 3px 11px; cursor: pointer;
129
+ letter-spacing: 0.05em; transition: all 0.15s ease;
130
+ }
131
+ #follow.on { background: var(--cf-primary); color: #fff; box-shadow: 0 0 0 3px var(--cf-primary-light); }
132
+
133
+ .pages { overflow: hidden; }
134
+ .track { display: flex; align-items: flex-start; transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
135
+ .page { flex: 0 0 100%; min-width: 0; padding: 1px; }
136
+ .page:not(.current) { max-height: 100vh; overflow: hidden; }
137
+ .track.settled .page:not(.current) { visibility: hidden; }
138
+
139
+ /* ── Cards ──────────────────────────────────────────────────────────────── */
140
+ .cf-card {
141
+ background: var(--cf-surface); border: 1px solid var(--cf-border);
142
+ border-radius: var(--cf-radius); box-shadow: var(--cf-shadow); margin-bottom: 12px; overflow: hidden;
143
+ }
144
+ .cf-card-header {
145
+ padding: 0.7rem 1rem; border-bottom: 1px solid var(--cf-border);
146
+ font-weight: 600; font-size: 0.8rem; display: flex; align-items: center; gap: 0.5rem;
147
+ }
148
+ .cf-card-header .count { margin-left: auto; font-weight: 600; font-size: 0.72rem; color: var(--cf-text-secondary); }
149
+ .cf-card-header.cf-dark { background: var(--cf-navy); color: #fff; border-bottom: none; }
150
+ .cf-card-header.cf-dark .count { color: rgba(255,255,255,0.7); }
151
+ .cf-label {
152
+ font-size: 0.68rem; font-weight: 600; color: var(--cf-text-secondary);
153
+ text-transform: uppercase; letter-spacing: 0.04em;
154
+ }
155
+
156
+ .narration { padding: 0.65rem 1rem; color: var(--cf-text-secondary); font-size: 0.78rem; border-bottom: 1px solid var(--cf-border-light); }
157
+ .narration:last-child { border-bottom: 0; }
158
+ .narration .nh { color: var(--cf-text); font-weight: 600; font-size: 0.74rem; }
159
+ .narration .nh .t { font-weight: 400; color: var(--cf-text-muted); }
160
+ .narration .nt { max-height: 3.1em; overflow: hidden; cursor: pointer; }
161
+ .narration.open .nt { max-height: none; }
162
+
163
+ /* ── Requirement rows ───────────────────────────────────────────────────── */
164
+ .req { border-bottom: 1px solid var(--cf-border-light); animation: cfSlideIn 0.3s ease both; }
165
+ .req:last-child { border-bottom: 0; }
166
+ @keyframes cfSlideIn { from { opacity: 0; transform: translateX(-12px); } to { opacity: 1; transform: none; } }
167
+ .req-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.55rem 1rem; cursor: default; }
168
+ .req.has-detail .req-row { cursor: pointer; }
169
+ .req.analyzing { background: var(--cf-primary-light); box-shadow: inset 3px 0 0 var(--cf-primary); }
170
+ .req .icon { flex: 0 0 16px; text-align: center; font-size: 0.8rem; }
171
+ .req .icon .ok { color: var(--cf-success); }
172
+ .req .icon .bad { color: var(--cf-danger); }
173
+ .req .icon .idle { color: var(--cf-text-muted); }
174
+ .req .id {
175
+ font-weight: 600; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em;
176
+ color: var(--cf-text-secondary); flex: 0 0 auto; min-width: 4.4rem;
177
+ }
178
+ .req .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; font-size: 0.8rem; }
179
+ .spinner {
180
+ width: 12px; height: 12px; border: 2px solid var(--cf-border); border-top-color: var(--cf-primary);
181
+ border-radius: 50%; display: inline-block; animation: spin 0.8s linear infinite; vertical-align: -2px;
182
+ }
183
+ @keyframes spin { to { transform: rotate(360deg); } }
184
+
185
+ .cf-badge {
186
+ display: inline-flex; align-items: center; gap: 4px; padding: 0.16rem 0.55rem;
187
+ font-size: 0.66rem; font-weight: 600; border-radius: 100px; line-height: 1.4; flex: 0 0 auto;
188
+ }
189
+ .cf-badge-success { background: var(--cf-success-light); color: var(--cf-success-text); }
190
+ .cf-badge-warning { background: var(--cf-warning-light); color: var(--cf-warning-text); }
191
+ .cf-badge-danger { background: var(--cf-danger-light); color: var(--cf-danger-text); }
192
+ .cf-badge-info { background: var(--cf-info-light); color: var(--cf-info-text); }
193
+ .cf-badge-coral { background: var(--cf-primary-light); color: var(--cf-primary); }
194
+ .cf-badge-neutral { background: var(--cf-border-light); color: var(--cf-text-secondary); }
195
+
196
+ .req .detail { display: none; padding: 0.1rem 1rem 0.7rem 2.4rem; }
197
+ .req.open .detail { display: block; }
198
+ .detail .summary { color: var(--cf-text-secondary); font-size: 0.76rem; margin-bottom: 0.4rem; }
199
+ .comp { display: flex; gap: 6px; font-size: 0.76rem; padding: 2px 0; align-items: baseline; }
200
+ .comp code {
201
+ background: var(--cf-code-bg); border-radius: 5px; padding: 0 6px; font-size: 0.72rem;
202
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
203
+ }
204
+ .comp .intent { color: var(--cf-text-muted); font-size: 0.68rem; }
205
+ .issue { border-radius: var(--cf-radius-sm); padding: 0.35rem 0.6rem; font-size: 0.76rem; margin: 3px 0; }
206
+ .issue-danger { background: var(--cf-danger-light); color: var(--cf-danger-text); }
207
+ .issue-warning { background: var(--cf-warning-light); color: var(--cf-warning-text); }
208
+
209
+ .cf-progress { background: var(--cf-border-light); border-radius: 100px; height: 8px; overflow: hidden; margin: 0 1rem 0.8rem; }
210
+ .cf-progress-bar { height: 100%; border-radius: 100px; background: var(--cf-primary); transition: width 0.6s cubic-bezier(0.4,0,0.2,1); }
211
+ .cf-progress-bar.complete { background: linear-gradient(90deg, var(--cf-success), #16a34a); }
212
+
213
+ /* ── Start page: session facts ──────────────────────────────────────────── */
214
+ .facts { display: grid; grid-template-columns: repeat(2, 1fr); }
215
+ .fact { padding: 0.7rem 1rem; border-bottom: 1px solid var(--cf-border-light); }
216
+ .fact:nth-child(odd) { border-right: 1px solid var(--cf-border-light); }
217
+ .fact .fv { font-size: 0.8rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
218
+ .fact .fk { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--cf-text-muted); font-weight: 600; margin-top: 1px; }
219
+
220
+ /* ── Waves ──────────────────────────────────────────────────────────────── */
221
+ .wave { border-bottom: 1px solid var(--cf-border-light); padding: 0.6rem 1rem; }
222
+ .wave:last-child { border-bottom: 0; }
223
+ .wave-head { display: flex; align-items: center; gap: 8px; }
224
+ .wave-head .name { font-weight: 600; font-size: 0.82rem; flex: 1; }
225
+ .wave .meta { color: var(--cf-text-secondary); font-size: 0.72rem; margin-top: 2px; }
226
+ .wave .comps { margin-top: 4px; display: none; }
227
+ .wave.open .comps { display: block; }
228
+ .wave.has-comps { cursor: pointer; }
229
+
230
+ /* ── Clarify: decision cards ────────────────────────────────────────────── */
231
+ .dcard {
232
+ border: 1px solid var(--cf-border); border-radius: var(--cf-radius-sm);
233
+ margin: 0.6rem 1rem; box-shadow: var(--cf-shadow); overflow: hidden;
234
+ }
235
+ .dcard-head { display: flex; align-items: center; gap: 8px; padding: 0.55rem 0.8rem; background: var(--cf-border-light); }
236
+ .dcard-head .h { font-weight: 700; font-size: 0.78rem; flex: 1; }
237
+ .dcard-head .rnd { font-size: 0.62rem; color: var(--cf-text-muted); font-weight: 600; }
238
+ .dcard-q { padding: 0.55rem 0.8rem 0.2rem; color: var(--cf-text-secondary); font-size: 0.76rem; white-space: pre-line; max-height: 4.6em; overflow: hidden; cursor: pointer; position: relative; }
239
+ .dcard.open .dcard-q { max-height: none; }
240
+ .dcard-q::after {
241
+ content: "more"; position: absolute; right: 0.6rem; bottom: 0; font-size: 0.64rem; font-weight: 600;
242
+ color: var(--cf-primary); background: linear-gradient(90deg, transparent, var(--cf-surface) 30%);
243
+ padding-left: 1.6rem;
244
+ }
245
+ .dcard.open .dcard-q::after { content: "less"; position: static; padding: 0; background: none; display: inline-block; margin-left: 6px; }
246
+ .dcard-opts { display: flex; flex-wrap: wrap; gap: 6px; padding: 0.55rem 0.8rem 0.7rem; }
247
+ .chip {
248
+ border: 1px solid var(--cf-border); border-radius: 100px; padding: 0.22rem 0.7rem;
249
+ font-size: 0.7rem; font-weight: 500; color: var(--cf-text-secondary);
250
+ }
251
+ .chip.chosen {
252
+ background: var(--cf-success-light); border-color: var(--cf-success);
253
+ color: var(--cf-success-text); font-weight: 600;
254
+ }
255
+ .chip.chosen::before { content: "✓ "; }
256
+
257
+ /* ── Results ────────────────────────────────────────────────────────────── */
258
+ .res-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; padding: 0.8rem 1rem; }
259
+ @media (max-width: 480px) { .res-grid { grid-template-columns: 1fr; } }
260
+ .summary-cell { padding: 0.8rem 0.9rem; border-radius: var(--cf-radius-sm); border-left: 3px solid transparent; }
261
+ .summary-cell--success { background: var(--cf-success-light); border-left-color: var(--cf-success); }
262
+ .summary-cell--warning { background: var(--cf-warning-light); border-left-color: var(--cf-warning); }
263
+ .summary-cell--danger { background: var(--cf-danger-light); border-left-color: var(--cf-danger); }
264
+ .summary-cell .cell-number { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
265
+ .summary-cell--success .cell-number { color: var(--cf-success-text); }
266
+ .summary-cell--warning .cell-number { color: var(--cf-warning-text); }
267
+ .summary-cell--danger .cell-number { color: var(--cf-danger-text); }
268
+ .summary-cell .cell-label {
269
+ text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.6rem; font-weight: 600;
270
+ color: var(--cf-text-secondary); margin-top: 0.15rem;
271
+ }
272
+ .summary-cell .cell-sub { font-size: 0.64rem; color: var(--cf-text-muted); }
273
+ .hero { display: flex; align-items: center; gap: 0.9rem; padding: 0.9rem 1rem 0.2rem; }
274
+ .hero .big { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; }
275
+ .hero .sub { font-size: 0.72rem; color: var(--cf-text-secondary); margin-top: 1px; }
276
+ .result-card {
277
+ border: 1px solid var(--cf-border); border-left-width: 3px; border-radius: var(--cf-radius-sm);
278
+ margin: 0 1rem 0.5rem; padding: 0.6rem 0.8rem; box-shadow: var(--cf-shadow); cursor: pointer;
279
+ }
280
+ .result-card.rc-success { border-left-color: var(--cf-success); }
281
+ .result-card.rc-warning { border-left-color: var(--cf-warning); }
282
+ .result-card.rc-danger { border-left-color: var(--cf-danger); }
283
+ .result-card.rc-info { border-left-color: var(--cf-info); }
284
+ .result-card.rc-neutral { border-left-color: var(--cf-text-muted); }
285
+ .result-head { display: flex; align-items: center; gap: 0.5rem; }
286
+ .result-head .rid { font-weight: 700; font-size: 0.76rem; }
287
+ .result-head .rtitle { font-size: 0.78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
288
+ .result-head .caret { color: var(--cf-text-muted); font-size: 0.66rem; transition: transform 0.15s ease; }
289
+ .result-card.open .result-head .caret { transform: rotate(90deg); }
290
+ .result-sub { font-size: 0.7rem; color: var(--cf-text-muted); margin-top: 2px; }
291
+
292
+ /* Per-requirement breakdown: component actions + the requirement's writeback row. */
293
+ .result-body { display: none; margin-top: 0.55rem; border-top: 1px solid var(--cf-border-light); padding-top: 0.45rem; }
294
+ .result-card.open .result-body { display: block; }
295
+ .action { display: flex; align-items: center; gap: 0.5rem; padding: 0.22rem 0; font-size: 0.74rem; }
296
+ .action .aicon { flex: 0 0 14px; text-align: center; font-size: 0.72rem; }
297
+ .action code {
298
+ background: var(--cf-code-bg); border-radius: 5px; padding: 0 6px; font-size: 0.7rem;
299
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
300
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
301
+ }
302
+ .action .aintent { color: var(--cf-text-muted); font-size: 0.66rem; flex: 0 0 auto; }
303
+ .action .awave { margin-left: auto; color: var(--cf-text-muted); font-size: 0.64rem; flex: 0 0 auto; }
304
+ .wbrow {
305
+ display: flex; align-items: center; gap: 0.5rem; margin-top: 0.45rem;
306
+ padding: 0.4rem 0.55rem; border-radius: var(--cf-radius-sm); background: var(--cf-border-light);
307
+ font-size: 0.72rem;
308
+ }
309
+ .wbrow .wlabel { font-weight: 600; color: var(--cf-text-secondary); flex: 1; }
310
+
311
+ .scope-stat { display: flex; gap: 0.6rem; padding: 0.8rem 1rem; border-bottom: 1px solid var(--cf-border-light); }
312
+ .scope-stat .cell { flex: 1; background: var(--cf-border-light); border-radius: var(--cf-radius-sm); padding: 0.55rem 0.7rem; text-align: center; }
313
+ .scope-stat .n { font-size: 1.25rem; font-weight: 700; line-height: 1.1; }
314
+ .scope-stat .l { font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--cf-text-secondary); font-weight: 600; margin-top: 2px; }
315
+
316
+ .findings { padding: 0.2rem 1rem 0.8rem; color: var(--cf-text-secondary); font-size: 0.76rem; }
317
+ .empty { color: var(--cf-text-muted); padding: 0.9rem 1rem; font-size: 0.78rem; }
318
+ .placeholder { text-align: center; padding: 2.2rem 1rem; color: var(--cf-text-muted); font-size: 0.78rem; }
319
+ .placeholder .ph-dot { width: 22px; height: 22px; border: 2px dashed var(--cf-border); border-radius: 50%; margin: 0 auto 8px; }
320
+ </style>
321
+ </head>
322
+ <body>
323
+ <div class="cf-navbar">
324
+ <span class="brand"><span class="brand-mark">IN2</span><span id="live-dot"></span>Delivery Run</span>
325
+ <span class="org" id="org-line">loading…</span>
326
+ <select id="session-picker" title="Session"></select>
327
+ </div>
328
+
329
+ <div class="wrap">
330
+ <div class="stepper-slot">
331
+ <div class="cf-stepper" id="stepper-a"></div>
332
+ </div>
333
+
334
+ <div class="pagebar">
335
+ <button class="navbtn" id="prev" title="Previous page">‹</button>
336
+ <span class="cf-heading" id="pg-title"></span>
337
+ <span class="cf-subtext" id="pg-sub"></span>
338
+ <button id="follow" class="on" title="Follow the run: auto-advance to the page of the active step">LIVE</button>
339
+ <button class="navbtn" id="next" title="Next page">›</button>
340
+ </div>
341
+
342
+ <div class="pages">
343
+ <div class="track" id="track">
344
+ <div class="page" id="page-start"></div>
345
+ <div class="page" id="page-analysis"></div>
346
+ <div class="page" id="page-clarify">
347
+ <div id="clarify-nar"></div>
348
+ <div id="clarify-a"></div>
349
+ </div>
350
+ <div class="page" id="page-plan"></div>
351
+ <div class="page" id="page-delivery"></div>
352
+ <div class="page" id="page-results"></div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <script>
358
+ (function () {
359
+ var currentSession = new URLSearchParams(location.search).get("session") || null;
360
+ var open = {};
361
+ var pageIdx = 0;
362
+ var follow = true;
363
+ var failCount = 0;
364
+ var lastState = null;
365
+
366
+ function esc(s) {
367
+ return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
368
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
369
+ });
370
+ }
371
+ function when(iso) {
372
+ if (!iso) return "";
373
+ return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
374
+ }
375
+
376
+ // Six top-level STEPS; the nine ledger chapters are subchapters of them. The ledger stays the
377
+ // source of truth — this table only groups it for display.
378
+ var STEPS = [
379
+ { lbl: "Start", page: 0, chapters: ["activation", "intake"] },
380
+ { lbl: "Analyze", page: 1, chapters: ["partitioning", "parallel-analysis"] },
381
+ { lbl: "Clarify", page: 2, chapters: ["clarification"] },
382
+ { lbl: "Plan", page: 3, chapters: ["scope-approval"] },
383
+ { lbl: "Deliver", page: 4, chapters: ["delivery", "review-results"] },
384
+ { lbl: "Results", page: 5, chapters: ["writeback-close"] }
385
+ ];
386
+ var PAGES = [
387
+ { title: "Batch", sub: "the requirements this run will deliver" },
388
+ { title: "Analysis", sub: "partition → parallel analyzers → plans" },
389
+ { title: "Clarifications", sub: "what the run needed you to decide" },
390
+ { title: "Plan & Approval", sub: "everything that will be built, in one sheet" },
391
+ { title: "Delivery", sub: "waves deployed with read-back proof" },
392
+ { title: "Results", sub: "outcomes, writeback, and the batch report" }
393
+ ];
394
+
395
+ function chapterByName(state, name) {
396
+ for (var i = 0; i < state.chapters.length; i++) if (state.chapters[i].name === name) return state.chapters[i];
397
+ return null;
398
+ }
399
+ // A step is done when all its chapters are done/skipped, active when any is active,
400
+ // pending otherwise.
401
+ function stepStatus(state, step) {
402
+ var sts = step.chapters.map(function (n) { return (chapterByName(state, n) || { status: "pending" }).status; });
403
+ if (sts.indexOf("active") >= 0) return "active";
404
+ if (sts.every(function (s) { return s === "done" || s === "skipped"; })) return "done";
405
+ if (sts.some(function (s) { return s === "done" || s === "skipped"; })) return "active";
406
+ return "pending";
407
+ }
408
+ function stepOfChapter(name) {
409
+ for (var i = 0; i < STEPS.length; i++) if (STEPS[i].chapters.indexOf(name) >= 0) return i;
410
+ return 0;
411
+ }
412
+
413
+ // ── stepper: grouped steps with sub-dots for each step's chapters ─────────
414
+ function renderSteppers(state) {
415
+ var a = [];
416
+ STEPS.forEach(function (s, i) {
417
+ var st = stepStatus(state, s);
418
+ var reached = st !== "pending";
419
+ var onpage = s.page === pageIdx;
420
+ var tip = s.chapters.map(function (n) {
421
+ var ch = chapterByName(state, n) || { status: "pending" };
422
+ return n + " · " + ch.status + (ch.at ? " · " + when(ch.at) : "");
423
+ }).join("\n");
424
+ var dots = s.chapters.length > 1 ? s.chapters.map(function (n) {
425
+ var ch = chapterByName(state, n) || { status: "pending" };
426
+ var d = ch.status === "done" || ch.status === "skipped" ? "done" : ch.status === "active" ? "now" : "";
427
+ return '<span class="subdot ' + d + '"></span>';
428
+ }).join("") : "";
429
+ a.push('<div class="cf-step ' + st + (reached ? " reached" : "") + (onpage ? " onpage" : "") +
430
+ '" data-page="' + s.page + '" title="' + esc(tip) + '">' +
431
+ '<div class="cf-step-line"></div>' +
432
+ '<div class="cf-step-circle">' + (st === "done" ? "✓" : String(i + 1)) + "</div>" +
433
+ '<div class="cf-step-label">' + esc(s.lbl) + "</div>" +
434
+ '<div class="subdots">' + dots + "</div>" +
435
+ '<div class="pgmark"></div></div>');
436
+ });
437
+ document.getElementById("stepper-a").innerHTML = a.join("");
438
+ }
439
+
440
+ function narration(state, names) {
441
+ var out = names.map(function (n) {
442
+ var c = chapterByName(state, n);
443
+ if (!c || !c.note) return "";
444
+ var key = "nar:" + n;
445
+ return '<div class="narration' + (open[key] ? " open" : "") + '" data-key="' + key + '">' +
446
+ '<div class="nh">' + esc(n) + (c.at ? ' <span class="t">' + when(c.at) + "</span>" : "") + "</div>" +
447
+ '<div class="nt">' + esc(c.note) + "</div></div>";
448
+ }).join("");
449
+ return out ? '<div class="cf-card">' + out + "</div>" : "";
450
+ }
451
+ function placeholder(text) {
452
+ return '<div class="cf-card"><div class="placeholder"><div class="ph-dot"></div>' + esc(text) + "</div></div>";
453
+ }
454
+
455
+ // ── page 0: batch (session facts + imported requirements) ─────────────────
456
+ function renderStartPage(state) {
457
+ var facts =
458
+ '<div class="cf-card"><div class="cf-card-header">Session<span class="count">' +
459
+ esc(state.sessionId.slice(0, 16)) + "…</span></div>" +
460
+ '<div class="facts">' +
461
+ '<div class="fact"><div class="fv">' + esc(state.org.targetAlias || state.org.targetOrgId || "—") + '</div><div class="fk">Target org</div></div>' +
462
+ '<div class="fact"><div class="fv">' + esc(state.org.operator || "—") + '</div><div class="fk">Operator</div></div>' +
463
+ '<div class="fact"><div class="fv">' + state.counts.total + '</div><div class="fk">Requirements imported</div></div>' +
464
+ '<div class="fact"><div class="fv">' + (state.createdAt ? new Date(state.createdAt).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "—") + '</div><div class="fk">Started</div></div>' +
465
+ "</div></div>";
466
+ var rows = state.requirements.map(function (r, i) {
467
+ return '<div class="req" style="animation-delay:' + Math.min(i * 0.04, 0.6) + 's"><div class="req-row">' +
468
+ '<span class="id">' + esc(r.extId) + "</span>" +
469
+ '<span class="title" title="' + esc(r.title) + '">' + esc(r.title) + "</span></div></div>";
470
+ }).join("");
471
+ document.getElementById("page-start").innerHTML =
472
+ narration(state, ["activation", "intake"]) + facts +
473
+ '<div class="cf-card"><div class="cf-card-header">Imported requirements<span class="count">' +
474
+ state.counts.total + "</span></div>" +
475
+ (rows || '<div class="empty">Requirements appear here as the batch is imported.</div>') + "</div>";
476
+ }
477
+
478
+ // ── page 1: analysis ──────────────────────────────────────────────────────
479
+ function reqBadge(status) {
480
+ if (status === "analyzed") return '<span class="cf-badge cf-badge-success">planned</span>';
481
+ if (status === "blocked") return '<span class="cf-badge cf-badge-danger">blocked</span>';
482
+ if (status === "analyzing") return '<span class="cf-badge cf-badge-coral">analyzing</span>';
483
+ return '<span class="cf-badge cf-badge-neutral">queued</span>';
484
+ }
485
+ function reqIcon(status) {
486
+ if (status === "analyzing") return '<span class="spinner"></span>';
487
+ if (status === "analyzed") return '<span class="ok">✓</span>';
488
+ if (status === "blocked") return '<span class="bad">✗</span>';
489
+ return '<span class="idle">○</span>';
490
+ }
491
+ function renderAnalysisPage(state) {
492
+ var done = state.counts.analyzed + state.counts.blocked;
493
+ var pct = state.counts.total ? Math.round((done / state.counts.total) * 100) : 0;
494
+ var rows = state.requirements.map(function (r, i) {
495
+ var key = "req:" + r.extId;
496
+ var cls = ["req", r.status];
497
+ if (r.detail) cls.push("has-detail");
498
+ if (open[key]) cls.push("open");
499
+ var detail = "";
500
+ if (r.detail) {
501
+ detail = '<div class="detail">' +
502
+ (r.detail.summary ? '<div class="summary">' + esc(r.detail.summary) + "</div>" : "") +
503
+ r.detail.components.map(function (c) {
504
+ return '<div class="comp"><code>' + esc(c.typeName + ":" + c.fullName) + "</code>" +
505
+ '<span class="intent">' + esc(c.intent || "") + "</span></div>";
506
+ }).join("") +
507
+ r.detail.blockers.map(function (b) {
508
+ var t = typeof b === "string" ? b : (b.question || b.summary || b.id || JSON.stringify(b));
509
+ return '<div class="issue issue-danger">' + esc(t) + "</div>";
510
+ }).join("") +
511
+ r.detail.assumptions.slice(0, 4).map(function (a) {
512
+ var t = typeof a === "string" ? a : (a.summary || a.text || a.id || JSON.stringify(a));
513
+ return '<div class="issue issue-warning">' + esc(t) + "</div>";
514
+ }).join("") + "</div>";
515
+ }
516
+ return '<div class="' + cls.join(" ") + '" data-key="' + key + '" style="animation-delay:' + Math.min(i * 0.04, 0.6) + 's">' +
517
+ '<div class="req-row">' +
518
+ '<span class="icon">' + reqIcon(r.status) + "</span>" +
519
+ '<span class="id">' + esc(r.extId) + "</span>" +
520
+ '<span class="title" title="' + esc(r.title) + '">' + esc(r.title) + "</span>" +
521
+ reqBadge(r.status) + "</div>" + detail + "</div>";
522
+ }).join("");
523
+ document.getElementById("page-analysis").innerHTML =
524
+ narration(state, ["partitioning", "parallel-analysis"]) +
525
+ '<div class="cf-card"><div class="cf-card-header">Analysis<span class="count">' +
526
+ state.counts.analyzed + "/" + state.counts.total + " planned" +
527
+ (state.counts.blocked ? " · " + state.counts.blocked + " blocked" : "") +
528
+ '</span></div><div style="padding-top:0.8rem"></div>' +
529
+ '<div class="cf-progress"><div class="cf-progress-bar' + (pct >= 100 ? " complete" : "") + '" style="width:' + pct + '%"></div></div>' +
530
+ (rows || '<div class="empty">Waiting for the analyzers to start.</div>') + "</div>";
531
+ }
532
+
533
+ // ── page 2: clarifications (three variants, all rendered) ─────────────────
534
+ function answerOf(q) {
535
+ if (q.answer && q.answer.chosenLabels && q.answer.chosenLabels.length) return q.answer.chosenLabels.join(", ");
536
+ if (typeof q.answer === "string") return q.answer;
537
+ return null;
538
+ }
539
+
540
+ function renderClarify(state) {
541
+ document.getElementById("clarify-nar").innerHTML = narration(state, ["clarification"]);
542
+ var out;
543
+ if (!state.questions.length) {
544
+ out = placeholder(chapterByName(state, "clarification")?.status === "skipped"
545
+ ? "No clarification needed — nothing was genuinely ambiguous."
546
+ : "No questions yet. Rounds appear here as the run raises them.");
547
+ } else {
548
+ // One decision card per question: header + round stamp + answered badge, truncated
549
+ // question with a "more" expander, all options as chips with the chosen one checked.
550
+ out = '<div class="cf-card"><div class="cf-card-header">Decisions<span class="count">' +
551
+ state.questions.reduce(function (n, r) { return n + r.questions.length; }, 0) + " questions · " +
552
+ state.questions.length + " rounds</span></div>" +
553
+ state.questions.map(function (round) {
554
+ return round.questions.map(function (q, i) {
555
+ var key = "qa:" + round.round + ":" + i;
556
+ var ans = answerOf(q);
557
+ var chips = (q.options || []).map(function (o) {
558
+ var chosen = ans && ans.indexOf(o.label) >= 0;
559
+ return '<span class="chip' + (chosen ? " chosen" : "") + '" title="' + esc(o.description || "") + '">' + esc(o.label) + "</span>";
560
+ }).join("");
561
+ return '<div class="dcard' + (open[key] ? " open" : "") + '" data-key="' + key + '">' +
562
+ '<div class="dcard-head"><span class="h">' + esc(q.header || "Question") + "</span>" +
563
+ '<span class="rnd">Round ' + round.round + " · " + when(round.askedAt) + "</span>" +
564
+ (ans ? '<span class="cf-badge cf-badge-success">answered</span>' : '<span class="cf-badge cf-badge-warning">waiting</span>') +
565
+ "</div>" +
566
+ '<div class="dcard-q">' + esc(q.question) + "</div>" +
567
+ '<div class="dcard-opts">' + (chips || (ans ? '<span class="chip chosen">' + esc(ans) + "</span>" : "")) + "</div></div>";
568
+ }).join("");
569
+ }).join("") + '<div style="height:0.4rem"></div></div>';
570
+ }
571
+ document.getElementById("clarify-a").innerHTML = out;
572
+ }
573
+
574
+ // ── page 3: plan & approval ───────────────────────────────────────────────
575
+ function renderPlanPage(state) {
576
+ var el = document.getElementById("page-plan");
577
+ var nar = narration(state, ["scope-approval"]);
578
+ var planned = state.requirements.filter(function (r) { return r.detail; });
579
+ if (!planned.length) {
580
+ el.innerHTML = nar + placeholder("The scope sheet appears once analysis has produced plans.");
581
+ return;
582
+ }
583
+ var approved = (chapterByName(state, "scope-approval") || {}).status === "done";
584
+ var nComp = 0, nAssume = 0, nBlock = 0;
585
+ planned.forEach(function (r) {
586
+ nComp += r.detail.components.length;
587
+ nAssume += r.detail.assumptions.length;
588
+ nBlock += r.detail.blockers.length;
589
+ });
590
+ el.innerHTML = nar +
591
+ '<div class="cf-card"><div class="cf-card-header">Scope sheet' +
592
+ (approved ? '<span class="cf-badge cf-badge-success">approved by operator</span>' : "") +
593
+ '<span class="count">' + planned.length + " requirements</span></div>" +
594
+ '<div class="scope-stat">' +
595
+ '<div class="cell"><div class="n">' + nComp + '</div><div class="l">components</div></div>' +
596
+ '<div class="cell"><div class="n">' + nAssume + '</div><div class="l">assumptions</div></div>' +
597
+ '<div class="cell"><div class="n' + (nBlock ? '" style="color:var(--cf-danger-text)' : "") + '">' + nBlock + '</div><div class="l">open blockers</div></div>' +
598
+ "</div>" +
599
+ planned.map(function (r) {
600
+ var key = "scope:" + r.extId;
601
+ return '<div class="req has-detail' + (open[key] ? " open" : "") + '" data-key="' + key + '">' +
602
+ '<div class="req-row">' +
603
+ '<span class="id">' + esc(r.extId) + "</span>" +
604
+ '<span class="title">' + esc(r.title) + "</span>" +
605
+ (approved ? '<span class="cf-badge cf-badge-success">in approved scope</span>'
606
+ : '<span class="cf-badge cf-badge-neutral">' + r.detail.components.length + " comp</span>") +
607
+ "</div>" +
608
+ '<div class="detail">' +
609
+ r.detail.components.map(function (c) {
610
+ return '<div class="comp"><code>' + esc(c.typeName + ":" + c.fullName) + "</code>" +
611
+ '<span class="intent">' + esc(c.intent || "") + "</span></div>";
612
+ }).join("") + "</div></div>";
613
+ }).join("") + "</div>";
614
+ }
615
+
616
+ // ── page 4: delivery ──────────────────────────────────────────────────────
617
+ function waveBadge(w) {
618
+ if (w.status !== "done") return '<span class="cf-badge cf-badge-coral"><span class="spinner"></span>' + esc(w.status) + "</span>";
619
+ var v = w.verdict || "";
620
+ var label = v.toLowerCase().replace(/_/g, " ");
621
+ if (v === "DIFF_VERIFIED") return '<span class="cf-badge cf-badge-success">verified</span>';
622
+ if (v.indexOf("DELIVERED") === 0) return '<span class="cf-badge cf-badge-success">' + esc(label) + "</span>";
623
+ return '<span class="cf-badge cf-badge-danger">' + esc(label) + "</span>";
624
+ }
625
+ function renderDeliveryPage(state) {
626
+ var el = document.getElementById("page-delivery");
627
+ var nar = narration(state, ["delivery", "review-results"]);
628
+ if (!state.waves.length) {
629
+ el.innerHTML = nar + placeholder("Waves appear here as delivery assembles and deploys them.");
630
+ return;
631
+ }
632
+ var done = state.waves.filter(function (w) { return w.status === "done"; }).length;
633
+ el.innerHTML = nar +
634
+ '<div class="cf-card"><div class="cf-card-header">Delivery waves<span class="count">' + done + "/" + state.waves.length + "</span></div>" +
635
+ state.waves.map(function (w) {
636
+ var key = "wave:" + w.name;
637
+ var meta = [];
638
+ if (w.components.length) meta.push(w.components.length + " components");
639
+ if (w.requirementIds.length) meta.push(w.requirementIds.join(", "));
640
+ if (w.diff) meta.push("diff +" + w.diff.added + " −" + w.diff.removed + " ~" + w.diff.changed);
641
+ return '<div class="wave' + (w.components.length ? " has-comps" : "") + (open[key] ? " open" : "") + '" data-key="' + key + '">' +
642
+ '<div class="wave-head"><span class="name">' + esc(w.name) + "</span>" + waveBadge(w) + "</div>" +
643
+ (meta.length ? '<div class="meta">' + esc(meta.join(" · ")) + "</div>" : "") +
644
+ w.failures.map(function (f) {
645
+ var t = typeof f === "string" ? f : ((f.fullName || "") + " " + (f.problem || f.message || JSON.stringify(f)));
646
+ return '<div class="issue issue-danger">' + esc(t) + "</div>";
647
+ }).join("") +
648
+ '<div class="comps">' + w.components.map(function (c) {
649
+ return '<div class="comp"><code>' + esc(c) + "</code></div>";
650
+ }).join("") + "</div></div>";
651
+ }).join("") + "</div>";
652
+ }
653
+
654
+ // ── page 5: results (writeback states + outcome cards + report) ───────────
655
+ var OUTCOME = {
656
+ "delivered": { cls: "rc-success", badge: "cf-badge-success", label: "Delivered", delivered: true },
657
+ "drift-reconciled": { cls: "rc-success", badge: "cf-badge-success", label: "Delivered · drift reconciled", delivered: true },
658
+ "drift-pending": { cls: "rc-warning", badge: "cf-badge-warning", label: "Delivered · drift pending review", delivered: true },
659
+ "problem": { cls: "rc-danger", badge: "cf-badge-danger", label: "Problem" },
660
+ "delivering": { cls: "rc-info", badge: "cf-badge-info", label: "Delivering…" },
661
+ "blocked": { cls: "rc-danger", badge: "cf-badge-danger", label: "Blocked" },
662
+ "not-delivered": { cls: "rc-neutral", badge: "cf-badge-neutral", label: "Not delivered" }
663
+ };
664
+ // Writeback phase, derived from what exists on disk. The FALLBACK: within a phase all rows move
665
+ // together, which is all the chapter ledger can say. A session whose writeback has started
666
+ // carries `r.writeback` per requirement (from writeback/manifest.json) and overrides this.
667
+ function writebackPhase(state) {
668
+ if (state.report) return "done";
669
+ if (state.head === "writeback-close") return "writing";
670
+ var rr = chapterByName(state, "review-results");
671
+ if (rr && rr.status === "done") return "ready";
672
+ return "not-yet";
673
+ }
674
+ // Per-requirement manifest status -> the same four render states the phase produces. `pending`
675
+ // means the requirement has nothing writable this pass (every row held or skipped), so it reads
676
+ // as awaiting the operator rather than as in flight.
677
+ var WB_STATUS = { writing: "writing", written: "done", failed: "failed", pending: "ready" };
678
+ function renderResultsPage(state) {
679
+ var el = document.getElementById("page-results");
680
+ var nar = narration(state, ["writeback-close"]);
681
+ var results = state.results || [];
682
+ if (!state.waves.length && !state.report) {
683
+ el.innerHTML = nar + placeholder("Results land here after delivery: outcomes, Campfire writeback, then the batch report.");
684
+ return;
685
+ }
686
+ var counts = { delivered: 0, pending: 0, bad: 0, reconciled: 0, clean: 0 };
687
+ results.forEach(function (r) {
688
+ var o = OUTCOME[r.outcome];
689
+ if (o && o.delivered) {
690
+ counts.delivered++;
691
+ if (r.outcome === "drift-reconciled") counts.reconciled++;
692
+ else if (r.outcome === "drift-pending") counts.pending++;
693
+ else counts.clean++;
694
+ } else if (r.outcome === "problem" || r.outcome === "blocked") counts.bad++;
695
+ });
696
+ var total = results.length;
697
+ var pct = total ? Math.round((counts.delivered / total) * 100) : 0;
698
+ var subBits = [];
699
+ if (counts.clean) subBits.push(counts.clean + " clean");
700
+ if (counts.reconciled) subBits.push(counts.reconciled + " with drift reconciled");
701
+ if (counts.pending) subBits.push(counts.pending + " awaiting drift review");
702
+ if (counts.bad) subBits.push(counts.bad + " needing attention");
703
+
704
+ var hero =
705
+ '<div class="hero"><div style="flex:1">' +
706
+ '<div class="big">' + counts.delivered + " of " + total + " requirements delivered (" + pct + "%)</div>" +
707
+ '<div class="sub">' + subBits.join(" · ") + "</div></div></div>" +
708
+ '<div class="cf-progress" style="margin-top:0.6rem"><div class="cf-progress-bar' + (pct >= 100 && !counts.pending ? " complete" : "") + '" style="width:' + pct + '%"></div></div>';
709
+
710
+ var cells =
711
+ '<div class="res-grid">' +
712
+ '<div class="summary-cell summary-cell--success"><div class="cell-number">' + counts.delivered + '</div>' +
713
+ '<div class="cell-label">Delivered</div><div class="cell-sub">' +
714
+ (counts.reconciled ? "incl. " + counts.reconciled + " drift reconciled" : "verified in org") + '</div></div>' +
715
+ '<div class="summary-cell summary-cell--warning"><div class="cell-number">' + counts.pending + '</div>' +
716
+ '<div class="cell-label">Pending review</div><div class="cell-sub">delivered, drift unreviewed</div></div>' +
717
+ '<div class="summary-cell summary-cell--danger"><div class="cell-number">' + counts.bad + '</div>' +
718
+ '<div class="cell-label">Attention</div><div class="cell-sub">problem or blocked</div></div></div>';
719
+
720
+ // Per-requirement cards. Each expands to its action breakdown — every component with what
721
+ // was done to it, the wave that carried it, and how that ended — plus the requirement's own
722
+ // Campfire writeback row. One card is the whole story of one requirement.
723
+ var wb = writebackPhase(state);
724
+ var ACTION_ICON = {
725
+ "verified": '<span class="ok">✓</span>',
726
+ "deploying": '<span class="spinner"></span>',
727
+ "failed": '<span class="bad">✗</span>',
728
+ "manual": '<span class="idle">✋</span>',
729
+ "not-deployed": '<span class="idle">○</span>'
730
+ };
731
+ var cards = results.map(function (r) {
732
+ var o = OUTCOME[r.outcome] || OUTCOME["not-delivered"];
733
+ var key = "res:" + r.extId;
734
+ var sub = [];
735
+ if (r.components) sub.push(r.components + (r.components === 1 ? " component" : " components"));
736
+ if (r.waves.length) sub.push(r.waves.join(", "));
737
+ if (r.assumptions) sub.push(r.assumptions + " assumptions");
738
+
739
+ var actions = (r.actions || []).map(function (a) {
740
+ return '<div class="action">' +
741
+ '<span class="aicon">' + (ACTION_ICON[a.status] || ACTION_ICON["not-deployed"]) + "</span>" +
742
+ "<code>" + esc(a.component) + "</code>" +
743
+ (a.intent ? '<span class="aintent">' + esc(a.intent) + "</span>" : "") +
744
+ '<span class="awave">' + esc(a.wave || (a.status === "manual" ? "manual step" : "not in a wave")) + "</span></div>";
745
+ }).join("");
746
+
747
+ var wbRow = "";
748
+ if (OUTCOME[r.outcome] && OUTCOME[r.outcome].delivered) {
749
+ // This requirement's own state when the manifest names it; the session-wide phase otherwise.
750
+ var rwb = (r.writeback && WB_STATUS[r.writeback.status]) || wb;
751
+ var n = r.writeback ? r.writeback.ciCount : 0;
752
+ var wicon, wbadge;
753
+ if (rwb === "done") { wicon = '<span class="ok">✓</span>'; wbadge = '<span class="cf-badge cf-badge-success">written back' + (n ? " · " + n + " item" + (n === 1 ? "" : "s") : "") + "</span>"; }
754
+ else if (rwb === "writing") { wicon = '<span class="spinner"></span>'; wbadge = '<span class="cf-badge cf-badge-coral">writing…</span>'; }
755
+ else if (rwb === "failed") { wicon = '<span class="bad">✗</span>'; wbadge = '<span class="cf-badge cf-badge-danger">writeback failed</span>'; }
756
+ else if (rwb === "ready") { wicon = '<span class="idle">○</span>'; wbadge = '<span class="cf-badge cf-badge-warning">awaiting approval</span>'; }
757
+ else { wicon = '<span class="idle">○</span>'; wbadge = '<span class="cf-badge cf-badge-neutral">after review</span>'; }
758
+ wbRow = '<div class="wbrow"><span class="aicon">' + wicon + "</span>" +
759
+ '<span class="wlabel">Campfire writeback</span>' + wbadge + "</div>";
760
+ }
761
+
762
+ return '<div class="result-card ' + o.cls + (open[key] ? " open" : "") + '" data-key="' + key + '">' +
763
+ '<div class="result-head"><span class="caret">▶</span><span class="rid">' + esc(r.extId) + '</span>' +
764
+ '<span class="rtitle" title="' + esc(r.title) + '">' + esc(r.title) + "</span>" +
765
+ '<span class="cf-badge ' + o.badge + '">' + o.label + "</span></div>" +
766
+ (sub.length ? '<div class="result-sub">' + esc(sub.join(" · ")) + "</div>" : "") +
767
+ '<div class="result-body">' + actions + wbRow + "</div></div>";
768
+ }).join("");
769
+
770
+ var wbHeaderBadge =
771
+ wb === "done" ? '<span class="cf-badge cf-badge-success">writeback complete</span>'
772
+ : wb === "writing" ? '<span class="cf-badge cf-badge-coral">writeback in progress</span>'
773
+ : wb === "ready" ? '<span class="cf-badge cf-badge-warning">writeback awaiting approval</span>'
774
+ : "";
775
+
776
+ var report = "";
777
+ if (state.report) {
778
+ var rp = state.report;
779
+ report =
780
+ '<div class="cf-card"><div class="cf-card-header cf-dark">Batch report' +
781
+ '<span class="count">' + when(rp.generatedAt) + "</span></div>" +
782
+ '<div style="padding:0.8rem 1rem 0.2rem;font-size:0.82rem"><strong>' + esc(rp.result) + "</strong> — " +
783
+ rp.deliveredWaves + " wave" + (rp.deliveredWaves === 1 ? "" : "s") + " delivered" +
784
+ (rp.problemWaves ? ", " + rp.problemWaves + " problem" : "") + "</div>" +
785
+ '<div class="findings">' + (rp.findings || []).map(function (f) { return "• " + esc(f); }).join("<br>") + "</div></div>";
786
+ }
787
+
788
+ el.innerHTML = nar +
789
+ '<div class="cf-card"><div class="cf-card-header">Batch results' + wbHeaderBadge +
790
+ '<span class="count">' + total + " requirements</span></div>" +
791
+ hero + cells +
792
+ '<div class="cf-label" style="padding:0.2rem 1rem 0.5rem">Requirement results</div>' +
793
+ cards + '<div style="height:0.6rem"></div></div>' +
794
+ report;
795
+ }
796
+
797
+ // ── paging ────────────────────────────────────────────────────────────────
798
+ function goTo(idx, user) {
799
+ pageIdx = Math.max(0, Math.min(idx, PAGES.length - 1));
800
+ if (user) {
801
+ var liveIdx = lastState ? STEPS[stepOfChapter(lastState.head)].page : 0;
802
+ follow = pageIdx === liveIdx;
803
+ }
804
+ document.getElementById("follow").classList.toggle("on", follow);
805
+ var track = document.getElementById("track");
806
+ track.classList.remove("settled");
807
+ var pages = track.children;
808
+ for (var i = 0; i < pages.length; i++) pages[i].classList.toggle("current", i === pageIdx);
809
+ track.style.transform = "translateX(-" + pageIdx * 100 + "%)";
810
+ clearTimeout(goTo._settle);
811
+ goTo._settle = setTimeout(function () { track.classList.add("settled"); }, 380);
812
+ document.getElementById("pg-title").textContent = PAGES[pageIdx].title;
813
+ document.getElementById("pg-sub").textContent = PAGES[pageIdx].sub;
814
+ document.getElementById("prev").disabled = pageIdx === 0;
815
+ document.getElementById("next").disabled = pageIdx === PAGES.length - 1;
816
+ if (lastState) renderSteppers(lastState);
817
+ }
818
+
819
+ document.getElementById("prev").onclick = function () { goTo(pageIdx - 1, true); };
820
+ document.getElementById("next").onclick = function () { goTo(pageIdx + 1, true); };
821
+ document.getElementById("follow").onclick = function () {
822
+ follow = true;
823
+ if (lastState) goTo(STEPS[stepOfChapter(lastState.head)].page, false);
824
+ document.getElementById("follow").classList.add("on");
825
+ };
826
+ document.querySelector(".stepper-slot").addEventListener("click", function (ev) {
827
+ var t = ev.target.closest("[data-page]");
828
+ if (t) goTo(Number(t.getAttribute("data-page")), true);
829
+ });
830
+ document.addEventListener("click", function (ev) {
831
+ var t = ev.target.closest("[data-key]");
832
+ if (!t) return;
833
+ if (!/has-detail|has-comps|narration|dcard|result-card/.test(t.className)) return;
834
+ var key = t.getAttribute("data-key");
835
+ open[key] = !open[key];
836
+ t.classList.toggle("open", !!open[key]);
837
+ });
838
+
839
+ // ── data loop ─────────────────────────────────────────────────────────────
840
+ function render(state) {
841
+ lastState = state;
842
+ document.getElementById("org-line").textContent =
843
+ (state.org.targetAlias || state.org.targetOrgId || "") +
844
+ (state.org.operator ? " · " + state.org.operator : "");
845
+ if (follow) {
846
+ var liveIdx = STEPS[stepOfChapter(state.head)].page;
847
+ if (liveIdx !== pageIdx) goTo(liveIdx, false);
848
+ }
849
+ renderSteppers(state);
850
+ renderStartPage(state);
851
+ renderAnalysisPage(state);
852
+ renderClarify(state);
853
+ renderPlanPage(state);
854
+ renderDeliveryPage(state);
855
+ renderResultsPage(state);
856
+ }
857
+
858
+ function populatePicker() {
859
+ fetch("/api/sessions").then(function (r) { return r.json(); }).then(function (d) {
860
+ var sel = document.getElementById("session-picker");
861
+ sel.innerHTML = d.sessions.map(function (s) {
862
+ var label = s.sessionId.slice(0, 12) + "… · " + (s.targetAlias || "?") + " · " + s.requirements + " reqs" + (s.hasReport ? " ✓" : "");
863
+ return '<option value="' + s.sessionId + '">' + esc(label) + "</option>";
864
+ }).join("");
865
+ if (currentSession) sel.value = currentSession;
866
+ else if (d.sessions.length) { currentSession = d.sessions[0].sessionId; sel.value = currentSession; }
867
+ sel.onchange = function () {
868
+ currentSession = sel.value;
869
+ open = {}; follow = true;
870
+ history.replaceState(null, "", "?session=" + currentSession);
871
+ tick();
872
+ };
873
+ });
874
+ }
875
+
876
+ function tick() {
877
+ var q = currentSession ? "?session=" + encodeURIComponent(currentSession) : "";
878
+ fetch("/api/state" + q)
879
+ .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
880
+ .then(function (state) {
881
+ failCount = 0;
882
+ document.getElementById("live-dot").classList.remove("stale");
883
+ if (!currentSession) currentSession = state.sessionId;
884
+ render(state);
885
+ })
886
+ .catch(function () {
887
+ failCount++;
888
+ if (failCount >= 3) document.getElementById("live-dot").classList.add("stale");
889
+ });
890
+ }
891
+
892
+ goTo(0, false);
893
+ populatePicker();
894
+ tick();
895
+ setInterval(tick, 2000);
896
+ })();
897
+ </script>
898
+ </body>
899
+ </html>