@astrosheep/square 0.3.2
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/codex-plugin/.codex-plugin/plugin.json +25 -0
- package/codex-plugin/hooks/hooks.json +28 -0
- package/dist/activity-feed.js +36 -0
- package/dist/activity.js +151 -0
- package/dist/artifact.js +739 -0
- package/dist/claude-hook.js +112 -0
- package/dist/cmd/notify-once.js +37 -0
- package/dist/compact.js +39 -0
- package/dist/decisions.js +286 -0
- package/dist/delivery-health.js +249 -0
- package/dist/delivery.js +93 -0
- package/dist/doctor.js +34 -0
- package/dist/harness.js +584 -0
- package/dist/help.js +131 -0
- package/dist/inbox.js +33 -0
- package/dist/index.js +163 -0
- package/dist/list.js +126 -0
- package/dist/model.js +44 -0
- package/dist/notifications.js +97 -0
- package/dist/paseo-timeline.js +206 -0
- package/dist/presentation.js +468 -0
- package/dist/presented.js +211 -0
- package/dist/registry.js +299 -0
- package/dist/runtime.js +304 -0
- package/dist/search.js +54 -0
- package/dist/square-core.js +183 -0
- package/dist/square.js +1366 -0
- package/dist/stream.js +149 -0
- package/dist/terminal.js +125 -0
- package/dist/time.js +81 -0
- package/dist/wake-sink.js +219 -0
- package/dist/watch.js +386 -0
- package/extensions/square-opencode.js +87 -0
- package/extensions/square-pi.js +167 -0
- package/guides/architect.md +165 -0
- package/guides/brainstorm.md +404 -0
- package/guides/participant.md +171 -0
- package/package.json +57 -0
- package/skills/brainstorm/SKILL.md +136 -0
- package/skills/square/.claude-plugin/plugin.json +8 -0
- package/skills/square/SKILL.md +154 -0
- package/skills/square/hooks/hooks.json +27 -0
- package/skills/square-feedback/SKILL.md +55 -0
- package/skills/square-feedback/agents/openai.yaml +4 -0
- package/template.md +4 -0
- package/templates/architect.md +4 -0
- package/templates/brainstorm.md +4 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadSquare, saveRuntimeSidecar } from './artifact.js';
|
|
5
|
+
import { SquareError, sameName } from './model.js';
|
|
6
|
+
import { planActNotifications } from './notifications.js';
|
|
7
|
+
import { formatDuration } from './time.js';
|
|
8
|
+
import { isCurrentlyJoined, isDeliveryDelivered, lastJoinAt, recordDeliveredDelivery, resolveRosterName, rosterNames, } from './runtime.js';
|
|
9
|
+
const DEFAULT_STALE_MS = 60_000;
|
|
10
|
+
/** Only acts inside this lookback can prove "hooks are dead right now". Older = historical debt. */
|
|
11
|
+
const DEFAULT_LOOKBACK_MS = 60 * 60 * 1000;
|
|
12
|
+
function parsePositiveIntegerEnv(name, fallback) {
|
|
13
|
+
const raw = process.env[name];
|
|
14
|
+
if (raw === undefined)
|
|
15
|
+
return fallback;
|
|
16
|
+
const value = Number.parseInt(raw, 10);
|
|
17
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
18
|
+
throw new SquareError('invalid_args', `Invalid ${name}: expected a positive integer.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
/** How long a mention/bell may remain undelivered before harness liveness is suspect. */
|
|
23
|
+
export function deliveryStaleMs() {
|
|
24
|
+
return parsePositiveIntegerEnv('SQUARE_DELIVERY_STALE_MS', DEFAULT_STALE_MS);
|
|
25
|
+
}
|
|
26
|
+
/** How far back liveness scans look. Older unreceipted mentions are backlog, not hook-death. */
|
|
27
|
+
export function deliveryLookbackMs() {
|
|
28
|
+
return parsePositiveIntegerEnv('SQUARE_DELIVERY_LOOKBACK_MS', DEFAULT_LOOKBACK_MS);
|
|
29
|
+
}
|
|
30
|
+
function baselinePath(env = process.env) {
|
|
31
|
+
return env.SQUARE_DELIVERY_BASELINE || path.join(os.homedir(), '.square', 'delivery-baseline.json');
|
|
32
|
+
}
|
|
33
|
+
function readBaselines(env = process.env) {
|
|
34
|
+
const file = baselinePath(env);
|
|
35
|
+
if (!fs.existsSync(file))
|
|
36
|
+
return {};
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
39
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
40
|
+
return {};
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function recordBacklogBaseline(squarePath, backlogCount, env = process.env, at = Date.now()) {
|
|
48
|
+
const file = baselinePath(env);
|
|
49
|
+
const data = readBaselines(env);
|
|
50
|
+
data[path.resolve(squarePath)] = { backlogCount, at };
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
53
|
+
fs.writeFileSync(temp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
54
|
+
fs.renameSync(temp, file);
|
|
55
|
+
}
|
|
56
|
+
export function previousBacklogCount(squarePath, env = process.env) {
|
|
57
|
+
return readBaselines(env)[path.resolve(squarePath)]?.backlogCount;
|
|
58
|
+
}
|
|
59
|
+
function isReceipted(_squarePath, doc, recipient, actIndex, _env) {
|
|
60
|
+
// This sidecar record is the only delivery authority. A machine-local
|
|
61
|
+
// presentation cache may suppress duplicate hook text, but never clears debt.
|
|
62
|
+
return isDeliveryDelivered(doc, recipient, actIndex);
|
|
63
|
+
}
|
|
64
|
+
function actedAfterMention(doc, recipient, mentionAt, mentionIndex) {
|
|
65
|
+
return doc.acts.some((act) => act.actor !== undefined &&
|
|
66
|
+
sameName(act.actor, recipient) &&
|
|
67
|
+
(act.at > mentionAt || act.index > mentionIndex));
|
|
68
|
+
}
|
|
69
|
+
function collectUnreceiptedMentions(squarePath, doc, now, opts = {}) {
|
|
70
|
+
const env = opts.env ?? process.env;
|
|
71
|
+
const pending = [];
|
|
72
|
+
for (const act of doc.acts) {
|
|
73
|
+
if (act.kind !== 'say')
|
|
74
|
+
continue;
|
|
75
|
+
const item = { act, index: act.index };
|
|
76
|
+
for (const planned of planActNotifications(doc, item)) {
|
|
77
|
+
if (planned.via !== 'mention' && planned.via !== 'bell')
|
|
78
|
+
continue;
|
|
79
|
+
const recipient = resolveRosterName(doc, planned.recipient) ?? planned.recipient;
|
|
80
|
+
if (opts.onlyJoined && !isCurrentlyJoined(doc.acts, recipient))
|
|
81
|
+
continue;
|
|
82
|
+
if (isReceipted(squarePath, doc, recipient, act.index, env))
|
|
83
|
+
continue;
|
|
84
|
+
pending.push({
|
|
85
|
+
squarePath,
|
|
86
|
+
recipient,
|
|
87
|
+
actIndex: act.index,
|
|
88
|
+
actor: act.actor,
|
|
89
|
+
at: act.at,
|
|
90
|
+
ageMs: now - act.at,
|
|
91
|
+
via: planned.via,
|
|
92
|
+
actedAfterWithoutDelivery: actedAfterMention(doc, recipient, act.at, act.index),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return pending;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Partition unreceipted mention/bell traffic.
|
|
100
|
+
* Liveness failure requires: past stale, inside lookback, currently joined, mention after last join.
|
|
101
|
+
*/
|
|
102
|
+
export function partitionPendingDeliveries(squarePath, opts = {}) {
|
|
103
|
+
const now = opts.now ?? Date.now();
|
|
104
|
+
const staleMs = opts.staleMs ?? deliveryStaleMs();
|
|
105
|
+
const lookbackMs = Math.max(opts.lookbackMs ?? deliveryLookbackMs(), staleMs);
|
|
106
|
+
const doc = opts.doc ?? loadSquare(squarePath);
|
|
107
|
+
const recent = [];
|
|
108
|
+
const historical = [];
|
|
109
|
+
// Include not-joined for historical/reconcile; liveness only uses joined.
|
|
110
|
+
for (const item of collectUnreceiptedMentions(squarePath, doc, now, {
|
|
111
|
+
onlyJoined: false,
|
|
112
|
+
env: opts.env,
|
|
113
|
+
})) {
|
|
114
|
+
const joined = isCurrentlyJoined(doc.acts, item.recipient);
|
|
115
|
+
const joinAt = lastJoinAt(doc.acts, item.recipient);
|
|
116
|
+
const afterJoin = joinAt === undefined || item.at >= joinAt;
|
|
117
|
+
const inLookback = item.ageMs <= lookbackMs;
|
|
118
|
+
const pastStale = item.ageMs >= staleMs;
|
|
119
|
+
if (joined && pastStale && inLookback && afterJoin) {
|
|
120
|
+
recent.push(item);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Still in grace window and would otherwise be live → ignore (not historical yet).
|
|
124
|
+
if (joined && !pastStale && inLookback && afterJoin)
|
|
125
|
+
continue;
|
|
126
|
+
historical.push(item);
|
|
127
|
+
}
|
|
128
|
+
return { recent, historical };
|
|
129
|
+
}
|
|
130
|
+
/** Recent stale only — the liveness signal. */
|
|
131
|
+
export function findStalePendingMentions(squarePath, opts = {}) {
|
|
132
|
+
return partitionPendingDeliveries(squarePath, opts).recent;
|
|
133
|
+
}
|
|
134
|
+
function summarizeByRecipient(items) {
|
|
135
|
+
const byRecipient = new Map();
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
const list = byRecipient.get(item.recipient) ?? [];
|
|
138
|
+
list.push(item);
|
|
139
|
+
byRecipient.set(item.recipient, list);
|
|
140
|
+
}
|
|
141
|
+
const lines = [];
|
|
142
|
+
for (const [recipient, group] of byRecipient) {
|
|
143
|
+
const oldest = group.reduce((a, b) => (a.at <= b.at ? a : b));
|
|
144
|
+
const ageSec = Math.max(1, Math.round(oldest.ageMs / 1000));
|
|
145
|
+
const adapter = group.some((item) => item.actedAfterWithoutDelivery);
|
|
146
|
+
lines.push(adapter
|
|
147
|
+
? ` · @${recipient}: ${group.length} pending — acted after the mention, but delivery was not acknowledged (oldest act_${oldest.actIndex}, ~${ageSec}s)`
|
|
148
|
+
: ` · @${recipient}: ${group.length} pending (oldest act_${oldest.actIndex} from @${oldest.actor}, ~${ageSec}s)`);
|
|
149
|
+
}
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
152
|
+
export function formatStaleDeliveryWarnings(recent, historical = [], opts = {}) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
if (recent.length > 0) {
|
|
155
|
+
const adapterFaults = recent.filter((item) => item.actedAfterWithoutDelivery);
|
|
156
|
+
if (adapterFaults.length > 0) {
|
|
157
|
+
lines.push(`⚠ ${adapterFaults.length} recent mention(s): the recipient acted after the mention, but delivery was not acknowledged.`);
|
|
158
|
+
lines.push(' This points to an adapter failure, not participant behavior.');
|
|
159
|
+
lines.push(' » square harness doctor');
|
|
160
|
+
}
|
|
161
|
+
const other = recent.filter((item) => !item.actedAfterWithoutDelivery);
|
|
162
|
+
if (other.length > 0) {
|
|
163
|
+
lines.push(`⚠ ${other.length} recent mention or bell notification(s) were not acknowledged inside the delivery window.`);
|
|
164
|
+
lines.push(' The installed hook or extension may not be consuming notifications.');
|
|
165
|
+
}
|
|
166
|
+
lines.push(...summarizeByRecipient(recent));
|
|
167
|
+
if (adapterFaults.length === 0)
|
|
168
|
+
lines.push(' » square harness doctor');
|
|
169
|
+
}
|
|
170
|
+
if (historical.length > 0) {
|
|
171
|
+
lines.push(`○ ${historical.length} historical or pre-join mention(s) remain unacknowledged — backlog, not a current delivery failure.`);
|
|
172
|
+
const counts = new Map();
|
|
173
|
+
for (const item of historical) {
|
|
174
|
+
counts.set(item.recipient, (counts.get(item.recipient) ?? 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
lines.push(` · ${[...counts.entries()].map(([name, n]) => `@${name}:${n}`).join(', ')}`);
|
|
177
|
+
if (opts.previousBacklog !== undefined) {
|
|
178
|
+
const delta = historical.length - opts.previousBacklog;
|
|
179
|
+
if (delta > 0) {
|
|
180
|
+
lines.push(` ⚠ backlog grew by ${delta} since last doctor (something aged out of lookback unnoticed).`);
|
|
181
|
+
}
|
|
182
|
+
else if (delta < 0) {
|
|
183
|
+
lines.push(` · backlog shrank by ${-delta} since last doctor.`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
lines.push(' · backlog unchanged since last doctor.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (opts.previousBacklog !== undefined && opts.previousBacklog > 0) {
|
|
191
|
+
lines.push(`○ backlog cleared (was ${opts.previousBacklog}).`);
|
|
192
|
+
}
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
export function doctorDeliveryHealth(squarePath, now = Date.now(), env = process.env) {
|
|
196
|
+
try {
|
|
197
|
+
const doc = loadSquare(squarePath);
|
|
198
|
+
const { recent, historical } = partitionPendingDeliveries(squarePath, { now, doc, env });
|
|
199
|
+
const previous = previousBacklogCount(squarePath, env);
|
|
200
|
+
const lines = [
|
|
201
|
+
`· stale after ${formatDuration(deliveryStaleMs())} · scan window ${formatDuration(deliveryLookbackMs())}`,
|
|
202
|
+
`· participants ${rosterNames(doc).join(', ') || '(none)'}`,
|
|
203
|
+
];
|
|
204
|
+
if (recent.length === 0 && historical.length === 0) {
|
|
205
|
+
lines.push('✓ no stale undelivered mentions or bells');
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
if (recent.length === 0) {
|
|
209
|
+
lines.push('✓ no current delivery failures inside the scan window');
|
|
210
|
+
}
|
|
211
|
+
lines.push(...formatStaleDeliveryWarnings(recent, historical, { previousBacklog: previous }).map((line) => line.startsWith('⚠ ') ? line.replace(/^⚠ /, '✕ ') : line));
|
|
212
|
+
}
|
|
213
|
+
recordBacklogBaseline(squarePath, historical.length, env, now);
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
return [`✕ delivery health unreadable: ${error instanceof Error ? error.message : String(error)}`];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Act-path only screams for recent liveness failures, not historical backlog. */
|
|
221
|
+
export function hasStalePendingForAny(squarePath, now = Date.now()) {
|
|
222
|
+
return findStalePendingMentions(squarePath, { now }).length > 0;
|
|
223
|
+
}
|
|
224
|
+
export function notificationMessageId(squarePath, actIndex) {
|
|
225
|
+
return `square:${squarePath}#act_${actIndex}`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Write delivered-class receipts with reason=reconciled for backlog items.
|
|
229
|
+
* Does not touch recent liveness failures.
|
|
230
|
+
*/
|
|
231
|
+
export function reconcileDeliveryBacklog(squarePath, opts = {}) {
|
|
232
|
+
const now = opts.now ?? Date.now();
|
|
233
|
+
const doc = loadSquare(squarePath);
|
|
234
|
+
const { recent, historical } = partitionPendingDeliveries(squarePath, {
|
|
235
|
+
now,
|
|
236
|
+
doc,
|
|
237
|
+
lookbackMs: opts.lookbackMs,
|
|
238
|
+
staleMs: opts.staleMs,
|
|
239
|
+
});
|
|
240
|
+
const actor = opts.actor ?? 'doctor --fix reconcile-backlog';
|
|
241
|
+
let reconciled = 0;
|
|
242
|
+
for (const item of historical) {
|
|
243
|
+
if (recordDeliveredDelivery(doc, item.recipient, item.actIndex, { at: now, reason: 'reconciled', actor }))
|
|
244
|
+
reconciled += 1;
|
|
245
|
+
}
|
|
246
|
+
if (reconciled > 0)
|
|
247
|
+
saveRuntimeSidecar(squarePath, doc.runtime);
|
|
248
|
+
return { reconciled, skippedRecent: recent.length, items: historical };
|
|
249
|
+
}
|
package/dist/delivery.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { findParticipantName, sameName, } from './model.js';
|
|
2
|
+
import { extractMentions, isDeliveryDelivered, lastJoinIndex, matchesMentionTarget, rosterNames } from './runtime.js';
|
|
3
|
+
function uniqueKnownMentions(body, roster) {
|
|
4
|
+
const recipients = [];
|
|
5
|
+
for (const mention of extractMentions(body)) {
|
|
6
|
+
const known = findParticipantName(roster, mention);
|
|
7
|
+
if (known !== undefined && !recipients.some((recipient) => sameName(recipient, known))) {
|
|
8
|
+
recipients.push(known);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return recipients;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Derive delivery behavior once from the parsed Square document.
|
|
15
|
+
* All consumers share these targets instead of reinterpreting artifact text or cursor state.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveDeliveryModel(doc) {
|
|
18
|
+
const roster = rosterNames(doc);
|
|
19
|
+
let pendingByRecipient;
|
|
20
|
+
function plan(item) {
|
|
21
|
+
if (item.act.kind !== 'say')
|
|
22
|
+
return [];
|
|
23
|
+
const sayItem = item;
|
|
24
|
+
const actor = sayItem.act.actor;
|
|
25
|
+
if (sayItem.act.reach === 'bell') {
|
|
26
|
+
return roster
|
|
27
|
+
.filter((recipient) => !sameName(recipient, actor))
|
|
28
|
+
.map((recipient) => ({ item: sayItem, recipient, via: 'bell' }));
|
|
29
|
+
}
|
|
30
|
+
if (sayItem.act.reach !== undefined) {
|
|
31
|
+
const recipient = findParticipantName(roster, sayItem.act.reach.beside);
|
|
32
|
+
return recipient === undefined || sameName(recipient, actor)
|
|
33
|
+
? []
|
|
34
|
+
: [{ item: sayItem, recipient, via: 'mention' }];
|
|
35
|
+
}
|
|
36
|
+
const mentions = uniqueKnownMentions(sayItem.act.body, roster).filter((recipient) => !sameName(recipient, actor));
|
|
37
|
+
const recipients = mentions.length > 0
|
|
38
|
+
? mentions
|
|
39
|
+
: roster.filter((recipient) => !sameName(recipient, actor));
|
|
40
|
+
const via = mentions.length > 0 ? 'mention' : 'broadcast';
|
|
41
|
+
return recipients.map((recipient) => ({ item: sayItem, recipient, via }));
|
|
42
|
+
}
|
|
43
|
+
function pendingFor(requestedRecipient) {
|
|
44
|
+
const recipient = findParticipantName(roster, requestedRecipient);
|
|
45
|
+
if (recipient === undefined)
|
|
46
|
+
return [];
|
|
47
|
+
if (pendingByRecipient === undefined) {
|
|
48
|
+
pendingByRecipient = new Map(roster.map((name) => [name, []]));
|
|
49
|
+
const joinedAfter = new Map(roster.map((name) => [name, lastJoinIndex(doc.acts, name)]));
|
|
50
|
+
for (const act of doc.acts) {
|
|
51
|
+
if (act.kind !== 'say')
|
|
52
|
+
continue;
|
|
53
|
+
// Broadcasts can never be pending directed notifications. Skipping them here
|
|
54
|
+
// avoids allocating one planned notification per participant per activity.
|
|
55
|
+
if (act.reach === undefined && extractMentions(act.body).length === 0)
|
|
56
|
+
continue;
|
|
57
|
+
for (const planned of plan({ act, index: act.index })) {
|
|
58
|
+
if (planned.via === 'broadcast')
|
|
59
|
+
continue;
|
|
60
|
+
const joinedAt = joinedAfter.get(planned.recipient);
|
|
61
|
+
if (joinedAt === undefined || act.index <= joinedAt)
|
|
62
|
+
continue;
|
|
63
|
+
if (isDeliveryDelivered(doc, planned.recipient, act.index))
|
|
64
|
+
continue;
|
|
65
|
+
pendingByRecipient.get(planned.recipient)?.push({ ...planned, via: planned.via });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...(pendingByRecipient.get(recipient) ?? [])];
|
|
70
|
+
}
|
|
71
|
+
return { plan, pendingFor };
|
|
72
|
+
}
|
|
73
|
+
export function planActNotifications(doc, item) {
|
|
74
|
+
return deriveDeliveryModel(doc).plan(item);
|
|
75
|
+
}
|
|
76
|
+
/** Canonical say-activity filter shared by catch selection and hook ownership. */
|
|
77
|
+
export function matchesCatchFilter(activity, filter) {
|
|
78
|
+
if (activity.bell)
|
|
79
|
+
return true;
|
|
80
|
+
if (filter.participants !== undefined &&
|
|
81
|
+
!filter.participants.some((participant) => sameName(participant, activity.actor))) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return filter.mention === undefined || matchesMentionTarget({ body: activity.body }, filter.mention);
|
|
85
|
+
}
|
|
86
|
+
/** True only when the live catch's own filters would deliver this notification. */
|
|
87
|
+
export function leaseOwnsNotification(lease, notification) {
|
|
88
|
+
return matchesCatchFilter({
|
|
89
|
+
actor: notification.actor,
|
|
90
|
+
body: notification.body,
|
|
91
|
+
bell: notification.via === 'bell',
|
|
92
|
+
}, lease.filter ?? {});
|
|
93
|
+
}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { diagnoseSquare, } from './artifact.js';
|
|
2
|
+
export function planRepair(text) {
|
|
3
|
+
const diagnosis = diagnoseSquare(text);
|
|
4
|
+
if (diagnosis.unfixable)
|
|
5
|
+
return { diagnosis };
|
|
6
|
+
const actions = [];
|
|
7
|
+
const acts = diagnosis.acts.map(({ act }, index) => ({ ...act, index }));
|
|
8
|
+
if (diagnosis.acts.some(({ act }, index) => act.index !== index)) {
|
|
9
|
+
actions.push({ message: 'renumbered act indexes to be contiguous' });
|
|
10
|
+
}
|
|
11
|
+
if (diagnosis.quarantined.length > 0) {
|
|
12
|
+
actions.push({ message: `quarantined ${diagnosis.quarantined.length} unparseable act block(s)` });
|
|
13
|
+
}
|
|
14
|
+
if (diagnosis.legacyParticipants.length > 0) {
|
|
15
|
+
actions.push({ message: 'dropped frontmatter participants; roster now derives from join/done acts' });
|
|
16
|
+
}
|
|
17
|
+
const nextActIndex = acts.length;
|
|
18
|
+
const doc = {
|
|
19
|
+
hardCap: diagnosis.hardCap,
|
|
20
|
+
throttlePerMinute: diagnosis.throttlePerMinute,
|
|
21
|
+
preamble: diagnosis.preamble,
|
|
22
|
+
warmup: diagnosis.warmup,
|
|
23
|
+
acts,
|
|
24
|
+
runtime: {
|
|
25
|
+
version: 2,
|
|
26
|
+
nextActIndex,
|
|
27
|
+
firstActIndex: acts.length > 0 ? (acts[0].index ?? 0) : 0,
|
|
28
|
+
cursors: {},
|
|
29
|
+
mentionReceipts: {},
|
|
30
|
+
leases: {},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return { diagnosis, repaired: { doc, actions, quarantinedBlocks: diagnosis.quarantined.map((q) => q.raw) } };
|
|
34
|
+
}
|