@etiquekit/etq 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/AGENTS.md +19 -26
  2. package/LICENSE +2 -3
  3. package/NOTICE +6 -4
  4. package/QuickStart.md +35 -47
  5. package/README.md +54 -54
  6. package/bin/etiquette +1 -5
  7. package/bin/etiquette-core +1 -5
  8. package/docs/ARCHITECTURE.md +5 -5
  9. package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
  10. package/docs/CONCEPTS.md +7 -23
  11. package/docs/CONCEPT_STATUS.md +66 -0
  12. package/docs/CORE_PROFILE.md +3 -5
  13. package/docs/LANE_PROVISIONING.md +7 -2
  14. package/docs/README.md +4 -4
  15. package/docs/RELEASE_SURFACE_AUDIT.md +8 -7
  16. package/docs/SEAT_DISCIPLINE.md +55 -91
  17. package/docs/SEAT_PROVISIONING.md +19 -22
  18. package/docs/TEAM_HANDOFF.md +21 -21
  19. package/docs/WORKTREE_QOL.md +7 -6
  20. package/docs/contracts/ledger-entry/README.md +11 -11
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
  22. package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
  23. package/package.json +9 -9
  24. package/packages/control/src/authority/lease.ts +261 -0
  25. package/packages/control/src/authority/node-delegation.ts +257 -0
  26. package/packages/control/src/authority/rig-conductor.ts +632 -0
  27. package/packages/control/src/cli/argv.ts +155 -0
  28. package/packages/control/src/cli/commands/console.ts +200 -0
  29. package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
  30. package/packages/control/src/cli/commands/dispatch.ts +279 -0
  31. package/packages/control/src/cli/commands/harness.ts +89 -0
  32. package/packages/control/src/cli/commands/hook.ts +50 -0
  33. package/packages/control/src/cli/commands/ledger.ts +91 -0
  34. package/packages/control/src/cli/commands/local-workflow.ts +4690 -0
  35. package/packages/control/src/cli/commands/memory.ts +445 -0
  36. package/packages/control/src/cli/commands/release.ts +108 -0
  37. package/packages/control/src/cli/commands/rubric.ts +103 -0
  38. package/packages/control/src/cli/commands/seat.ts +179 -0
  39. package/packages/control/src/cli/commands/session.ts +127 -0
  40. package/packages/control/src/cli/commands/supervision.ts +246 -0
  41. package/packages/control/src/cli/commands/sync.ts +119 -0
  42. package/packages/control/src/cli/commands/workflow.ts +86 -0
  43. package/packages/control/src/cli/commands/workspace.ts +50 -0
  44. package/packages/control/src/cli/core-usage.ts +34 -0
  45. package/packages/control/src/cli/prompt.ts +67 -0
  46. package/packages/control/src/cli/supervision-deps.ts +44 -0
  47. package/packages/control/src/cli/usage.ts +241 -0
  48. package/packages/control/src/cli.ts +207 -0
  49. package/packages/control/src/core-cli.ts +50 -0
  50. package/packages/control/src/dispatch/decision.ts +202 -0
  51. package/packages/control/src/dispatch/projection.ts +293 -0
  52. package/packages/control/src/dispatch/record.ts +153 -0
  53. package/packages/control/src/engagement/project.ts +170 -0
  54. package/packages/control/src/fs.ts +19 -0
  55. package/packages/control/src/harness/pruning.ts +406 -0
  56. package/packages/control/src/hooks/dispatcher.ts +117 -0
  57. package/packages/control/src/hooks/outbox.ts +86 -0
  58. package/packages/control/src/hooks/sanitize.ts +6 -0
  59. package/packages/control/src/hooks/types.ts +34 -0
  60. package/packages/control/src/index.ts +384 -0
  61. package/packages/control/src/ledger/entry.ts +303 -0
  62. package/packages/control/src/ledger/indexer.ts +542 -0
  63. package/packages/control/src/memory/context.ts +149 -0
  64. package/packages/control/src/memory/drain-import.ts +207 -0
  65. package/packages/control/src/memory/indexer.ts +284 -0
  66. package/packages/control/src/memory/query.ts +75 -0
  67. package/packages/control/src/memory/sanitize.ts +50 -0
  68. package/packages/control/src/memory/sharded-drain-import.ts +212 -0
  69. package/packages/control/src/memory/status.ts +211 -0
  70. package/packages/control/src/memory/store-lifecycle.ts +509 -0
  71. package/packages/control/src/memory/store.ts +284 -0
  72. package/packages/control/src/memory/types.ts +146 -0
  73. package/packages/control/src/parity/surfaces.ts +748 -0
  74. package/packages/control/src/project.ts +141 -0
  75. package/packages/control/src/projection/local-ledger-view.ts +373 -0
  76. package/packages/control/src/projection/return-enforcement.ts +48 -0
  77. package/packages/control/src/projection/timeline-preview.ts +539 -0
  78. package/packages/control/src/projection/timeline.ts +708 -0
  79. package/packages/control/src/release/readiness.ts +842 -0
  80. package/packages/control/src/rubric/loader.ts +326 -0
  81. package/packages/control/src/rubric/promotion.ts +54 -0
  82. package/packages/control/src/rubric/runner.ts +159 -0
  83. package/packages/control/src/rubric/types.ts +158 -0
  84. package/packages/control/src/seat/owner-card.ts +388 -0
  85. package/packages/control/src/seat/readiness.ts +834 -0
  86. package/packages/control/src/session/runbook.ts +431 -0
  87. package/packages/control/src/shared/sanitize.ts +49 -0
  88. package/packages/control/src/supervision/action-classes.ts +192 -0
  89. package/packages/control/src/supervision/command-apply.ts +378 -0
  90. package/packages/control/src/supervision/errors.ts +14 -0
  91. package/packages/control/src/supervision/event-replay.ts +155 -0
  92. package/packages/control/src/supervision/events.ts +109 -0
  93. package/packages/control/src/supervision/index.ts +16 -0
  94. package/packages/control/src/supervision/manifest.ts +127 -0
  95. package/packages/control/src/supervision/paths.ts +49 -0
  96. package/packages/control/src/supervision/projection-adapter.ts +274 -0
  97. package/packages/control/src/supervision/projection.ts +75 -0
  98. package/packages/control/src/supervision/rebuild.ts +99 -0
  99. package/packages/control/src/supervision/session-open.ts +131 -0
  100. package/packages/control/src/supervision/session-read.ts +99 -0
  101. package/packages/control/src/supervision/sqlite-impl.ts +71 -0
  102. package/packages/control/src/supervision/sqlite.ts +121 -0
  103. package/packages/control/src/supervision/store-rows.ts +371 -0
  104. package/packages/control/src/supervision/turn-close.ts +154 -0
  105. package/packages/control/src/supervision/turn-open.ts +284 -0
  106. package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
  107. package/packages/control/src/sync/join-plan.ts +375 -0
  108. package/packages/control/src/sync/outbox.ts +492 -0
  109. package/packages/control/src/workflow/evaluator.ts +140 -0
  110. package/packages/control/src/workflow/loader.ts +200 -0
  111. package/packages/control/src/workflow/types.ts +90 -0
  112. package/packages/control/src/workspace/authority.ts +499 -0
  113. package/packages/protocol/src/guards.ts +119 -0
  114. package/packages/protocol/src/huddle-board.ts +198 -0
  115. package/packages/protocol/src/huddle.ts +295 -0
  116. package/packages/protocol/src/incident.ts +251 -0
  117. package/packages/protocol/src/index.ts +8 -0
  118. package/packages/protocol/src/interfaces.ts +107 -0
  119. package/packages/protocol/src/packet-profile.ts +195 -0
  120. package/packages/protocol/src/state.ts +81 -0
  121. package/packages/protocol/src/types.ts +434 -0
  122. package/release/lineage.v0.json +15 -0
  123. package/scripts/release-candidate-verify.sh +175 -0
  124. package/scripts/release-checksum.sh +25 -0
  125. package/scripts/release-pack-canary.sh +97 -0
  126. package/scripts/release-scan.sh +249 -0
  127. package/scripts/release-sign.sh +34 -0
  128. package/templates/etiquette-vanilla-v0/README.md +2 -3
  129. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
  130. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
  131. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
  132. package/templates/seat-packs-v0/README.md +3 -2
  133. package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
  134. package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
  135. package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
  136. package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
  137. package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
  138. package/docs/AGENT_SPAWNING.md +0 -78
  139. package/docs/INTEGRATION_UI.md +0 -215
  140. package/docs/OPERATOR_PLAYBOOK.md +0 -77
  141. package/docs/TAG_ROUTE.md +0 -95
  142. package/lib/etiquette-core.js +0 -315
  143. package/lib/etiquette.js +0 -1160
  144. package/templates/DISPATCH_PREMISES.md +0 -39
  145. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
  146. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
  147. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
  148. package/templates/hosted-receiver/README.md +0 -41
  149. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
@@ -0,0 +1,539 @@
1
+ import { applyTimelineDeepLink, type TimelineFeedItem, type TimelineItemType, type TimelineView } from './timeline';
2
+
3
+ export const TIMELINE_PREVIEW_ALLOWED_METHODS = ['GET', 'HEAD', 'OPTIONS'] as const;
4
+
5
+ function escapeHtml(value: string): string {
6
+ return value
7
+ .replace(/&/g, '&')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;')
11
+ .replace(/'/g, '&#39;');
12
+ }
13
+
14
+ function titleCase(value: string): string {
15
+ return value.charAt(0).toUpperCase() + value.slice(1);
16
+ }
17
+
18
+ function itemBadge(itemType: TimelineItemType): string {
19
+ switch (itemType) {
20
+ case 'task_card':
21
+ return 'TASK';
22
+ case 'return':
23
+ return 'RETURN';
24
+ case 'receipt':
25
+ return 'RECEIPT';
26
+ case 'decision':
27
+ return 'DECISION';
28
+ case 'chatter':
29
+ return 'CHATTER';
30
+ }
31
+ }
32
+
33
+ function itemTone(itemType: TimelineItemType): string {
34
+ switch (itemType) {
35
+ case 'task_card':
36
+ return 'task';
37
+ case 'return':
38
+ return 'return';
39
+ case 'receipt':
40
+ return 'receipt';
41
+ case 'decision':
42
+ return 'decision';
43
+ case 'chatter':
44
+ return 'chatter';
45
+ }
46
+ }
47
+
48
+ function sourceRef(item: TimelineFeedItem): string {
49
+ return item.source.event_ref ?? item.source.packet_ref ?? 'unsequenced';
50
+ }
51
+
52
+ function participants(item: TimelineFeedItem): string {
53
+ const parts = [
54
+ item.actor_seat_id ? `from ${item.actor_seat_id}` : null,
55
+ item.seat_id ? `seat ${item.seat_id}` : null,
56
+ item.operator_principal ? `operator ${item.operator_principal}` : null,
57
+ ].filter((part): part is string => Boolean(part));
58
+ return parts.length > 0 ? parts.join(' / ') : 'seat unresolved';
59
+ }
60
+
61
+ function chip(label: string, value: string | null, className = ''): string {
62
+ return value ? `<span class="chip ${escapeHtml(className)}"><strong>${escapeHtml(label)}</strong>${escapeHtml(value)}</span>` : '';
63
+ }
64
+
65
+ function renderSections(item: TimelineFeedItem): string {
66
+ return `<div class="sections" role="tablist" aria-label="Card sections">
67
+ ${item.display.sections.map((section, index) => `<section class="card-section" role="tabpanel" aria-labelledby="${escapeHtml(item.item_id)}-tab-${index}">
68
+ <button id="${escapeHtml(item.item_id)}-tab-${index}" type="button" class="section-tab">${escapeHtml(section.label)}</button>
69
+ <p>${escapeHtml(section.body)}</p>
70
+ </section>`).join('\n')}
71
+ </div>`;
72
+ }
73
+
74
+ function renderItem(item: TimelineFeedItem): string {
75
+ const tone = itemTone(item.item_type);
76
+ const seqAnchor = item.source.seq === null ? escapeHtml(item.item_id) : `seq-${String(item.source.seq)}`;
77
+ return `<article id="${seqAnchor}" class="feed-item ${tone}" data-item-type="${escapeHtml(item.item_type)}" data-thread-id="${escapeHtml(item.thread.thread_id)}" data-depth="${String(item.thread.depth)}">
78
+ <div class="item-head">
79
+ <span class="badge ${tone}">${escapeHtml(itemBadge(item.item_type))}</span>
80
+ <time>${escapeHtml(item.created_at ?? 'time unknown')}</time>
81
+ </div>
82
+ <div class="chips" aria-label="Timeline metadata">
83
+ ${chip('seat', item.display.seat_badge, 'seat')}
84
+ ${chip('role', item.display.role_chip, 'role')}
85
+ ${chip('lane', item.display.lane_chip, 'lane')}
86
+ ${chip('verdict', item.display.verdict_pill, 'verdict-pill')}
87
+ ${chip('status', item.display.status_subtitle, 'status')}
88
+ ${item.display.topic_chips.map((topic) => chip('topic', topic, 'topic')).join('')}
89
+ ${item.requires_ack === true ? '<span class="chip ack"><strong>ack</strong>requires_ack</span>' : ''}
90
+ </div>
91
+ <h2>${escapeHtml(item.title)}</h2>
92
+ <p class="body">${escapeHtml(item.body)}</p>
93
+ ${renderSections(item)}
94
+ <dl>
95
+ <div><dt>Source</dt><dd><code>${escapeHtml(sourceRef(item))}</code></dd></div>
96
+ <div><dt>Participants</dt><dd>${escapeHtml(participants(item))}</dd></div>
97
+ ${item.lane ? `<div><dt>Lane</dt><dd><code>${escapeHtml(item.lane)}</code></dd></div>` : ''}
98
+ <div><dt>Thread</dt><dd><code>${escapeHtml(item.thread.thread_id)}</code> - ${String(item.thread.thread_item_count)} item${item.thread.thread_item_count === 1 ? '' : 's'}</dd></div>
99
+ <div><dt>Authority</dt><dd>projection only; cannot execute, write back, grant, close, merge, or sign</dd></div>
100
+ </dl>
101
+ </article>`;
102
+ }
103
+
104
+ function renderThreadIndex(view: TimelineView): string {
105
+ if (view.feed.threads.length === 0) return '<section class="empty">No threads found for this scope.</section>';
106
+ return `<ol class="thread-index">
107
+ ${view.feed.threads.map((thread) => `<li>
108
+ <a href="#${escapeHtml(thread.latest_seq === null ? thread.item_ids[0] : `seq-${thread.latest_seq}`)}">${escapeHtml(thread.title)}</a>
109
+ <span>${String(thread.item_count)} items</span>
110
+ </li>`).join('\n')}
111
+ </ol>`;
112
+ }
113
+
114
+ function renderCuratorPortal(view: TimelineView): string {
115
+ const seats = view.curator_portal.health_board.seats.length > 0
116
+ ? view.curator_portal.health_board.seats.map((seat) => `<tr>
117
+ <td><code>${escapeHtml(seat.seat_id)}</code></td>
118
+ <td>${escapeHtml(seat.lifecycle_state)}</td>
119
+ <td>${seat.ready ? 'ready' : 'not ready'}</td>
120
+ <td>${escapeHtml(seat.source)}</td>
121
+ <td><code>${escapeHtml(seat.sentinel_state_ref)}</code></td>
122
+ </tr>`).join('\n')
123
+ : '<tr><td colspan="5">No seats in this projection.</td></tr>';
124
+ const queue = view.curator_portal.promotion_queue.items.length > 0
125
+ ? view.curator_portal.promotion_queue.items.map((item) => `<li><code>${escapeHtml(item.source_ref)}</code> ${escapeHtml(item.reason)} ${item.next_owner ? `- next ${escapeHtml(item.next_owner)}` : ''}</li>`).join('\n')
126
+ : '<li>No display-only queue items.</li>';
127
+ return `<section class="curator" aria-label="Curator Portal">
128
+ <div class="section-title">
129
+ <h2>Curator Portal</h2>
130
+ <span>display-only</span>
131
+ </div>
132
+ <div class="curator-grid">
133
+ <section>
134
+ <h3>Health board</h3>
135
+ <table>
136
+ <thead><tr><th>Seat</th><th>Lifecycle</th><th>Ready</th><th>Source</th><th>Sentinel</th></tr></thead>
137
+ <tbody>${seats}</tbody>
138
+ </table>
139
+ </section>
140
+ <section>
141
+ <h3>Digest view</h3>
142
+ <dl>
143
+ <div><dt>Threads</dt><dd>${String(view.curator_portal.digest_view.threads)}</dd></div>
144
+ <div><dt>Requires ack</dt><dd>${String(view.curator_portal.digest_view.requires_ack)}</dd></div>
145
+ <div><dt>Tasks</dt><dd>${String(view.curator_portal.digest_view.items_by_type.task_card)}</dd></div>
146
+ <div><dt>Returns</dt><dd>${String(view.curator_portal.digest_view.items_by_type.return)}</dd></div>
147
+ </dl>
148
+ </section>
149
+ <section>
150
+ <h3>Promotion queue</h3>
151
+ <p>Display-only; cannot promote, grant, close, merge, execute, or write back.</p>
152
+ <ul>${queue}</ul>
153
+ </section>
154
+ </div>
155
+ </section>`;
156
+ }
157
+
158
+ export function renderTimelinePreviewHtml(view: TimelineView): string {
159
+ const items = view.feed.items.length > 0
160
+ ? view.feed.items.map((item) => renderItem(item)).join('\n')
161
+ : '<section class="empty">No timeline items found for this scope.</section>';
162
+ const selected = view.feed.deep_link.item_id ? `Selected ${view.feed.deep_link.event_ref ?? view.feed.deep_link.item_id}` : 'No deep link selected';
163
+ const scopeLabel = titleCase(view.scope);
164
+ return `<!doctype html>
165
+ <html lang="en">
166
+ <head>
167
+ <meta charset="utf-8">
168
+ <meta name="viewport" content="width=device-width, initial-scale=1">
169
+ <title>Etiquette Timeline Preview</title>
170
+ <style>
171
+ :root {
172
+ color-scheme: light;
173
+ --ink: #172026;
174
+ --muted: #5d6973;
175
+ --line: #d7dee5;
176
+ --surface: #ffffff;
177
+ --page: #f6f7f8;
178
+ --teal: #0f766e;
179
+ --amber: #a15c00;
180
+ --red: #b42318;
181
+ --blue: #2563eb;
182
+ --violet: #6d28d9;
183
+ }
184
+ * { box-sizing: border-box; }
185
+ body {
186
+ margin: 0;
187
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
188
+ color: var(--ink);
189
+ background: var(--page);
190
+ }
191
+ main { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 24px 0 40px; }
192
+ header {
193
+ display: grid;
194
+ grid-template-columns: minmax(0, 1fr) auto;
195
+ gap: 16px;
196
+ align-items: end;
197
+ padding: 18px 0 16px;
198
+ border-bottom: 1px solid var(--line);
199
+ }
200
+ h1 { margin: 0; font-size: 28px; line-height: 1.15; letter-spacing: 0; }
201
+ .subhead { margin: 6px 0 0; color: var(--muted); line-height: 1.45; }
202
+ .scope {
203
+ display: inline-flex;
204
+ align-items: center;
205
+ min-height: 32px;
206
+ padding: 6px 10px;
207
+ border-radius: 8px;
208
+ background: #e6f4f1;
209
+ color: #075e57;
210
+ font-weight: 700;
211
+ white-space: nowrap;
212
+ }
213
+ .summary {
214
+ display: grid;
215
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
216
+ gap: 10px;
217
+ margin: 18px 0;
218
+ }
219
+ .metric {
220
+ background: var(--surface);
221
+ border: 1px solid var(--line);
222
+ border-radius: 8px;
223
+ padding: 12px;
224
+ min-width: 0;
225
+ }
226
+ .metric span { display: block; color: var(--muted); font-size: 12px; }
227
+ .metric strong { display: block; margin-top: 4px; font-size: 18px; overflow-wrap: anywhere; }
228
+ .boundary {
229
+ display: grid;
230
+ gap: 8px;
231
+ margin: 18px 0;
232
+ padding: 14px;
233
+ border: 1px solid #f0c36d;
234
+ border-left: 4px solid var(--amber);
235
+ border-radius: 8px;
236
+ background: #fff8eb;
237
+ }
238
+ .boundary strong { color: #7a4100; }
239
+ .view-controls {
240
+ display: flex;
241
+ flex-wrap: wrap;
242
+ align-items: center;
243
+ justify-content: space-between;
244
+ gap: 10px;
245
+ margin: 18px 0 10px;
246
+ }
247
+ .toggle {
248
+ display: inline-flex;
249
+ overflow: hidden;
250
+ border: 1px solid var(--line);
251
+ border-radius: 8px;
252
+ background: var(--surface);
253
+ }
254
+ .toggle a {
255
+ min-height: 34px;
256
+ padding: 8px 12px;
257
+ border-right: 1px solid var(--line);
258
+ color: var(--ink);
259
+ text-decoration: none;
260
+ font-weight: 700;
261
+ }
262
+ .toggle a:last-child { border-right: 0; }
263
+ .toggle a[aria-current="true"] { background: #e6f4f1; color: #075e57; }
264
+ .thread-index {
265
+ display: grid;
266
+ gap: 8px;
267
+ margin: 0 0 18px;
268
+ padding: 0;
269
+ list-style: none;
270
+ }
271
+ .thread-index li {
272
+ display: flex;
273
+ gap: 10px;
274
+ justify-content: space-between;
275
+ padding: 10px 12px;
276
+ border: 1px solid var(--line);
277
+ border-radius: 8px;
278
+ background: var(--surface);
279
+ }
280
+ .thread-index a { color: var(--blue); font-weight: 700; overflow-wrap: anywhere; }
281
+ .feed { display: grid; gap: 12px; margin-top: 18px; }
282
+ .feed-item {
283
+ background: var(--surface);
284
+ border: 1px solid var(--line);
285
+ border-left: 4px solid var(--teal);
286
+ border-radius: 8px;
287
+ padding: 14px;
288
+ }
289
+ .feed-item.return { border-left-color: var(--blue); }
290
+ .feed-item.receipt { border-left-color: var(--violet); }
291
+ .feed-item.decision { border-left-color: var(--red); }
292
+ .feed-item.chatter { border-left-color: #64748b; }
293
+ .item-head { display: flex; gap: 10px; align-items: center; justify-content: space-between; }
294
+ .badge {
295
+ display: inline-flex;
296
+ min-height: 24px;
297
+ align-items: center;
298
+ padding: 3px 8px;
299
+ border-radius: 6px;
300
+ background: #e6f4f1;
301
+ color: #075e57;
302
+ font-size: 12px;
303
+ font-weight: 800;
304
+ }
305
+ .badge.return { background: #eaf1ff; color: #1d4ed8; }
306
+ .badge.receipt { background: #f1eafe; color: #5b21b6; }
307
+ .badge.decision { background: #feeceb; color: #991b1b; }
308
+ .badge.chatter { background: #eef2f5; color: #475569; }
309
+ .chips {
310
+ display: flex;
311
+ flex-wrap: wrap;
312
+ gap: 6px;
313
+ margin-top: 10px;
314
+ }
315
+ .chip {
316
+ display: inline-flex;
317
+ gap: 6px;
318
+ align-items: center;
319
+ min-height: 24px;
320
+ padding: 3px 8px;
321
+ border: 1px solid #d5dde6;
322
+ border-radius: 999px;
323
+ background: #f8fafc;
324
+ color: #334155;
325
+ font-size: 12px;
326
+ }
327
+ .chip strong { color: #64748b; font-weight: 800; }
328
+ .chip.verdict-pill { border-color: #bfd1ff; background: #eaf1ff; color: #1d4ed8; }
329
+ .chip.ack { border-color: #f5c2c0; background: #feeceb; color: #991b1b; }
330
+ time { color: var(--muted); font-size: 12px; text-align: right; }
331
+ h2 { margin: 10px 0 6px; font-size: 17px; letter-spacing: 0; line-height: 1.25; }
332
+ .body { margin: 0 0 12px; color: #334155; overflow-wrap: anywhere; }
333
+ .sections {
334
+ display: grid;
335
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
336
+ gap: 8px;
337
+ margin: 12px 0;
338
+ }
339
+ .card-section {
340
+ padding: 8px;
341
+ border: 1px solid var(--line);
342
+ border-radius: 8px;
343
+ background: #fbfcfd;
344
+ }
345
+ .section-tab {
346
+ border: 0;
347
+ padding: 0;
348
+ background: transparent;
349
+ color: var(--ink);
350
+ font-weight: 800;
351
+ text-align: left;
352
+ }
353
+ .card-section p { margin: 6px 0 0; color: var(--muted); font-size: 13px; line-height: 1.35; }
354
+ dl {
355
+ display: grid;
356
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
357
+ gap: 8px 14px;
358
+ margin: 0;
359
+ }
360
+ dl div { min-width: 0; }
361
+ dt { color: var(--muted); font-size: 12px; }
362
+ dd { margin: 2px 0 0; overflow-wrap: anywhere; }
363
+ code {
364
+ padding: 2px 5px;
365
+ border-radius: 4px;
366
+ background: #eef2f5;
367
+ color: #263238;
368
+ font-size: 12px;
369
+ }
370
+ .empty {
371
+ padding: 20px;
372
+ border: 1px solid var(--line);
373
+ border-radius: 8px;
374
+ background: var(--surface);
375
+ color: var(--muted);
376
+ }
377
+ .curator {
378
+ margin-top: 22px;
379
+ padding-top: 18px;
380
+ border-top: 1px solid var(--line);
381
+ }
382
+ .section-title {
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: space-between;
386
+ gap: 12px;
387
+ margin-bottom: 10px;
388
+ }
389
+ .section-title h2 { margin: 0; }
390
+ .section-title span {
391
+ padding: 4px 8px;
392
+ border-radius: 999px;
393
+ background: #eef2f5;
394
+ color: #475569;
395
+ font-size: 12px;
396
+ font-weight: 800;
397
+ }
398
+ .curator-grid {
399
+ display: grid;
400
+ grid-template-columns: minmax(0, 1.4fr) minmax(220px, .8fr);
401
+ gap: 12px;
402
+ }
403
+ .curator-grid section {
404
+ min-width: 0;
405
+ padding: 12px;
406
+ border: 1px solid var(--line);
407
+ border-radius: 8px;
408
+ background: var(--surface);
409
+ }
410
+ .curator-grid section:first-child { grid-row: span 2; }
411
+ .curator h3 { margin: 0 0 8px; font-size: 16px; }
412
+ table {
413
+ width: 100%;
414
+ border-collapse: collapse;
415
+ font-size: 13px;
416
+ }
417
+ th, td {
418
+ padding: 7px 6px;
419
+ border-bottom: 1px solid var(--line);
420
+ text-align: left;
421
+ vertical-align: top;
422
+ overflow-wrap: anywhere;
423
+ }
424
+ th { color: var(--muted); font-size: 12px; }
425
+ ul { margin: 8px 0 0; padding-left: 18px; }
426
+ @media (max-width: 640px) {
427
+ main { width: min(100% - 20px, 1120px); padding-top: 12px; }
428
+ header { grid-template-columns: 1fr; align-items: start; }
429
+ .scope { justify-self: start; }
430
+ .item-head { align-items: flex-start; flex-direction: column; }
431
+ time { text-align: left; }
432
+ .curator-grid { grid-template-columns: 1fr; }
433
+ }
434
+ </style>
435
+ </head>
436
+ <body data-authority="projection-only" data-ledger-write-path="false">
437
+ <main>
438
+ <header>
439
+ <div>
440
+ <h1>Etiquette Timeline Preview</h1>
441
+ <p class="subhead">Read-only loopback panel over the existing timeline projection.</p>
442
+ </div>
443
+ <span class="scope">${escapeHtml(scopeLabel)}</span>
444
+ </header>
445
+ <section class="summary" aria-label="Projection summary">
446
+ <div class="metric"><span>Schema</span><strong>${escapeHtml(view.schema)}</strong></div>
447
+ <div class="metric"><span>Items</span><strong>${String(view.feed.items_matched)}</strong></div>
448
+ <div class="metric"><span>Source</span><strong>${escapeHtml(view.source.projection_schema)}</strong></div>
449
+ <div class="metric"><span>Watermark</span><strong>${escapeHtml(view.watermark.latest_event_ref)}</strong></div>
450
+ <div class="metric"><span>Commit</span><strong>${escapeHtml(view.watermark.commit_sha)}</strong></div>
451
+ <div class="metric"><span>Event log hash</span><strong>${escapeHtml(view.watermark.event_log_hash)}</strong></div>
452
+ </section>
453
+ <section class="boundary">
454
+ <strong>Boundary</strong>
455
+ <span>${escapeHtml(view.boundary)}</span>
456
+ <span>GET/HEAD/OPTIONS only; no hosted backend, no auth provider, no impersonation, no execution, no write-back.</span>
457
+ </section>
458
+ <section class="view-controls" aria-label="Feed modes">
459
+ <nav class="toggle" aria-label="Timeline display modes">
460
+ <a href="#stream" aria-current="true">Stream</a>
461
+ <a href="#threads">Thread</a>
462
+ </nav>
463
+ <span>${escapeHtml(selected)}</span>
464
+ </section>
465
+ <section id="threads" aria-label="Thread index">
466
+ ${renderThreadIndex(view)}
467
+ </section>
468
+ <section id="stream" class="feed" aria-label="Timeline feed">
469
+ ${items}
470
+ </section>
471
+ ${renderCuratorPortal(view)}
472
+ </main>
473
+ </body>
474
+ </html>`;
475
+ }
476
+
477
+ function previewHeaders(contentType: string): Headers {
478
+ return new Headers({
479
+ 'allow': TIMELINE_PREVIEW_ALLOWED_METHODS.join(', '),
480
+ 'cache-control': 'no-store',
481
+ 'content-type': contentType,
482
+ 'x-etiquette-authority': 'projection-only',
483
+ 'x-etiquette-ledger-write-path': 'false',
484
+ });
485
+ }
486
+
487
+ function emptyIfHead(request: Request, body: string): string | null {
488
+ return request.method === 'HEAD' ? null : body;
489
+ }
490
+
491
+ export async function timelinePreviewResponseForRequest(
492
+ request: Request,
493
+ buildView: () => Promise<TimelineView>,
494
+ ): Promise<Response> {
495
+ if (request.method === 'OPTIONS') {
496
+ return new Response(null, { status: 204, headers: previewHeaders('text/plain; charset=utf-8') });
497
+ }
498
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
499
+ return Response.json(
500
+ {
501
+ error: 'method_not_allowed',
502
+ allowed_methods: TIMELINE_PREVIEW_ALLOWED_METHODS,
503
+ authority_boundary: {
504
+ request_can_write_ledger: false,
505
+ request_can_execute: false,
506
+ request_can_grant: false,
507
+ },
508
+ },
509
+ { status: 405, headers: previewHeaders('application/json; charset=utf-8') },
510
+ );
511
+ }
512
+
513
+ const url = new URL(request.url);
514
+ const requestedSeq = Number.parseInt(url.searchParams.get('seq') ?? '', 10);
515
+ const selectSeq = Number.isFinite(requestedSeq) ? requestedSeq : null;
516
+ if (url.pathname === '/api/timeline') {
517
+ const view = applyTimelineDeepLink(await buildView(), selectSeq);
518
+ return new Response(emptyIfHead(request, JSON.stringify(view, null, 2)), {
519
+ headers: previewHeaders('application/json; charset=utf-8'),
520
+ });
521
+ }
522
+ if (url.pathname === '/' || url.pathname === '/index.html') {
523
+ const view = applyTimelineDeepLink(await buildView(), selectSeq);
524
+ return new Response(emptyIfHead(request, renderTimelinePreviewHtml(view)), {
525
+ headers: previewHeaders('text/html; charset=utf-8'),
526
+ });
527
+ }
528
+ return Response.json(
529
+ {
530
+ error: 'not_found',
531
+ authority_boundary: {
532
+ request_can_write_ledger: false,
533
+ request_can_execute: false,
534
+ request_can_grant: false,
535
+ },
536
+ },
537
+ { status: 404, headers: previewHeaders('application/json; charset=utf-8') },
538
+ );
539
+ }