@astrosheep/square 0.3.11 → 0.3.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/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/hooks/hooks.json +9 -0
- package/dist/activity-feed.d.ts +14 -0
- package/dist/activity-feed.js +5 -5
- package/dist/activity.d.ts +9 -0
- package/dist/activity.js +112 -96
- package/dist/artifact.d.ts +20 -0
- package/dist/artifact.js +317 -648
- package/dist/automatic-session.d.ts +5 -0
- package/dist/automatic-session.js +86 -0
- package/dist/boundary-presentation.d.ts +5 -0
- package/dist/boundary-presentation.js +6 -5
- package/dist/claude-hook.d.ts +9 -0
- package/dist/claude-hook.js +35 -3
- package/dist/cli/context.d.ts +35 -0
- package/dist/cli/context.js +26 -31
- package/dist/cli/harness-command.d.ts +28 -0
- package/dist/cli/harness-command.js +30 -15
- package/dist/cli/maintenance-commands.d.ts +7 -0
- package/dist/cli/maintenance-commands.js +12 -69
- package/dist/cli/meta-commands.d.ts +5 -0
- package/dist/cli/observation-commands.d.ts +20 -0
- package/dist/cli/observation-commands.js +159 -139
- package/dist/cli/program.d.ts +2 -0
- package/dist/cli/program.js +3 -12
- package/dist/cli/registry.d.ts +9 -0
- package/dist/cli/registry.js +2 -4
- package/dist/cli/square-commands.d.ts +34 -0
- package/dist/cli/square-commands.js +90 -84
- package/dist/cmd/notify-once.d.ts +2 -0
- package/dist/codex-hook.d.ts +10 -0
- package/dist/codex-hook.js +35 -3
- package/dist/decisions.d.ts +92 -0
- package/dist/decisions.js +93 -81
- package/dist/delivery-health.d.ts +21 -0
- package/dist/delivery-health.js +41 -34
- package/dist/delivery.d.ts +81 -0
- package/dist/delivery.js +71 -69
- package/dist/file-lock.d.ts +6 -0
- package/dist/harness-claude.d.ts +19 -0
- package/dist/harness-claude.js +8 -3
- package/dist/harness-codex.d.ts +29 -0
- package/dist/harness-codex.js +2 -2
- package/dist/harness-links.d.ts +17 -0
- package/dist/harness-links.js +2 -2
- package/dist/harness-pi.d.ts +11 -0
- package/dist/harness-pi.js +2 -2
- package/dist/harness-stage.d.ts +6 -0
- package/dist/harness.d.ts +20 -0
- package/dist/harness.js +5 -3
- package/dist/help.d.ts +6 -0
- package/dist/help.js +12 -13
- package/dist/identity.d.ts +13 -0
- package/dist/inbox.d.ts +2 -0
- package/dist/inbox.js +15 -19
- package/dist/index.d.ts +13 -0
- package/dist/index.js +31 -67
- package/dist/landing.d.ts +10 -0
- package/dist/landing.js +95 -0
- package/dist/list.d.ts +1 -0
- package/dist/list.js +39 -61
- package/dist/model.d.ts +147 -0
- package/dist/model.js +12 -8
- package/dist/notifications.d.ts +30 -0
- package/dist/notifications.js +83 -91
- package/dist/open-square.d.ts +11 -0
- package/dist/open-square.js +4 -0
- package/dist/participant-identity.d.ts +1 -0
- package/dist/participant-identity.js +3 -0
- package/dist/paseo-connection.d.ts +15 -0
- package/dist/paseo-delivery.d.ts +14 -0
- package/dist/paseo-state.d.ts +11 -0
- package/dist/paseo-timeline.d.ts +16 -0
- package/dist/presence.d.ts +3 -0
- package/dist/presence.js +42 -0
- package/dist/presentation.d.ts +91 -0
- package/dist/presentation.js +52 -63
- package/dist/presented.d.ts +11 -0
- package/dist/registry.d.ts +58 -0
- package/dist/registry.js +36 -3
- package/dist/routes.d.ts +42 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.js +48 -55
- package/dist/search.d.ts +11 -0
- package/dist/square-core.d.ts +111 -0
- package/dist/square-core.js +63 -9
- package/dist/square-facade.d.ts +86 -0
- package/dist/square-facade.js +1 -0
- package/dist/square-file-adapter.d.ts +18 -0
- package/dist/square-file-adapter.js +70 -0
- package/dist/square-storage.d.ts +20 -0
- package/dist/square-storage.js +171 -0
- package/dist/square-wiring.d.ts +19 -0
- package/dist/square-wiring.js +70 -0
- package/dist/square.d.ts +2 -0
- package/dist/state-cell.d.ts +14 -0
- package/dist/state-cell.js +1 -0
- package/dist/stream.d.ts +3 -0
- package/dist/stream.js +19 -23
- package/dist/time.d.ts +6 -0
- package/dist/views.d.ts +95 -0
- package/dist/views.js +81 -0
- package/dist/wake-attempts.d.ts +39 -0
- package/dist/wake-attempts.js +8 -4
- package/dist/wake-evidence.d.ts +12 -0
- package/dist/wake-evidence.js +7 -10
- package/dist/wake-port.d.ts +20 -0
- package/dist/wake-sink.d.ts +12 -0
- package/dist/wakes.d.ts +40 -0
- package/dist/wakes.js +62 -0
- package/dist/watch.d.ts +2 -0
- package/dist/watch.js +89 -210
- package/extensions/square-opencode.js +27 -1
- package/extensions/square-pi.js +13 -0
- package/guides/participant.md +7 -174
- package/package.json +5 -4
- package/skills/brainstorm/SKILL.md +30 -30
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +56 -29
- package/skills/square/hooks/hooks.json +9 -0
- package/skills/square-feedback/SKILL.md +9 -9
- package/dist/compact.js +0 -26
- package/dist/doctor.js +0 -36
- package/dist/square-application.js +0 -262
- package/template.md +0 -4
- package/templates/architect.md +0 -4
- package/templates/brainstorm.md +0 -4
package/dist/artifact.js
CHANGED
|
@@ -1,705 +1,374 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
while (out.length > 0 && out[0] === '')
|
|
18
|
-
out.shift();
|
|
19
|
-
while (out.length > 0 && out[out.length - 1] === '')
|
|
20
|
-
out.pop();
|
|
21
|
-
return out
|
|
22
|
-
.map((line) => {
|
|
23
|
-
if (line === '>')
|
|
24
|
-
return '';
|
|
25
|
-
if (line.startsWith('> '))
|
|
26
|
-
return line.slice(2);
|
|
27
|
-
if (line.startsWith('>'))
|
|
28
|
-
return line.slice(1);
|
|
29
|
-
return line;
|
|
30
|
-
})
|
|
31
|
-
.join('\n')
|
|
32
|
-
.trim();
|
|
33
|
-
}
|
|
34
|
-
function actIndex(act) {
|
|
35
|
-
if (act.index === undefined || !Number.isInteger(act.index) || act.index < 0) {
|
|
36
|
-
throw new SquareError('invalid_args', `Invalid square act: missing stable index for ${act.kind}.`);
|
|
37
|
-
}
|
|
38
|
-
return act.index;
|
|
39
|
-
}
|
|
40
|
-
function renderActMarker(act) {
|
|
41
|
-
const marker = {
|
|
42
|
-
index: actIndex(act),
|
|
43
|
-
kind: act.kind,
|
|
44
|
-
actor: act.actor,
|
|
45
|
-
at: act.at,
|
|
46
|
-
...(act.kind === 'say' && act.reach !== undefined ? { reach: act.reach } : {}),
|
|
47
|
-
...(act.kind === 'say' && act.reply !== undefined ? { reply: act.reply } : {}),
|
|
48
|
-
};
|
|
49
|
-
return `${ACT_MARKER_PREFIX} ${JSON.stringify(marker)} -->`;
|
|
50
|
-
}
|
|
51
|
-
function actNeedsBody(act) {
|
|
52
|
-
return act.kind === 'say' || act.kind === 'done' || act.kind === 'hold';
|
|
53
|
-
}
|
|
54
|
-
export function renderArtifactAct(act, opts = {}) {
|
|
55
|
-
const head = opts.first ? '' : '\n';
|
|
56
|
-
const out = [`${head}${renderActMarker(act)}`, `### ${act.actor}`, `_${act.kind} · ${formatTimestamp(act.at)}_`];
|
|
57
|
-
if (actNeedsBody(act))
|
|
58
|
-
out.push('', quoteBody('body' in act ? act.body ?? '' : ''));
|
|
59
|
-
return out.join('\n');
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { TextDecoder } from 'node:util';
|
|
5
|
+
import zlib from 'node:zlib';
|
|
6
|
+
import { isWakeRouteKind, InternalSquareError, nameKey, SquareError, } from './model.js';
|
|
7
|
+
import { parseActivityId } from './square-core.js';
|
|
8
|
+
const SQUARE_MAGIC = Buffer.from('SQUARE01', 'ascii');
|
|
9
|
+
const LENGTH_BYTES = 4;
|
|
10
|
+
const DIGEST_BYTES = 32;
|
|
11
|
+
const HEADER_BYTES = SQUARE_MAGIC.length + LENGTH_BYTES + DIGEST_BYTES;
|
|
12
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true });
|
|
13
|
+
function invalidArtifact(detail) {
|
|
14
|
+
return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
|
|
15
|
+
}
|
|
16
|
+
function isObject(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
60
18
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
19
|
+
function hasExactKeys(value, required, optional = []) {
|
|
20
|
+
const allowed = new Set([...required, ...optional]);
|
|
21
|
+
return required.every((key) => Object.hasOwn(value, key))
|
|
22
|
+
&& Object.keys(value).every((key) => allowed.has(key));
|
|
23
|
+
}
|
|
24
|
+
function isFiniteNumber(value) {
|
|
25
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
26
|
+
}
|
|
27
|
+
function isNonNegativeInteger(value) {
|
|
28
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
29
|
+
}
|
|
30
|
+
function isNonblankString(value) {
|
|
31
|
+
return typeof value === 'string' && value.length > 0;
|
|
32
|
+
}
|
|
33
|
+
function isStringArray(value) {
|
|
34
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
35
|
+
}
|
|
36
|
+
function validateReadCursor(value) {
|
|
37
|
+
return isObject(value)
|
|
38
|
+
&& hasExactKeys(value, ['consumedThroughIndex', 'updatedAt'])
|
|
39
|
+
&& Number.isSafeInteger(value.consumedThroughIndex)
|
|
40
|
+
&& value.consumedThroughIndex >= -1
|
|
41
|
+
&& isFiniteNumber(value.updatedAt);
|
|
42
|
+
}
|
|
43
|
+
function validateDeliveryReceipt(value) {
|
|
44
|
+
return isObject(value)
|
|
45
|
+
&& hasExactKeys(value, ['status', 'at'])
|
|
46
|
+
&& value.status === 'delivered'
|
|
47
|
+
&& isFiniteNumber(value.at);
|
|
48
|
+
}
|
|
49
|
+
function validateWatchLease(value) {
|
|
50
|
+
if (!isObject(value)
|
|
51
|
+
|| !hasExactKeys(value, ['leaseId', 'heartbeatAt', 'expiresAt'], ['ownerId', 'filter'])
|
|
52
|
+
|| !isNonblankString(value.leaseId)
|
|
53
|
+
|| (value.ownerId !== undefined && !isNonblankString(value.ownerId))
|
|
54
|
+
|| !isFiniteNumber(value.heartbeatAt)
|
|
55
|
+
|| !isFiniteNumber(value.expiresAt)
|
|
56
|
+
|| value.expiresAt < value.heartbeatAt)
|
|
57
|
+
return false;
|
|
58
|
+
if (value.filter === undefined)
|
|
59
|
+
return true;
|
|
60
|
+
return isObject(value.filter)
|
|
61
|
+
&& hasExactKeys(value.filter, [], ['participants', 'mention'])
|
|
62
|
+
&& (value.filter.participants === undefined || isStringArray(value.filter.participants))
|
|
63
|
+
&& (value.filter.mention === undefined || typeof value.filter.mention === 'string');
|
|
64
|
+
}
|
|
65
|
+
function validateNotifyLease(value) {
|
|
66
|
+
if (!isObject(value)
|
|
67
|
+
|| !hasExactKeys(value, ['leaseId', 'expiresAt', 'phase'], ['attemptN', 'routeKind'])
|
|
68
|
+
|| !isNonblankString(value.leaseId)
|
|
69
|
+
|| !isFiniteNumber(value.expiresAt)
|
|
70
|
+
|| (value.phase !== 'claimed' && value.phase !== 'dispatching')
|
|
71
|
+
|| (value.attemptN !== undefined && (!Number.isSafeInteger(value.attemptN) || value.attemptN <= 0))
|
|
72
|
+
|| (value.routeKind !== undefined && !isWakeRouteKind(value.routeKind)))
|
|
73
|
+
return false;
|
|
74
|
+
return value.phase !== 'dispatching' || (value.attemptN !== undefined && value.routeKind !== undefined);
|
|
70
75
|
}
|
|
71
|
-
function
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
`hard_cap: ${formatHardCap(doc.hardCap)}`,
|
|
75
|
-
...(doc.throttlePerMinute === undefined ? [] : [`throttle_per_minute: ${doc.throttlePerMinute}`]),
|
|
76
|
-
`format_version: ${CURRENT_FORMAT_VERSION}`,
|
|
77
|
-
'---',
|
|
78
|
-
].join('\n');
|
|
79
|
-
}
|
|
80
|
-
export function renderSquare(opts, snippet) {
|
|
81
|
-
const templateFile = opts.template
|
|
82
|
-
? new URL(`../templates/${opts.template}.md`, import.meta.url)
|
|
83
|
-
: new URL('../template.md', import.meta.url);
|
|
84
|
-
const template = fs.readFileSync(templateFile, 'utf8');
|
|
85
|
-
const body = template.replace(/^---\n[\s\S]*?\n---\n?/, '').trimStart();
|
|
86
|
-
const genericGuide = fs.readFileSync(new URL('../guides/participant.md', import.meta.url), 'utf8').trim();
|
|
87
|
-
let templateGuide = '';
|
|
88
|
-
if (opts.template) {
|
|
89
|
-
try {
|
|
90
|
-
templateGuide = '\n\n' + fs.readFileSync(new URL(`../guides/${opts.template}.md`, import.meta.url), 'utf8').trim();
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// No template-specific guide.
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const training = [
|
|
97
|
-
'---',
|
|
98
|
-
'',
|
|
99
|
-
WARMUP_HEADING,
|
|
100
|
-
WARMUP_MARKER,
|
|
101
|
-
'',
|
|
102
|
-
genericGuide + templateGuide,
|
|
103
|
-
'',
|
|
104
|
-
].join('\n');
|
|
105
|
-
const normalizedSnippet = snippet.replace(/\r\n/g, '\n').trim();
|
|
106
|
-
return [
|
|
107
|
-
renderFrontmatter({
|
|
108
|
-
hardCap: opts.hardCap,
|
|
109
|
-
throttlePerMinute: opts.throttlePerMinute,
|
|
110
|
-
}),
|
|
111
|
-
'',
|
|
112
|
-
normalizedSnippet,
|
|
113
|
-
'',
|
|
114
|
-
training,
|
|
115
|
-
body,
|
|
116
|
-
].join('\n');
|
|
117
|
-
}
|
|
118
|
-
function sidecarPath(squarePath) {
|
|
119
|
-
return `${squarePath}.runtime.json`;
|
|
120
|
-
}
|
|
121
|
-
function invalidRuntimeSidecar(squarePath, detail) {
|
|
122
|
-
return new SquareError('invalid_args', `Invalid square runtime sidecar ${sidecarPath(squarePath)}: ${detail}`);
|
|
123
|
-
}
|
|
124
|
-
function validateRuntimeSidecar(squarePath, value) {
|
|
125
|
-
if (!isObject(value))
|
|
126
|
-
throw invalidRuntimeSidecar(squarePath, 'expected a JSON object.');
|
|
127
|
-
if (value.version !== 2)
|
|
128
|
-
throw invalidRuntimeSidecar(squarePath, 'unsupported or missing version.');
|
|
129
|
-
if (typeof value.nextActIndex !== 'number' || !Number.isInteger(value.nextActIndex) || value.nextActIndex < 0) {
|
|
130
|
-
throw invalidRuntimeSidecar(squarePath, 'nextActIndex must be a non-negative integer.');
|
|
131
|
-
}
|
|
132
|
-
if (!isObject(value.cursors) || !Object.values(value.cursors).every(isReadCursor)) {
|
|
133
|
-
throw invalidRuntimeSidecar(squarePath, 'cursors contains an invalid read cursor.');
|
|
134
|
-
}
|
|
135
|
-
if (!isObject(value.deliveryReceipts) || !Object.values(value.deliveryReceipts).every(isDeliveryReceiptMap)) {
|
|
136
|
-
throw invalidRuntimeSidecar(squarePath, 'deliveryReceipts contains an invalid receipt map.');
|
|
137
|
-
}
|
|
138
|
-
if (!isObject(value.leases) || !Object.values(value.leases).every(isWatchLease)) {
|
|
139
|
-
throw invalidRuntimeSidecar(squarePath, 'leases contains an invalid watch lease.');
|
|
140
|
-
}
|
|
141
|
-
if (value.notifyLeases !== undefined && (!isObject(value.notifyLeases) || !Object.values(value.notifyLeases).every(isNotifyLease))) {
|
|
142
|
-
throw invalidRuntimeSidecar(squarePath, 'notifyLeases contains an invalid notify lease.');
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
version: 2,
|
|
146
|
-
nextActIndex: value.nextActIndex,
|
|
147
|
-
cursors: value.cursors,
|
|
148
|
-
deliveryReceipts: value.deliveryReceipts,
|
|
149
|
-
leases: value.leases,
|
|
150
|
-
notifyLeases: (value.notifyLeases ?? {}),
|
|
151
|
-
};
|
|
76
|
+
function validateRecord(value, item) {
|
|
77
|
+
return isObject(value)
|
|
78
|
+
&& Object.entries(value).every(([key, candidate]) => key.length > 0 && item(candidate));
|
|
152
79
|
}
|
|
153
|
-
|
|
154
|
-
|
|
80
|
+
function parseNotifyLeaseKey(key) {
|
|
81
|
+
let parsed;
|
|
155
82
|
try {
|
|
156
|
-
|
|
157
|
-
let parsed;
|
|
158
|
-
try {
|
|
159
|
-
parsed = JSON.parse(raw);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
throw invalidRuntimeSidecar(squarePath, 'malformed JSON.');
|
|
163
|
-
}
|
|
164
|
-
return validateRuntimeSidecar(squarePath, parsed);
|
|
83
|
+
parsed = JSON.parse(key);
|
|
165
84
|
}
|
|
166
|
-
catch
|
|
167
|
-
|
|
168
|
-
return fallbackRuntime;
|
|
169
|
-
}
|
|
170
|
-
throw err;
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
171
87
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
export function saveRuntimeSidecar(squarePath, runtime) {
|
|
180
|
-
const target = sidecarPath(squarePath);
|
|
181
|
-
const normalized = validateRuntimeSidecar(squarePath, runtime);
|
|
182
|
-
const temporary = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
183
|
-
try {
|
|
184
|
-
fs.writeFileSync(temporary, JSON.stringify(normalized, null, 2));
|
|
185
|
-
fs.renameSync(temporary, target);
|
|
88
|
+
if (!Array.isArray(parsed) || parsed.length !== 2)
|
|
89
|
+
return undefined;
|
|
90
|
+
const [id, name] = parsed;
|
|
91
|
+
if (typeof id !== 'string' || typeof name !== 'string' || name.length === 0 || nameKey(name) !== name) {
|
|
92
|
+
return undefined;
|
|
186
93
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
94
|
+
const index = parseActivityId(id);
|
|
95
|
+
if (index === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
if (JSON.stringify([id, name]) !== key)
|
|
98
|
+
return undefined;
|
|
99
|
+
return { index, name };
|
|
100
|
+
}
|
|
101
|
+
function validateRuntime(value) {
|
|
102
|
+
if (!isObject(value)
|
|
103
|
+
|| !hasExactKeys(value, ['nextActIndex', 'cursors', 'deliveryReceipts', 'leases', 'notifyLeases'])
|
|
104
|
+
|| !isNonNegativeInteger(value.nextActIndex)
|
|
105
|
+
|| !validateRecord(value.cursors, validateReadCursor)
|
|
106
|
+
|| !validateRecord(value.leases, validateWatchLease)
|
|
107
|
+
|| !validateRecord(value.notifyLeases, validateNotifyLease)
|
|
108
|
+
|| !isObject(value.deliveryReceipts))
|
|
109
|
+
return false;
|
|
110
|
+
return Object.entries(value.deliveryReceipts).every(([name, receipts]) => name.length > 0
|
|
111
|
+
&& isObject(receipts)
|
|
112
|
+
&& Object.entries(receipts).every(([id, receipt]) => parseActivityId(id) !== undefined && validateDeliveryReceipt(receipt)));
|
|
113
|
+
}
|
|
114
|
+
function validateAssignedRuntimeReferences(runtime) {
|
|
115
|
+
const bound = runtime.nextActIndex;
|
|
116
|
+
for (const cursor of Object.values(runtime.cursors)) {
|
|
117
|
+
if (cursor.consumedThroughIndex !== -1 && cursor.consumedThroughIndex >= bound)
|
|
118
|
+
return 'future';
|
|
119
|
+
}
|
|
120
|
+
for (const receipts of Object.values(runtime.deliveryReceipts)) {
|
|
121
|
+
for (const id of Object.keys(receipts)) {
|
|
122
|
+
const index = parseActivityId(id);
|
|
123
|
+
if (index === undefined)
|
|
124
|
+
return 'malformed';
|
|
125
|
+
if (index >= bound)
|
|
126
|
+
return 'future';
|
|
190
127
|
}
|
|
191
|
-
catch { }
|
|
192
|
-
throw error;
|
|
193
128
|
}
|
|
129
|
+
for (const key of Object.keys(runtime.notifyLeases)) {
|
|
130
|
+
const parsed = parseNotifyLeaseKey(key);
|
|
131
|
+
if (parsed === undefined)
|
|
132
|
+
return 'malformed';
|
|
133
|
+
if (parsed.index >= bound)
|
|
134
|
+
return 'future';
|
|
135
|
+
}
|
|
136
|
+
return 'ok';
|
|
194
137
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return [
|
|
198
|
-
renderFrontmatter({ hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute }),
|
|
199
|
-
'',
|
|
200
|
-
...doc.preamble,
|
|
201
|
-
...(doc.preamble.length > 0 ? [''] : []),
|
|
202
|
-
...warmup,
|
|
203
|
-
'',
|
|
204
|
-
ACTIVITIES_HEADING,
|
|
205
|
-
ACTIVITIES_MARKER,
|
|
206
|
-
...(doc.acts.length === 0 ? [] : ['', doc.acts.map((act, index) => renderArtifactAct(act, { first: index === 0 })).join('\n')]),
|
|
207
|
-
'',
|
|
208
|
-
].join('\n');
|
|
209
|
-
}
|
|
210
|
-
function renderWarmupSection(warmup) {
|
|
211
|
-
if (warmup[0]?.trim() === WARMUP_HEADING)
|
|
212
|
-
return [warmup[0], WARMUP_MARKER, ...warmup.slice(1)];
|
|
213
|
-
return [WARMUP_MARKER, ...warmup];
|
|
214
|
-
}
|
|
215
|
-
export function parsePreamble(text) {
|
|
216
|
-
const lines = text.split('\n');
|
|
217
|
-
const frontmatterEnd = lines.findIndex((line, index) => index > 0 && line === '---');
|
|
218
|
-
if (lines[0] !== '---' || frontmatterEnd < 0)
|
|
219
|
-
throw new SquareError('invalid_args', 'Invalid square: missing frontmatter.');
|
|
220
|
-
const marker = lines.findIndex((line) => line.trim() === WARMUP_MARKER);
|
|
221
|
-
if (marker < 0)
|
|
222
|
-
throw new SquareError('invalid_args', 'Invalid square: missing embedded warmup.');
|
|
223
|
-
const warmupStart = marker > 0 && lines[marker - 1].trim() === WARMUP_HEADING ? marker - 1 : marker;
|
|
224
|
-
const out = lines.slice(frontmatterEnd + 1, warmupStart);
|
|
225
|
-
trimBlankEdges(out);
|
|
226
|
-
return out;
|
|
227
|
-
}
|
|
228
|
-
function trimBlankEdges(lines) {
|
|
229
|
-
while (lines.length > 0 && lines[0] === '')
|
|
230
|
-
lines.shift();
|
|
231
|
-
while (lines.length > 0 && lines[lines.length - 1] === '')
|
|
232
|
-
lines.pop();
|
|
233
|
-
}
|
|
234
|
-
function parseFrontmatter(text) {
|
|
235
|
-
const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
236
|
-
if (!match)
|
|
237
|
-
throw new SquareError('invalid_args', 'Invalid square: missing frontmatter.');
|
|
238
|
-
return match[1];
|
|
239
|
-
}
|
|
240
|
-
export function isObject(value) {
|
|
241
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
138
|
+
function validateActor(value, required) {
|
|
139
|
+
return required ? isNonblankString(value) : value === undefined || isNonblankString(value);
|
|
242
140
|
}
|
|
243
|
-
|
|
244
|
-
if (!isObject(value)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
value.consumedThroughIndex >= -1 &&
|
|
249
|
-
typeof value.updatedAt === 'number' &&
|
|
250
|
-
Number.isFinite(value.updatedAt));
|
|
251
|
-
}
|
|
252
|
-
export function isDeliveryReceipt(value) {
|
|
253
|
-
if (!isObject(value))
|
|
254
|
-
return false;
|
|
255
|
-
if (value.status !== 'delivered')
|
|
256
|
-
return false;
|
|
257
|
-
if (typeof value.at !== 'number' || !Number.isFinite(value.at))
|
|
141
|
+
function validateStoredAct(value) {
|
|
142
|
+
if (!isObject(value)
|
|
143
|
+
|| typeof value.kind !== 'string'
|
|
144
|
+
|| !isNonNegativeInteger(value.index)
|
|
145
|
+
|| !isFiniteNumber(value.at))
|
|
258
146
|
return false;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
147
|
+
switch (value.kind) {
|
|
148
|
+
case 'join':
|
|
149
|
+
return hasExactKeys(value, ['kind', 'actor', 'at', 'index']) && validateActor(value.actor, true);
|
|
150
|
+
case 'done':
|
|
151
|
+
return hasExactKeys(value, ['kind', 'actor', 'at', 'index'], ['body'])
|
|
152
|
+
&& validateActor(value.actor, true)
|
|
153
|
+
&& (value.body === undefined || typeof value.body === 'string');
|
|
154
|
+
case 'say':
|
|
155
|
+
return hasExactKeys(value, ['kind', 'actor', 'at', 'body', 'index'], ['reach', 'reply'])
|
|
156
|
+
&& validateActor(value.actor, true)
|
|
157
|
+
&& typeof value.body === 'string'
|
|
158
|
+
&& (value.reach === undefined || value.reach === 'bell')
|
|
159
|
+
&& (value.reply === undefined || isNonNegativeInteger(value.reply));
|
|
160
|
+
case 'hold':
|
|
161
|
+
return hasExactKeys(value, ['kind', 'at', 'index'], ['actor', 'body'])
|
|
162
|
+
&& validateActor(value.actor, false)
|
|
163
|
+
&& (value.body === undefined || typeof value.body === 'string');
|
|
164
|
+
case 'resume':
|
|
165
|
+
return hasExactKeys(value, ['kind', 'at', 'index'], ['actor']) && validateActor(value.actor, false);
|
|
166
|
+
case 'read':
|
|
167
|
+
return hasExactKeys(value, ['kind', 'actor', 'at', 'through', 'index'])
|
|
168
|
+
&& validateActor(value.actor, true)
|
|
169
|
+
&& isNonNegativeInteger(value.through);
|
|
170
|
+
default:
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function validateActs(value) {
|
|
175
|
+
if (!Array.isArray(value) || !value.every(validateStoredAct))
|
|
264
176
|
return false;
|
|
177
|
+
let previous = -1;
|
|
178
|
+
for (const act of value) {
|
|
179
|
+
if (act.index <= previous)
|
|
180
|
+
return false;
|
|
181
|
+
if (act.kind === 'say' && act.reply !== undefined && act.reply >= act.index)
|
|
182
|
+
return false;
|
|
183
|
+
previous = act.index;
|
|
184
|
+
}
|
|
265
185
|
return true;
|
|
266
186
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return mention === undefined || typeof mention === 'string';
|
|
292
|
-
}
|
|
293
|
-
export function isNotifyLease(value) {
|
|
294
|
-
if (!isObject(value))
|
|
295
|
-
return false;
|
|
296
|
-
if (typeof value.leaseId !== 'string' || value.leaseId === '')
|
|
297
|
-
return false;
|
|
298
|
-
if (typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt))
|
|
299
|
-
return false;
|
|
300
|
-
if (value.phase !== 'claimed' && value.phase !== 'dispatching')
|
|
301
|
-
return false;
|
|
302
|
-
if (value.attemptN !== undefined && (typeof value.attemptN !== 'number' || !Number.isInteger(value.attemptN) || value.attemptN <= 0))
|
|
303
|
-
return false;
|
|
304
|
-
if (value.routeKind !== undefined && !isWakeRouteKind(value.routeKind))
|
|
305
|
-
return false;
|
|
306
|
-
return value.phase !== 'dispatching' || (value.attemptN !== undefined && value.routeKind !== undefined);
|
|
187
|
+
function validateSquareState(value) {
|
|
188
|
+
if (!isObject(value)
|
|
189
|
+
|| !hasExactKeys(value, ['hardCap', 'preamble', 'warmup', 'acts', 'runtime'], ['throttlePerMinute'])
|
|
190
|
+
|| !(value.hardCap === null || (Number.isSafeInteger(value.hardCap) && value.hardCap > 0))
|
|
191
|
+
|| (value.throttlePerMinute !== undefined
|
|
192
|
+
&& (!Number.isSafeInteger(value.throttlePerMinute) || value.throttlePerMinute <= 0))
|
|
193
|
+
|| !isStringArray(value.preamble)
|
|
194
|
+
|| !isStringArray(value.warmup)
|
|
195
|
+
|| !validateActs(value.acts)
|
|
196
|
+
|| !validateRuntime(value.runtime)) {
|
|
197
|
+
throw invalidArtifact('snapshot schema is malformed.');
|
|
198
|
+
}
|
|
199
|
+
const acts = value.acts;
|
|
200
|
+
const runtime = value.runtime;
|
|
201
|
+
const historyBoundary = acts.at(-1)?.index ?? -1;
|
|
202
|
+
if (runtime.nextActIndex <= historyBoundary) {
|
|
203
|
+
throw invalidArtifact('nextActIndex is behind the activity history.');
|
|
204
|
+
}
|
|
205
|
+
const references = validateAssignedRuntimeReferences(runtime);
|
|
206
|
+
if (references === 'malformed')
|
|
207
|
+
throw invalidArtifact('snapshot schema is malformed.');
|
|
208
|
+
if (references === 'future')
|
|
209
|
+
throw invalidArtifact('runtime references an unassigned activity index.');
|
|
210
|
+
return value;
|
|
307
211
|
}
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const match = frontmatter.match(/^format_version:\s*(\d+)\s*$/m);
|
|
336
|
-
if (!match) {
|
|
337
|
-
throw invalidVersionGuidance('Invalid square: missing format_version in frontmatter.');
|
|
212
|
+
function encodeEnvelope(magic, value) {
|
|
213
|
+
const json = Buffer.from(JSON.stringify(value), 'utf8');
|
|
214
|
+
const payload = zlib.gzipSync(json, { level: 9 });
|
|
215
|
+
if (payload.length > 0xffff_ffff)
|
|
216
|
+
throw invalidArtifact('compressed payload is too large.');
|
|
217
|
+
const header = Buffer.alloc(HEADER_BYTES);
|
|
218
|
+
magic.copy(header, 0);
|
|
219
|
+
header.writeUInt32BE(payload.length, magic.length);
|
|
220
|
+
crypto.createHash('sha256').update(payload).digest().copy(header, magic.length + LENGTH_BYTES);
|
|
221
|
+
return Buffer.concat([header, payload]);
|
|
222
|
+
}
|
|
223
|
+
function decodeEnvelope(bytes, magic) {
|
|
224
|
+
if (bytes.length < HEADER_BYTES)
|
|
225
|
+
throw invalidArtifact('truncated header.');
|
|
226
|
+
if (!bytes.subarray(0, magic.length).equals(magic))
|
|
227
|
+
throw invalidArtifact('bad magic or unsupported format version.');
|
|
228
|
+
const length = bytes.readUInt32BE(magic.length);
|
|
229
|
+
if (bytes.length !== HEADER_BYTES + length)
|
|
230
|
+
throw invalidArtifact('payload length does not match the file.');
|
|
231
|
+
const expected = bytes.subarray(magic.length + LENGTH_BYTES, HEADER_BYTES);
|
|
232
|
+
const payload = bytes.subarray(HEADER_BYTES);
|
|
233
|
+
const actual = crypto.createHash('sha256').update(payload).digest();
|
|
234
|
+
if (!crypto.timingSafeEqual(expected, actual))
|
|
235
|
+
throw invalidArtifact('payload digest mismatch.');
|
|
236
|
+
let inflated;
|
|
237
|
+
try {
|
|
238
|
+
inflated = zlib.gunzipSync(payload);
|
|
338
239
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
throw invalidVersionGuidance(`Invalid square: unsupported format_version ${version} (expected ${CURRENT_FORMAT_VERSION}).`);
|
|
240
|
+
catch {
|
|
241
|
+
throw invalidArtifact('payload is not valid gzip data.');
|
|
342
242
|
}
|
|
343
|
-
return version;
|
|
344
|
-
}
|
|
345
|
-
export function parseSquare(text) {
|
|
346
|
-
parseFormatVersion(text);
|
|
347
|
-
const runtime = emptyRuntimeState(0);
|
|
348
|
-
const parsedActs = parseActs(text);
|
|
349
|
-
runtime.nextActIndex = parsedActs.nextActIndex;
|
|
350
|
-
return {
|
|
351
|
-
hardCap: parseCap(text),
|
|
352
|
-
throttlePerMinute: parseThrottle(text),
|
|
353
|
-
preamble: parsePreamble(text),
|
|
354
|
-
warmup: parseWarmup(text),
|
|
355
|
-
acts: parsedActs.acts,
|
|
356
|
-
runtime,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
export function loadSquare(squarePath) {
|
|
360
243
|
let text;
|
|
361
244
|
try {
|
|
362
|
-
text =
|
|
245
|
+
text = utf8.decode(inflated);
|
|
363
246
|
}
|
|
364
|
-
catch
|
|
365
|
-
|
|
366
|
-
throw new SquareError('not_found', `square file not found: ${squarePath}`);
|
|
367
|
-
}
|
|
368
|
-
throw err;
|
|
247
|
+
catch {
|
|
248
|
+
throw invalidArtifact('payload is not valid UTF-8.');
|
|
369
249
|
}
|
|
370
|
-
const doc = parseSquare(text);
|
|
371
|
-
const markdownRuntime = doc.runtime;
|
|
372
|
-
const sidecar = loadRuntimeSidecar(squarePath, markdownRuntime);
|
|
373
|
-
// Markdown owns the activity history; runtime state owns delivery metadata.
|
|
374
|
-
// Preserve whichever history boundary is furthest ahead so a missing or
|
|
375
|
-
// stale sidecar can never cause an index to be reused.
|
|
376
|
-
doc.runtime = mergeRuntimeState(markdownRuntime, sidecar);
|
|
377
|
-
return doc;
|
|
378
|
-
}
|
|
379
|
-
function isSquareMarker(line) {
|
|
380
|
-
return /^<!-- square:(warmup|activities) -->$/.test(line.trim());
|
|
381
|
-
}
|
|
382
|
-
function findActivitiesMarker(lines, after = -1) {
|
|
383
|
-
return lines.findIndex((line, index) => index > after && line.trim() === ACTIVITIES_MARKER);
|
|
384
|
-
}
|
|
385
|
-
export function parseWarmup(text) {
|
|
386
|
-
const lines = text.split('\n');
|
|
387
|
-
const marker = lines.findIndex((line) => line.trim() === WARMUP_MARKER);
|
|
388
|
-
if (marker < 0)
|
|
389
|
-
throw new SquareError('invalid_args', 'Invalid square: missing embedded warmup.');
|
|
390
|
-
const start = marker > 0 && lines[marker - 1].trim() === WARMUP_HEADING ? marker - 1 : marker;
|
|
391
|
-
const endMarker = findActivitiesMarker(lines, start);
|
|
392
|
-
if (endMarker < 0)
|
|
393
|
-
throw new SquareError('invalid_args', 'Invalid square: missing ACTIVITIES section.');
|
|
394
|
-
const end = lines[endMarker].trim() === ACTIVITIES_MARKER && endMarker > 0 && lines[endMarker - 1].trim() === ACTIVITIES_HEADING
|
|
395
|
-
? endMarker - 1
|
|
396
|
-
: endMarker;
|
|
397
|
-
const out = lines.slice(start, end).filter((line) => !isSquareMarker(line));
|
|
398
|
-
trimSection(out);
|
|
399
|
-
return out;
|
|
400
|
-
}
|
|
401
|
-
function parseReach(value) {
|
|
402
|
-
if (value === undefined)
|
|
403
|
-
return undefined;
|
|
404
|
-
if (value === 'bell')
|
|
405
|
-
return 'bell';
|
|
406
|
-
if (isObject(value) && typeof value.beside === 'string')
|
|
407
|
-
return { beside: value.beside };
|
|
408
|
-
throw new SquareError('invalid_args', 'Invalid square: malformed act reach metadata.');
|
|
409
|
-
}
|
|
410
|
-
export function parseActMarker(line) {
|
|
411
|
-
if (line === undefined || !line.startsWith(ACT_MARKER_PREFIX))
|
|
412
|
-
return null;
|
|
413
|
-
const match = line.match(/^<!-- square:act\s+(\{.*\})\s*-->$/);
|
|
414
|
-
if (!match)
|
|
415
|
-
return null;
|
|
416
|
-
let parsed;
|
|
417
250
|
try {
|
|
418
|
-
|
|
251
|
+
return JSON.parse(text);
|
|
419
252
|
}
|
|
420
253
|
catch {
|
|
421
|
-
throw
|
|
422
|
-
}
|
|
423
|
-
if (!isObject(parsed))
|
|
424
|
-
throw new SquareError('invalid_args', 'Invalid square: act marker is missing index metadata.');
|
|
425
|
-
const index = parsed.index;
|
|
426
|
-
if (typeof index !== 'number' || !Number.isInteger(index) || index < 0) {
|
|
427
|
-
throw new SquareError('invalid_args', 'Invalid square: act marker is missing index metadata.');
|
|
254
|
+
throw invalidArtifact('payload is not valid JSON.');
|
|
428
255
|
}
|
|
256
|
+
}
|
|
257
|
+
export function emptyRuntimeState(nextActIndex = 0) {
|
|
429
258
|
return {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
...(parsed.reply !== undefined ? { reply: parseReply(parsed.reply) } : {}),
|
|
259
|
+
nextActIndex,
|
|
260
|
+
cursors: {},
|
|
261
|
+
deliveryReceipts: {},
|
|
262
|
+
leases: {},
|
|
263
|
+
notifyLeases: {},
|
|
436
264
|
};
|
|
437
265
|
}
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
return value;
|
|
266
|
+
function normalizedLines(value) {
|
|
267
|
+
const normalized = value.replace(/\r\n/g, '\n').trim();
|
|
268
|
+
return normalized === '' ? [] : normalized.split('\n');
|
|
443
269
|
}
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
if (marker.kind !== kind) {
|
|
449
|
-
throw new SquareError('invalid_args', `Invalid square: act marker kind ${marker.kind} does not match ${kind}.`);
|
|
450
|
-
}
|
|
451
|
-
if (!sameName(marker.actor, actor)) {
|
|
452
|
-
throw new SquareError('invalid_args', `Invalid square: act marker actor ${marker.actor} does not match ${actor}.`);
|
|
453
|
-
}
|
|
454
|
-
if (marker.at === undefined) {
|
|
455
|
-
throw new SquareError('invalid_args', `Invalid square: act marker is missing timestamp for ${kind} ${actor}.`);
|
|
456
|
-
}
|
|
457
|
-
if (marker.at !== head.at) {
|
|
458
|
-
throw new SquareError('invalid_args', `Invalid square: act marker timestamp does not match ${kind} ${actor}.`);
|
|
270
|
+
function readGuide(name) {
|
|
271
|
+
try {
|
|
272
|
+
return fs.readFileSync(new URL(`../guides/${name}.md`, import.meta.url), 'utf8').trim();
|
|
459
273
|
}
|
|
460
|
-
|
|
461
|
-
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error.code === 'ENOENT') {
|
|
276
|
+
throw new SquareError('invalid_args', `Unknown square guide: ${name}`);
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
462
279
|
}
|
|
463
|
-
return {
|
|
464
|
-
index: marker.index,
|
|
465
|
-
...(marker.reach !== undefined ? { reach: marker.reach } : {}),
|
|
466
|
-
...(marker.reply !== undefined ? { reply: marker.reply } : {}),
|
|
467
|
-
};
|
|
468
280
|
}
|
|
469
|
-
export function
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
throw new SquareError('invalid_args', 'Invalid square: missing ACTIVITIES section.');
|
|
474
|
-
let start = marker + 1;
|
|
475
|
-
while (start < lines.length && (lines[start] === '' || isSquareMarker(lines[start])))
|
|
476
|
-
start++;
|
|
477
|
-
return lines.slice(start);
|
|
478
|
-
}
|
|
479
|
-
export function parseParticipantHeading(line) {
|
|
480
|
-
const match = line.match(/^### (.+)\s*$/);
|
|
481
|
-
return match ? match[1] : null;
|
|
482
|
-
}
|
|
483
|
-
export function parseActLine(line, actor) {
|
|
484
|
-
const match = line.match(/^_(say|join|done|hold|resume) · ([^_]+)_\s*$/);
|
|
485
|
-
if (!match)
|
|
486
|
-
return null;
|
|
487
|
-
const at = parseTimestamp(match[2]);
|
|
488
|
-
if (!Number.isFinite(at) || !V2_KINDS.has(match[1]))
|
|
489
|
-
return null;
|
|
490
|
-
return { kind: match[1], actor, at };
|
|
491
|
-
}
|
|
492
|
-
function isActSeparator(lines, index) {
|
|
493
|
-
return lines[index]?.trim().startsWith(ACT_MARKER_PREFIX) === true;
|
|
494
|
-
}
|
|
495
|
-
function parseActBlock(blockLines) {
|
|
496
|
-
let i = 0;
|
|
497
|
-
const marker = parseActMarker(blockLines[i]?.trim());
|
|
498
|
-
if (!marker)
|
|
499
|
-
throw new SquareError('invalid_args', `Invalid square: expected act marker, got: ${blockLines[i] ?? ''}`);
|
|
500
|
-
i++;
|
|
501
|
-
const actor = parseParticipantHeading(blockLines[i] ?? '');
|
|
502
|
-
if (!actor)
|
|
503
|
-
throw new SquareError('invalid_args', `Invalid square: expected participant heading, got: ${blockLines[i] ?? ''}`);
|
|
504
|
-
i++;
|
|
505
|
-
if (blockLines[i] === '')
|
|
506
|
-
i++;
|
|
507
|
-
const head = parseActLine(blockLines[i] ?? '', actor);
|
|
508
|
-
if (!head)
|
|
509
|
-
throw new SquareError('invalid_args', `Invalid square: expected act line, got: ${blockLines[i] ?? ''}`);
|
|
510
|
-
i++;
|
|
511
|
-
const meta = normalizeActMeta(marker, head.kind, actor, head);
|
|
512
|
-
let body = '';
|
|
513
|
-
if (head.kind === 'say' || head.kind === 'done' || head.kind === 'hold') {
|
|
514
|
-
if (blockLines[i] === '')
|
|
515
|
-
i++;
|
|
516
|
-
body = unquoteBody(blockLines.slice(i));
|
|
517
|
-
}
|
|
281
|
+
export function createSquareState(options, snippet) {
|
|
282
|
+
const guides = [readGuide('participant')];
|
|
283
|
+
if (options.template !== undefined)
|
|
284
|
+
guides.push(readGuide(options.template));
|
|
518
285
|
return {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
286
|
+
hardCap: options.hardCap,
|
|
287
|
+
...(options.throttlePerMinute === undefined ? {} : { throttlePerMinute: options.throttlePerMinute }),
|
|
288
|
+
preamble: normalizedLines(snippet),
|
|
289
|
+
warmup: normalizedLines(guides.join('\n\n')),
|
|
290
|
+
acts: [],
|
|
291
|
+
runtime: emptyRuntimeState(),
|
|
524
292
|
};
|
|
525
293
|
}
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
let end = i + 1;
|
|
538
|
-
while (end < lines.length && !isActSeparator(lines, end))
|
|
539
|
-
end++;
|
|
540
|
-
const act = parseActBlock(lines.slice(i, end));
|
|
541
|
-
if (detectedFirst === -1)
|
|
542
|
-
detectedFirst = act.index;
|
|
543
|
-
if (indexes.has(act.index))
|
|
544
|
-
throw new SquareError('invalid_args', `Invalid square: duplicate act index ${act.index}.`);
|
|
545
|
-
indexes.add(act.index);
|
|
546
|
-
maxIndex = Math.max(maxIndex, act.index);
|
|
547
|
-
acts.push(act);
|
|
548
|
-
i = end;
|
|
294
|
+
export function encodeSquare(squareState) {
|
|
295
|
+
return encodeEnvelope(SQUARE_MAGIC, validateSquareState(squareState));
|
|
296
|
+
}
|
|
297
|
+
export function decodeSquare(bytes) {
|
|
298
|
+
return validateSquareState(decodeEnvelope(bytes, SQUARE_MAGIC));
|
|
299
|
+
}
|
|
300
|
+
function readArtifact(squarePath) {
|
|
301
|
+
try {
|
|
302
|
+
return fs.readFileSync(squarePath);
|
|
549
303
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
throw new SquareError('invalid_args', 'Invalid square: act indexes must be contiguous from the first retained index.');
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error.code === 'ENOENT') {
|
|
306
|
+
throw new InternalSquareError('not_found', `square file not found: ${squarePath}`);
|
|
554
307
|
}
|
|
308
|
+
throw error;
|
|
555
309
|
}
|
|
556
|
-
return { acts, nextActIndex: indexes.size > 0 ? maxIndex + 1 : 0 };
|
|
557
|
-
}
|
|
558
|
-
function isDivider(line) {
|
|
559
|
-
return line.trim() === '---';
|
|
560
|
-
}
|
|
561
|
-
function trimSection(lines) {
|
|
562
|
-
while (lines.length > 0 && (lines[0] === '' || isDivider(lines[0])))
|
|
563
|
-
lines.shift();
|
|
564
|
-
while (lines.length > 0 && (lines[lines.length - 1] === '' || isDivider(lines[lines.length - 1])))
|
|
565
|
-
lines.pop();
|
|
566
310
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
hardCap: null,
|
|
572
|
-
preamble: [],
|
|
573
|
-
warmup: [],
|
|
574
|
-
acts: [],
|
|
575
|
-
quarantined: [],
|
|
576
|
-
};
|
|
311
|
+
function requireSquareExtension(squarePath) {
|
|
312
|
+
if (!squarePath.endsWith('.square')) {
|
|
313
|
+
throw new SquareError('invalid_args', `Square artifacts must use the .square extension: ${squarePath}`);
|
|
314
|
+
}
|
|
577
315
|
}
|
|
578
|
-
function
|
|
316
|
+
function atomicWrite(target, bytes) {
|
|
317
|
+
const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
318
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
579
319
|
try {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
return { ok: false, reason: 'act marker is missing kind/actor metadata.' };
|
|
583
|
-
const actor = parseParticipantHeading(blockLines[1] ?? '');
|
|
584
|
-
if (!actor)
|
|
585
|
-
return { ok: false, reason: `act block missing participant heading, got: ${blockLines[1] ?? ''}` };
|
|
586
|
-
const headLine = blockLines[2] === '' ? blockLines[3] : blockLines[2];
|
|
587
|
-
if (!parseActLine(headLine ?? '', actor))
|
|
588
|
-
return { ok: false, reason: `act block missing or malformed timestamp line, got: ${headLine ?? ''}` };
|
|
589
|
-
return { ok: true, act: parseActBlock(blockLines) };
|
|
320
|
+
fs.writeFileSync(temporary, bytes);
|
|
321
|
+
fs.renameSync(temporary, target);
|
|
590
322
|
}
|
|
591
|
-
catch {
|
|
592
|
-
|
|
323
|
+
catch (error) {
|
|
324
|
+
try {
|
|
325
|
+
fs.unlinkSync(temporary);
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
throw error;
|
|
593
329
|
}
|
|
594
330
|
}
|
|
595
|
-
function
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
i++;
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
const parseBlock = detectBlockParser(lines[i]);
|
|
616
|
-
if (!parseBlock) {
|
|
617
|
-
problems.push({ kind: 'act_block', message: `expected act marker, got: ${lines[i]}` });
|
|
618
|
-
let j = i + 1;
|
|
619
|
-
while (j < lines.length && !isActBlockStart(lines[j]))
|
|
620
|
-
j++;
|
|
621
|
-
quarantined.push({ raw: lines.slice(i, j).join('\n'), reason: 'expected current-format act marker' });
|
|
622
|
-
i = j;
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
const blockStart = i;
|
|
626
|
-
let j = i + 1;
|
|
627
|
-
while (j < lines.length && !isActBlockStart(lines[j]))
|
|
628
|
-
j++;
|
|
629
|
-
const blockLines = lines.slice(blockStart, j);
|
|
630
|
-
const outcome = parseBlock(blockLines);
|
|
631
|
-
if (outcome.ok) {
|
|
632
|
-
const index = outcome.act.index;
|
|
633
|
-
if (seenIndexes.has(index))
|
|
634
|
-
problems.push({ kind: 'duplicate_index', message: `duplicate act index ${index}.` });
|
|
635
|
-
seenIndexes.add(index);
|
|
636
|
-
acts.push({ act: outcome.act, raw: blockLines.join('\n') });
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
problems.push({ kind: 'act_block', message: outcome.reason });
|
|
640
|
-
quarantined.push({ raw: blockLines.join('\n'), reason: outcome.reason });
|
|
331
|
+
export function writeSquareFile(squarePath, squareState) {
|
|
332
|
+
requireSquareExtension(squarePath);
|
|
333
|
+
atomicWrite(squarePath, encodeSquare(squareState));
|
|
334
|
+
}
|
|
335
|
+
export function loadSquare(squarePath) {
|
|
336
|
+
requireSquareExtension(squarePath);
|
|
337
|
+
return decodeSquare(readArtifact(squarePath));
|
|
338
|
+
}
|
|
339
|
+
export function probeSquare(squarePath) {
|
|
340
|
+
if (!squarePath.endsWith('.square'))
|
|
341
|
+
return undefined;
|
|
342
|
+
let descriptor;
|
|
343
|
+
try {
|
|
344
|
+
descriptor = fs.openSync(squarePath, 'r');
|
|
345
|
+
const magic = Buffer.alloc(SQUARE_MAGIC.length);
|
|
346
|
+
if (fs.readSync(descriptor, magic, 0, magic.length, 0) !== magic.length || !magic.equals(SQUARE_MAGIC)) {
|
|
347
|
+
return undefined;
|
|
641
348
|
}
|
|
642
|
-
i = j;
|
|
643
349
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
const min = uniqueIndexes[0];
|
|
647
|
-
const max = uniqueIndexes[uniqueIndexes.length - 1];
|
|
648
|
-
if (max - min + 1 !== uniqueIndexes.length) {
|
|
649
|
-
problems.push({ kind: 'non_contiguous_indexes', message: 'act indexes are not contiguous.' });
|
|
650
|
-
}
|
|
350
|
+
catch {
|
|
351
|
+
return undefined;
|
|
651
352
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const fmMatch = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
656
|
-
if (!fmMatch)
|
|
657
|
-
return unfixableResult('cannot locate frontmatter: missing opening/closing "---" delimiters.');
|
|
658
|
-
const frontmatter = fmMatch[1];
|
|
659
|
-
const lines = text.split('\n');
|
|
660
|
-
if (!lines.some((line) => line.trim() === WARMUP_MARKER) || !lines.some((line) => line.trim() === ACTIVITIES_MARKER)) {
|
|
661
|
-
return unfixableResult('cannot locate the warmup/activities section markers; the act stream boundaries are unknown.');
|
|
353
|
+
finally {
|
|
354
|
+
if (descriptor !== undefined)
|
|
355
|
+
fs.closeSync(descriptor);
|
|
662
356
|
}
|
|
663
|
-
const problems = [];
|
|
664
|
-
const fvMatch = frontmatter.match(/^format_version:\s*(\d+)\s*$/m);
|
|
665
|
-
const formatVersion = fvMatch ? parseInt(fvMatch[1], 10) : undefined;
|
|
666
|
-
if (formatVersion === undefined)
|
|
667
|
-
return unfixableResult('missing format_version in frontmatter. Create a new square with `square build`.');
|
|
668
|
-
if (formatVersion !== CURRENT_FORMAT_VERSION)
|
|
669
|
-
return unfixableResult(`format_version ${formatVersion} is no longer supported. Create a new square with \`square build\`.`);
|
|
670
|
-
let hardCap = null;
|
|
671
|
-
const hcMatch = frontmatter.match(/^hard_cap:\s*(-1|\d+)\s*$/m);
|
|
672
|
-
if (!hcMatch)
|
|
673
|
-
problems.push({ kind: 'hard_cap', message: 'missing or malformed hard_cap in frontmatter.' });
|
|
674
|
-
else
|
|
675
|
-
hardCap = hcMatch[1] === '-1' ? null : parseInt(hcMatch[1], 10);
|
|
676
|
-
let throttlePerMinute;
|
|
677
|
-
const tMatch = frontmatter.match(/^throttle_per_minute:\s*(\d+)\s*$/m);
|
|
678
|
-
if (tMatch)
|
|
679
|
-
throttlePerMinute = parseInt(tMatch[1], 10);
|
|
680
|
-
let preamble = [];
|
|
681
357
|
try {
|
|
682
|
-
|
|
358
|
+
return loadSquare(squarePath);
|
|
683
359
|
}
|
|
684
|
-
catch
|
|
685
|
-
|
|
360
|
+
catch {
|
|
361
|
+
return undefined;
|
|
686
362
|
}
|
|
687
|
-
|
|
363
|
+
}
|
|
364
|
+
export function diagnoseSquareFile(squarePath) {
|
|
688
365
|
try {
|
|
689
|
-
|
|
366
|
+
return { problems: [], state: loadSquare(squarePath) };
|
|
690
367
|
}
|
|
691
|
-
catch (
|
|
692
|
-
|
|
368
|
+
catch (error) {
|
|
369
|
+
return {
|
|
370
|
+
unfixable: error instanceof Error ? error.message : String(error),
|
|
371
|
+
problems: [],
|
|
372
|
+
};
|
|
693
373
|
}
|
|
694
|
-
const { acts, quarantined } = diagnoseActs(text, problems);
|
|
695
|
-
return {
|
|
696
|
-
formatVersion,
|
|
697
|
-
problems,
|
|
698
|
-
hardCap,
|
|
699
|
-
throttlePerMinute,
|
|
700
|
-
preamble,
|
|
701
|
-
warmup,
|
|
702
|
-
acts,
|
|
703
|
-
quarantined,
|
|
704
|
-
};
|
|
705
374
|
}
|