@etiquekit/etq 1.0.11 → 1.0.13
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/AGENTS.md +19 -26
- package/LICENSE +2 -3
- package/NOTICE +6 -4
- package/QuickStart.md +35 -47
- package/README.md +54 -56
- package/bin/etiquette +1 -5
- package/bin/etiquette-core +1 -5
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
- package/docs/CONCEPTS.md +7 -23
- package/docs/CONCEPT_STATUS.md +66 -0
- package/docs/CORE_PROFILE.md +3 -5
- package/docs/LANE_PROVISIONING.md +7 -2
- package/docs/README.md +4 -4
- package/docs/RELEASE_SURFACE_AUDIT.md +7 -6
- package/docs/SEAT_DISCIPLINE.md +55 -91
- package/docs/SEAT_PROVISIONING.md +19 -22
- package/docs/TEAM_HANDOFF.md +21 -21
- package/docs/WORKTREE_QOL.md +7 -6
- package/docs/contracts/ledger-entry/README.md +11 -11
- package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
- package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
- package/package.json +11 -11
- package/packages/control/src/authority/lease.ts +261 -0
- package/packages/control/src/authority/node-delegation.ts +257 -0
- package/packages/control/src/authority/rig-conductor.ts +632 -0
- package/packages/control/src/cli/argv.ts +155 -0
- package/packages/control/src/cli/commands/console.ts +200 -0
- package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
- package/packages/control/src/cli/commands/dispatch.ts +279 -0
- package/packages/control/src/cli/commands/harness.ts +89 -0
- package/packages/control/src/cli/commands/hook.ts +50 -0
- package/packages/control/src/cli/commands/ledger.ts +91 -0
- package/packages/control/src/cli/commands/local-workflow.ts +4686 -0
- package/packages/control/src/cli/commands/memory.ts +445 -0
- package/packages/control/src/cli/commands/release.ts +108 -0
- package/packages/control/src/cli/commands/rubric.ts +103 -0
- package/packages/control/src/cli/commands/seat.ts +179 -0
- package/packages/control/src/cli/commands/session.ts +127 -0
- package/packages/control/src/cli/commands/supervision.ts +246 -0
- package/packages/control/src/cli/commands/sync.ts +119 -0
- package/packages/control/src/cli/commands/workflow.ts +86 -0
- package/packages/control/src/cli/commands/workspace.ts +50 -0
- package/packages/control/src/cli/core-usage.ts +34 -0
- package/packages/control/src/cli/prompt.ts +67 -0
- package/packages/control/src/cli/supervision-deps.ts +44 -0
- package/packages/control/src/cli/usage.ts +241 -0
- package/packages/control/src/cli.ts +207 -0
- package/packages/control/src/core-cli.ts +50 -0
- package/packages/control/src/dispatch/decision.ts +202 -0
- package/packages/control/src/dispatch/projection.ts +293 -0
- package/packages/control/src/dispatch/record.ts +153 -0
- package/packages/control/src/engagement/project.ts +170 -0
- package/packages/control/src/fs.ts +19 -0
- package/packages/control/src/harness/pruning.ts +406 -0
- package/packages/control/src/hooks/dispatcher.ts +117 -0
- package/packages/control/src/hooks/outbox.ts +86 -0
- package/packages/control/src/hooks/sanitize.ts +6 -0
- package/packages/control/src/hooks/types.ts +34 -0
- package/packages/control/src/index.ts +384 -0
- package/packages/control/src/ledger/entry.ts +303 -0
- package/packages/control/src/ledger/indexer.ts +542 -0
- package/packages/control/src/memory/context.ts +149 -0
- package/packages/control/src/memory/drain-import.ts +207 -0
- package/packages/control/src/memory/indexer.ts +284 -0
- package/packages/control/src/memory/query.ts +75 -0
- package/packages/control/src/memory/sanitize.ts +50 -0
- package/packages/control/src/memory/sharded-drain-import.ts +212 -0
- package/packages/control/src/memory/status.ts +211 -0
- package/packages/control/src/memory/store-lifecycle.ts +509 -0
- package/packages/control/src/memory/store.ts +284 -0
- package/packages/control/src/memory/types.ts +146 -0
- package/packages/control/src/parity/surfaces.ts +748 -0
- package/packages/control/src/project.ts +141 -0
- package/packages/control/src/projection/local-ledger-view.ts +373 -0
- package/packages/control/src/projection/return-enforcement.ts +48 -0
- package/packages/control/src/projection/timeline-preview.ts +539 -0
- package/packages/control/src/projection/timeline.ts +708 -0
- package/packages/control/src/release/readiness.ts +831 -0
- package/packages/control/src/rubric/loader.ts +326 -0
- package/packages/control/src/rubric/promotion.ts +54 -0
- package/packages/control/src/rubric/runner.ts +159 -0
- package/packages/control/src/rubric/types.ts +158 -0
- package/packages/control/src/seat/owner-card.ts +388 -0
- package/packages/control/src/seat/readiness.ts +834 -0
- package/packages/control/src/session/runbook.ts +431 -0
- package/packages/control/src/shared/sanitize.ts +49 -0
- package/packages/control/src/supervision/action-classes.ts +192 -0
- package/packages/control/src/supervision/command-apply.ts +378 -0
- package/packages/control/src/supervision/errors.ts +14 -0
- package/packages/control/src/supervision/event-replay.ts +155 -0
- package/packages/control/src/supervision/events.ts +109 -0
- package/packages/control/src/supervision/index.ts +16 -0
- package/packages/control/src/supervision/manifest.ts +127 -0
- package/packages/control/src/supervision/paths.ts +49 -0
- package/packages/control/src/supervision/projection-adapter.ts +274 -0
- package/packages/control/src/supervision/projection.ts +75 -0
- package/packages/control/src/supervision/rebuild.ts +99 -0
- package/packages/control/src/supervision/session-open.ts +131 -0
- package/packages/control/src/supervision/session-read.ts +99 -0
- package/packages/control/src/supervision/sqlite-impl.ts +71 -0
- package/packages/control/src/supervision/sqlite.ts +121 -0
- package/packages/control/src/supervision/store-rows.ts +371 -0
- package/packages/control/src/supervision/turn-close.ts +154 -0
- package/packages/control/src/supervision/turn-open.ts +284 -0
- package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
- package/packages/control/src/sync/join-plan.ts +375 -0
- package/packages/control/src/sync/outbox.ts +492 -0
- package/packages/control/src/workflow/evaluator.ts +140 -0
- package/packages/control/src/workflow/loader.ts +200 -0
- package/packages/control/src/workflow/types.ts +90 -0
- package/packages/control/src/workspace/authority.ts +499 -0
- package/packages/protocol/src/guards.ts +119 -0
- package/packages/protocol/src/huddle-board.ts +198 -0
- package/packages/protocol/src/huddle.ts +295 -0
- package/packages/protocol/src/incident.ts +251 -0
- package/packages/protocol/src/index.ts +8 -0
- package/packages/protocol/src/interfaces.ts +107 -0
- package/packages/protocol/src/packet-profile.ts +195 -0
- package/packages/protocol/src/state.ts +81 -0
- package/packages/protocol/src/types.ts +434 -0
- package/release/lineage.v0.json +14 -0
- package/scripts/release-candidate-verify.sh +175 -0
- package/scripts/release-checksum.sh +25 -0
- package/scripts/release-pack-canary.sh +97 -0
- package/scripts/release-scan.sh +249 -0
- package/scripts/release-sign.sh +34 -0
- package/templates/etiquette-vanilla-v0/README.md +2 -3
- package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
- package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
- package/templates/seat-packs-v0/README.md +3 -2
- package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
- package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
- package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
- package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
- package/docs/AGENT_SPAWNING.md +0 -78
- package/docs/INTEGRATION_UI.md +0 -215
- package/docs/OPERATOR_PLAYBOOK.md +0 -77
- package/docs/TAG_ROUTE.md +0 -95
- package/lib/etiquette-core.js +0 -315
- package/lib/etiquette.js +0 -1160
- package/templates/DISPATCH_PREMISES.md +0 -39
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
- package/templates/hosted-receiver/README.md +0 -41
- 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, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"')
|
|
11
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|