@astrosheep/square 0.3.11 → 0.3.12

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/dist/list.js CHANGED
@@ -1,37 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { parseSquare } from './artifact.js';
3
+ import { probeSquare } from './artifact.js';
4
4
  import { inSquareCount, publicActs } from './runtime.js';
5
5
  import { formatRelativeTime } from './time.js';
6
6
  const DEFAULT_LIST_DEPTH = 4;
7
7
  const LIST_SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']);
8
- function frontmatterOf(text) {
9
- const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
10
- return match ? match[1] : null;
11
- }
12
- function candidateFrontmatter(filePath) {
13
- let fd;
14
- try {
15
- fd = fs.openSync(filePath, 'r');
16
- const buffer = Buffer.allocUnsafe(4096);
17
- const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
18
- const prefix = buffer.toString('utf8', 0, bytesRead);
19
- if (!prefix.startsWith('---\n'))
20
- return null;
21
- const frontmatter = frontmatterOf(prefix);
22
- return frontmatter ?? frontmatterOf(fs.readFileSync(filePath, 'utf8'));
23
- }
24
- catch {
25
- return null;
26
- }
27
- finally {
28
- if (fd !== undefined)
29
- fs.closeSync(fd);
30
- }
31
- }
32
8
  function readSquareListItem(filePath, root) {
33
- let text;
34
- let doc;
35
9
  let stat;
36
10
  try {
37
11
  stat = fs.statSync(filePath);
@@ -39,27 +13,9 @@ function readSquareListItem(filePath, root) {
39
13
  catch {
40
14
  return null;
41
15
  }
42
- const frontmatter = candidateFrontmatter(filePath);
43
- if (!frontmatter)
44
- return null;
45
- if (!/^hard_cap:\s*(-1|\d+)\s*$/m.test(frontmatter))
16
+ const doc = probeSquare(filePath);
17
+ if (doc === undefined)
46
18
  return null;
47
- if (!/^format_version:\s*3\s*$/m.test(frontmatter))
48
- return null;
49
- try {
50
- text = fs.readFileSync(filePath, 'utf8');
51
- }
52
- catch {
53
- return null;
54
- }
55
- if (!text.includes('<!-- square:warmup -->') || !text.includes('<!-- square:activities -->'))
56
- return null;
57
- try {
58
- doc = parseSquare(text);
59
- }
60
- catch {
61
- return null;
62
- }
63
19
  const relative = path.relative(root, filePath) || path.basename(filePath);
64
20
  return {
65
21
  path: relative,
package/dist/model.js CHANGED
@@ -11,12 +11,6 @@ export class SquareError extends Error {
11
11
  this.name = 'SquareError';
12
12
  }
13
13
  }
14
- export const WARMUP_HEADING = '## Warmup';
15
- export const WARMUP_MARKER = '<!-- square:warmup -->';
16
- export const ACTIVITIES_HEADING = '## Activities';
17
- export const ACTIVITIES_MARKER = '<!-- square:activities -->';
18
- export const ACT_MARKER_PREFIX = '<!-- square:act';
19
- export const CURRENT_FORMAT_VERSION = 3;
20
14
  export function formatHardCap(hardCap) {
21
15
  return hardCap === null ? '-1' : String(hardCap);
22
16
  }
@@ -4,11 +4,12 @@ import { homedir } from 'node:os';
4
4
  import { setTimeout as sleep } from 'node:timers/promises';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { loadSquare } from './artifact.js';
7
- import { deriveDeliveryModel, isDeliveryDelivered, isPendingNotification, leaseOwnsNotification, planActNotifications, } from './delivery.js';
7
+ import { deriveDeliveryModel, isDeliveryDelivered, leaseOwnsNotification, planActNotifications, } from './delivery.js';
8
8
  import { sessionInbox } from './inbox.js';
9
9
  import { hasPresentedAttention } from './presented.js';
10
10
  import { nameKey, SquareError } from './model.js';
11
11
  import { SLEEP_MS, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
12
+ import { formatActivityId, parseActivityId } from './square-core.js';
12
13
  import { PaseoAdapter } from './paseo-delivery.js';
13
14
  import { quoteShell } from './presentation.js';
14
15
  import { lookupParticipant } from './registry.js';
@@ -42,7 +43,7 @@ function renderWakePayload(request) {
42
43
  : request.squarePath;
43
44
  return [
44
45
  '<system-reminder source="square">',
45
- `${request.route === 'bell' ? 'Bell' : request.route === 'beside' ? 'Beside' : 'Mention'} from @${request.actor} in \`${display}\``,
46
+ `${request.route === 'bell' ? 'Bell' : 'Mention'} from @${request.actor} in \`${display}\``,
46
47
  'The native adapter will present it at the next boundary. If no native wake is available, pull from the square yourself.',
47
48
  `\`${catchCommand(request.squarePath, request.recipient)}\``,
48
49
  '</system-reminder>',
@@ -57,7 +58,6 @@ async function waitForCatch(route, request, body) {
57
58
  actor: request.actor,
58
59
  body,
59
60
  route: request.route,
60
- recipient: request.recipient,
61
61
  }))
62
62
  return false;
63
63
  const deadline = Date.now() + 180_000;
@@ -75,14 +75,22 @@ async function waitForCatch(route, request, body) {
75
75
  }
76
76
  return false;
77
77
  }
78
+ function notificationIndex(ref) {
79
+ if (typeof ref === 'number')
80
+ return ref;
81
+ const index = parseActivityId(ref);
82
+ if (index === undefined)
83
+ throw new Error(`Invalid act ref: ${ref}`);
84
+ return index;
85
+ }
78
86
  export function hasDeliveredNotification(squarePath, name, ref) {
79
87
  const doc = loadSquare(squarePath);
80
- return isDeliveryDelivered(doc, known(doc, name), typeof ref === 'number' ? ref : Number(ref.slice(4)));
88
+ return isDeliveryDelivered(doc, known(doc, name), notificationIndex(ref));
81
89
  }
82
90
  export function hasAttentionNotification(squarePath, name, ref, env = process.env) {
83
91
  const doc = loadSquare(squarePath);
84
92
  const recipient = known(doc, name);
85
- const index = typeof ref === 'number' ? ref : Number(ref.slice(4));
93
+ const index = notificationIndex(ref);
86
94
  return isDeliveryDelivered(doc, recipient, index) || hasPresentedAttention(squarePath, recipient, index, env);
87
95
  }
88
96
  export async function waitForDeliveredNotification(squarePath, name, ref, opts = {}) {
@@ -95,7 +103,7 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
95
103
  return false;
96
104
  }
97
105
  function notifyLeaseKey(recipient, actIndex) {
98
- return JSON.stringify([`act_${actIndex}`, nameKey(recipient)]);
106
+ return JSON.stringify([formatActivityId(actIndex), nameKey(recipient)]);
99
107
  }
100
108
  async function claimNotifyLease(squarePath, recipient, actIndex) {
101
109
  const at = Date.now();
@@ -214,7 +222,7 @@ export async function processActNotificationsOnce(squarePath, actIndex, opts = {
214
222
  const item = doc.acts.find((candidate) => candidate.index === actIndex);
215
223
  if (item === undefined)
216
224
  return;
217
- const notifications = planActNotifications(doc, item).filter(isPendingNotification);
225
+ const notifications = planActNotifications(doc, item);
218
226
  await Promise.all(notifications.map((notification) => processNotification(squarePath, notification, opts)));
219
227
  }
220
228
  function launchWorker(workerPath, args) {
@@ -227,7 +235,7 @@ export async function dispatchActNotifications(squarePath, item, opts = {}) {
227
235
  if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
228
236
  return;
229
237
  const doc = loadSquare(squarePath);
230
- if (!planActNotifications(doc, item).some(isPendingNotification))
238
+ if (planActNotifications(doc, item).length === 0)
231
239
  return;
232
240
  (opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--location', squarePath, '--act-index', String(item.index)]);
233
241
  }
@@ -1,8 +1,8 @@
1
1
  import { sameName } from './model.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { fold, perceive } from './square-core.js';
5
- import { actId, actStableIndex, extractMentions, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
4
+ import { audienceIncludes, audienceOf, perceive } from './square-core.js';
5
+ import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
6
6
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
7
7
  import { grepSnippet } from './search.js';
8
8
  function headerLine(squarePath, opts = {}) {
@@ -150,7 +150,7 @@ export function renderEventCli(event, opts = {}) {
150
150
  case 'say': {
151
151
  const body = renderedBody(event.body, maxBody);
152
152
  const mention = opts.mention;
153
- const mentionSuffix = mention !== undefined && extractMentions(event.body).some((name) => sameName(name, mention))
153
+ const mentionSuffix = mention !== undefined && audienceIncludes(audienceOf(event), mention)
154
154
  ? ` · calls your name across the square — @${mention}`
155
155
  : '';
156
156
  const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
@@ -165,21 +165,15 @@ export function renderEventCli(event, opts = {}) {
165
165
  }
166
166
  }
167
167
  function renderPresenceOnlySay(event) {
168
- if (event.kind !== 'say' || event.reach === undefined || event.reach === 'bell')
169
- return '';
170
- return `*walks over to @${event.reach.beside}*`;
171
- }
172
- function perceptionFor(history, event, viewer) {
173
- const cutoff = history.findIndex((item) => actStableIndex(item) === actStableIndex(event));
174
- const acts = cutoff >= 0 ? history.slice(0, cutoff) : history;
175
- return perceive(fold(acts), event, viewer);
168
+ const audience = audienceOf(event);
169
+ const targets = audience.kind === 'bell' ? [] : audience.names;
170
+ const dest = targets.length === 0 ? '' : ` to ${targets.map((name) => `@${name}`).join(' and ')}`;
171
+ return `*${event.actor} walks over${dest}*`;
176
172
  }
177
- export function renderVisibleEvent(history, event, viewer, opts = {}) {
173
+ export function renderAmbientEvent(event, viewer, opts = {}) {
178
174
  if (event.kind !== 'say')
179
175
  return renderEventCli(event, opts);
180
- const seen = perceptionFor(history, event, viewer);
181
- if (seen === 'none')
182
- return '';
176
+ const seen = perceive(event, viewer);
183
177
  if (seen === 'presence')
184
178
  return renderPresenceOnlySay(event);
185
179
  return renderEventCli(event, opts);
@@ -194,7 +188,7 @@ function renderUnreadSummary(opts) {
194
188
  return [
195
189
  ...opts.activitySummaries.flatMap((item) => [
196
190
  ...item.previews.slice(-1).map((preview) => {
197
- const rendered = renderVisibleEvent([preview.act], preview.act, opts.viewer, { actNumber: preview.number });
191
+ const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number });
198
192
  if (rendered === '')
199
193
  return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago`;
200
194
  if (rendered.startsWith('*'))
@@ -208,7 +202,7 @@ function renderUnreadSummary(opts) {
208
202
  export function renderPendingFeed(history, publicItems, roomChanges, viewer = '') {
209
203
  const lines = [];
210
204
  for (const act of publicItems) {
211
- const rendered = renderVisibleEvent(history, act, viewer, {
205
+ const rendered = renderAmbientEvent(act, viewer, {
212
206
  actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
213
207
  });
214
208
  if (rendered !== '')
@@ -262,7 +256,7 @@ export function renderPublicTail(events, lastN, now, viewer = '') {
262
256
  const selected = lastN == null ? publicItems : publicItems.slice(-lastN);
263
257
  const preview = lastN == null ? undefined : BODY_PREVIEW_LENGTH;
264
258
  return selected
265
- .map((event) => renderVisibleEvent(events, event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined }))
259
+ .map((event) => renderAmbientEvent(event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined }))
266
260
  .filter(Boolean)
267
261
  .join('\n\n');
268
262
  }
@@ -270,7 +264,7 @@ function lastPresenceAnchor(doc, name) {
270
264
  const cursor = readCursor(doc, name);
271
265
  for (let i = doc.acts.length - 1; i >= 0; i--) {
272
266
  const event = doc.acts[i];
273
- const index = actStableIndex(event);
267
+ const index = event.index;
274
268
  if (index > cursor)
275
269
  continue;
276
270
  if (event.kind === 'say' || event.kind === 'done')
@@ -281,7 +275,7 @@ function lastPresenceAnchor(doc, name) {
281
275
  function renderLastPresenceMarker(name) {
282
276
  return `· ${name}'s footprints reach here`;
283
277
  }
284
- export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '') {
278
+ export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '', mode = 'ambient') {
285
279
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
286
280
  const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
287
281
  const previewLen = full ? undefined : BODY_PREVIEW_LENGTH;
@@ -293,10 +287,13 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
293
287
  }
294
288
  const chunks = [];
295
289
  for (const act of shown) {
296
- const rendered = renderVisibleEvent(doc.acts, act, viewer, {
290
+ const opts = {
297
291
  preview: previewLen,
298
292
  actNumber: act.kind === 'say' ? sayNumberFor(doc.acts, act) : undefined,
299
- });
293
+ };
294
+ const rendered = mode === 'archive'
295
+ ? renderEventCli(act, opts)
296
+ : renderAmbientEvent(act, viewer, opts);
300
297
  if (rendered !== '')
301
298
  chunks.push(rendered);
302
299
  for (const participant of markers.get(act.index) ?? []) {
@@ -306,7 +303,7 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
306
303
  if (chunks.length === 0)
307
304
  return 'latest\n ○ no public activity in this view';
308
305
  if (previewLen !== undefined) {
309
- const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen);
306
+ const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceive(act, viewer) === 'full'));
310
307
  if (truncated)
311
308
  chunks.push(`» ${commandPrefix(squarePath)} history --full`);
312
309
  }
@@ -412,20 +409,8 @@ function renderRoomChanges(changes) {
412
409
  export function renderDoctorClean() {
413
410
  return '✓ no problems found';
414
411
  }
415
- export function renderDoctorProblems(problems) {
416
- return [`✕ ${problems.length} ${pluralize(problems.length, 'problem')} found`, ...problems.map((problem) => ` · ${problem.kind}: ${problem.message}`)].join('\n');
417
- }
418
412
  export function renderDoctorUnfixable(reason) {
419
- return ['✕ cannot repair', ` · ${reason}`].join('\n');
420
- }
421
- export function renderDoctorRepaired(actions, quarantinedCount, sidecarPath) {
422
- if (actions.length === 0)
423
- return '✓ no problems found';
424
- return [
425
- '✓ repaired',
426
- ...actions.map((action) => ` · ${action.message}`),
427
- ...(quarantinedCount > 0 && sidecarPath !== undefined ? [` · quarantined ${quarantinedCount} act block(s)`, ` · sidecar ${sidecarPath}`] : []),
428
- ].join('\n');
413
+ return ['✕ unreadable artifact', ` · ${reason}`].join('\n');
429
414
  }
430
415
  export function renderWatchOutput(history, publicItems, roomChanges, opts) {
431
416
  const sections = [];
@@ -448,7 +433,7 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
448
433
  sections.push(room);
449
434
  if (publicItems.length > 0) {
450
435
  const rendered = publicItems
451
- .map((act) => renderVisibleEvent(history, act, opts.viewer, {
436
+ .map((act) => renderAmbientEvent(act, opts.viewer, {
452
437
  actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
453
438
  mention: opts.mention,
454
439
  }))
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { fold } from './square-core.js';
1
+ import { audienceIncludes, audienceOf, fold, formatActivityId } from './square-core.js';
2
2
  import { SquareError, findParticipantName, nameKey, sameName, } from './model.js';
3
3
  function parseIntegerEnvValue(name, raw, fallback) {
4
4
  if (raw === undefined)
@@ -51,27 +51,12 @@ export function nowMs() {
51
51
  }
52
52
  return value;
53
53
  }
54
- export function extractMentions(body) {
55
- const matches = [];
56
- const re = /@([\p{L}\p{N}_-]+)/gu;
57
- let match;
58
- while ((match = re.exec(body)) !== null)
59
- matches.push(match[1]);
60
- return matches;
61
- }
62
- /** Pure directed-activity filter. Broadcast bodies (no @) match any named viewer. */
54
+ /** Pure directed-activity filter. Bell matches every viewer; mentions match by audience. */
63
55
  export function matchesMentionTarget(act, mention) {
64
- if (act.reach === 'bell')
65
- return true;
66
- if (act.reach !== undefined) {
67
- return mention === true || sameName(act.reach.beside, mention);
68
- }
69
- const mentions = extractMentions(act.body);
56
+ const audience = audienceOf(act);
70
57
  if (mention === true)
71
- return mentions.length > 0;
72
- if (mentions.length === 0)
73
- return true;
74
- return mentions.some((name) => sameName(name, mention));
58
+ return audience.kind === 'bell' || audience.names.length > 0;
59
+ return audienceIncludes(audience, mention);
75
60
  }
76
61
  export function foldedState(doc) {
77
62
  return fold(doc.acts);
@@ -131,7 +116,7 @@ export function actStableIndex(act) {
131
116
  }
132
117
  export function actId(actOrIndex) {
133
118
  const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
134
- return `act_${index}`;
119
+ return formatActivityId(index);
135
120
  }
136
121
  export function getReadState(doc, name) {
137
122
  return doc.runtime.cursors[name] ?? Object.entries(doc.runtime.cursors).find(([participant]) => sameName(participant, name))?.[1];
@@ -1,21 +1,16 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { emptyRuntimeState, loadRuntimeSidecar, loadSquare, mergeRuntimeState, renderArtifactAct, renderSquare, renderSquareDoc, saveRuntimeSidecar } from './artifact.js';
2
+ import { createSquareDoc, loadArchive, loadSquare, writeArchiveFile, writeSquareFile } from './artifact.js';
4
3
  import { coreCompact, coreDone, coreHold, coreResume, decideAct, decideJoin, resolveKnownName } from './decisions.js';
5
- import { planRepair } from './doctor.js';
6
4
  import { withFileLock } from './file-lock.js';
7
5
  import { stageReplacement } from './harness-stage.js';
8
6
  import { SquareError } from './model.js';
9
7
  import { advanceCursor, freshWatchLease, LOCK_RETRY_MS, LOCK_STALE_MS, removeWatchLease, touchPresenceCursor, watchLease, writeWatchLease } from './runtime.js';
10
- /** The only persistence primitive: one per-square lock, one Markdown write, one sidecar write. */
8
+ /** The only mutation boundary: one per-square lock around one complete snapshot commit. */
11
9
  export async function withSquareLock(squarePath, fn) {
12
10
  return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, fn);
13
11
  }
14
12
  export function writeSquareDoc(squarePath, doc) {
15
- const temporary = path.join(path.dirname(squarePath), `.${path.basename(squarePath)}.${process.pid}.${Date.now()}.tmp`);
16
- fs.writeFileSync(temporary, renderSquareDoc(doc));
17
- fs.renameSync(temporary, squarePath);
18
- saveRuntimeSidecar(squarePath, doc.runtime);
13
+ writeSquareFile(squarePath, doc);
19
14
  }
20
15
  export function appendAct(squarePath, doc, act) {
21
16
  const stored = applyActs(doc, [act])[0];
@@ -35,13 +30,11 @@ function applyActs(doc, acts, mutateRuntime) {
35
30
  mutateRuntime?.(doc);
36
31
  return stored;
37
32
  }
38
- /**
39
- * Publish a dependent persistence file before the Square document. A retained
40
- * backup lets a failed document commit restore the prior file exactly.
41
- */
42
- function prepareAppend(filePath, block, existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '') {
33
+ /** Publish a dependent archive before the main snapshot, retaining rollback evidence. */
34
+ function prepareArchive(filePath, acts) {
35
+ const existing = fs.existsSync(filePath) ? loadArchive(filePath) : [];
43
36
  return stageReplacement(filePath, (stage) => {
44
- fs.writeFileSync(stage, `${existing}${existing === '' ? '' : '\n'}${block}\n`);
37
+ writeArchiveFile(stage, [...existing, ...acts]);
45
38
  });
46
39
  }
47
40
  function plan(doc, intent) {
@@ -156,27 +149,9 @@ function plan(doc, intent) {
156
149
  replaceDoc: result.doc,
157
150
  preparePersistence: archive.length === 0
158
151
  ? undefined
159
- : () => {
160
- const existing = fs.existsSync(intent.archivePath) ? fs.readFileSync(intent.archivePath, 'utf8') : '';
161
- const block = archive
162
- .map((act, index) => renderArtifactAct(act, { first: existing === '' && index === 0 }))
163
- .join('\n');
164
- return prepareAppend(intent.archivePath, block, existing);
165
- },
152
+ : () => prepareArchive(intent.archivePath, archive),
166
153
  };
167
154
  }
168
- case 'repair':
169
- return {
170
- result: undefined,
171
- acts: [],
172
- replaceDoc: intent.doc,
173
- preparePersistence: intent.quarantine === undefined || intent.quarantine.blocks.length === 0
174
- ? undefined
175
- : () => {
176
- const block = intent.quarantine.blocks.join('\n\n');
177
- return prepareAppend(intent.quarantine.path, block);
178
- },
179
- };
180
155
  }
181
156
  }
182
157
  function commitPlan(squarePath, doc, planned) {
@@ -213,50 +188,6 @@ export async function createSquare(squarePath, options, snippet) {
213
188
  if (fs.existsSync(squarePath) && !options.force) {
214
189
  throw new SquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
215
190
  }
216
- const temporary = path.join(path.dirname(squarePath), `.${path.basename(squarePath)}.${process.pid}.${Date.now()}.tmp`);
217
- fs.mkdirSync(path.dirname(squarePath), { recursive: true });
218
- fs.writeFileSync(temporary, renderSquare(options, snippet));
219
- fs.renameSync(temporary, squarePath);
220
- saveRuntimeSidecar(squarePath, emptyRuntimeState(0));
221
- });
222
- }
223
- /** Keep artifact repair planning and dependent quarantine persistence inside the application boundary. */
224
- export async function repairSquare(squarePath) {
225
- const result = await withSquareLock(squarePath, () => {
226
- let text;
227
- try {
228
- text = fs.readFileSync(squarePath, 'utf8');
229
- }
230
- catch (error) {
231
- if (error.code === 'ENOENT')
232
- throw new SquareError('not_found', `square file not found: ${squarePath}`);
233
- throw error;
234
- }
235
- const repair = planRepair(text);
236
- if (repair.diagnosis.unfixable || repair.repaired === undefined)
237
- return { repair };
238
- // Repair changes Markdown only. Keep the sidecar's runtime metadata and
239
- // merge history boundaries so a doctor run cannot erase delivery state or
240
- // reuse a stable activity index.
241
- const sidecarRuntime = loadRuntimeSidecar(squarePath, repair.repaired.doc.runtime);
242
- const indexesPreserved = repair.diagnosis.acts.every(({ act }, index) => repair.repaired.doc.acts[index]?.index === act.index);
243
- if (indexesPreserved) {
244
- repair.repaired.doc.runtime = mergeRuntimeState(repair.repaired.doc.runtime, sidecarRuntime);
245
- }
246
- else {
247
- repair.repaired.doc.runtime = emptyRuntimeState(Math.max(repair.repaired.doc.runtime.nextActIndex, sidecarRuntime.nextActIndex));
248
- repair.repaired.actions.push({ message: 'reset runtime delivery metadata because act indexes changed' });
249
- }
250
- const quarantinePath = squarePath.replace(/\.md$/, '') + '.quarantine.md';
251
- const intent = {
252
- type: 'repair',
253
- doc: repair.repaired.doc,
254
- ...(repair.repaired.quarantinedBlocks.length === 0
255
- ? {}
256
- : { quarantine: { path: quarantinePath, blocks: repair.repaired.quarantinedBlocks } }),
257
- };
258
- commitPlan(squarePath, repair.repaired.doc, plan(repair.repaired.doc, intent));
259
- return { repair };
191
+ writeSquareFile(squarePath, createSquareDoc(options, snippet));
260
192
  });
261
- return result.repair;
262
193
  }
@@ -1,9 +1,62 @@
1
+ export function formatActivityId(index) {
2
+ if (!Number.isSafeInteger(index) || index < 0) {
3
+ throw new Error(`Invalid activity index: ${index}`);
4
+ }
5
+ return `act/${index}`;
6
+ }
7
+ export function parseActivityId(value) {
8
+ if (value === 'act/0')
9
+ return 0;
10
+ if (typeof value !== 'string' || !/^act\/[1-9]\d*$/.test(value))
11
+ return undefined;
12
+ const index = Number(value.slice(4));
13
+ return Number.isSafeInteger(index) ? index : undefined;
14
+ }
1
15
  function nameKey(name) {
2
16
  return name.toLocaleLowerCase();
3
17
  }
4
18
  function sameName(a, b) {
5
19
  return nameKey(a) === nameKey(b);
6
20
  }
21
+ export function extractMentions(body) {
22
+ const matches = [];
23
+ const re = /@([\p{L}\p{N}_-]+)/gu;
24
+ let match;
25
+ while ((match = re.exec(body)) !== null)
26
+ matches.push(match[1]);
27
+ return matches;
28
+ }
29
+ function uniqueMentionNames(names) {
30
+ const unique = [];
31
+ for (const name of names) {
32
+ if (unique.some((existing) => sameName(existing, name)))
33
+ continue;
34
+ unique.push(name);
35
+ }
36
+ return unique;
37
+ }
38
+ export function audienceOf(say) {
39
+ if (say.reach === 'bell')
40
+ return { kind: 'bell' };
41
+ return { kind: 'mentions', names: uniqueMentionNames(extractMentions(say.body)) };
42
+ }
43
+ export function audienceIncludes(audience, name) {
44
+ if (audience.kind === 'bell')
45
+ return true;
46
+ return audience.names.some((mentioned) => sameName(mentioned, name));
47
+ }
48
+ export function resolveAudience(audience, candidateNames) {
49
+ if (audience.kind === 'bell')
50
+ return [...candidateNames];
51
+ const resolved = [];
52
+ for (const mention of audience.names) {
53
+ const known = candidateNames.find((candidate) => sameName(candidate, mention));
54
+ if (known !== undefined && !resolved.some((existing) => sameName(existing, known))) {
55
+ resolved.push(known);
56
+ }
57
+ }
58
+ return resolved;
59
+ }
7
60
  function actorOf(act) {
8
61
  if ('actor' in act && typeof act.actor === 'string')
9
62
  return act.actor;
@@ -160,16 +213,10 @@ export function validate(state, act, options = {}) {
160
213
  return { ok: true };
161
214
  }
162
215
  }
163
- export function perceive(state, act, viewer) {
164
- void state;
216
+ export function perceive(act, viewer) {
165
217
  if (act.kind !== 'say')
166
218
  return 'full';
167
- const actor = act.actor;
168
- if (sameName(actor, viewer))
169
- return 'full';
170
- if (act.reach === undefined || act.reach === 'bell')
171
- return 'full';
172
- if (sameName(act.reach.beside, viewer))
219
+ if (sameName(act.actor, viewer))
173
220
  return 'full';
174
- return 'presence';
221
+ return audienceIncludes(audienceOf(act), viewer) ? 'full' : 'presence';
175
222
  }
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { withFileLockSync } from './file-lock.js';
5
5
  import { isWakeRouteKind, nameKey } from './model.js';
6
6
  import { canonicalSquarePath } from './registry.js';
7
+ import { formatActivityId, parseActivityId } from './square-core.js';
7
8
  const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
8
9
  const LOCK_STALE_MS = 5 * 60 * 1000;
9
10
  const LOCK_RETRY_MS = 10;
@@ -12,7 +13,7 @@ export function wakeAttemptsPath(env = process.env) {
12
13
  return env.SQUARE_WAKE_ATTEMPTS || path.join(os.homedir(), '.square', 'wake-attempts.ndjsonl');
13
14
  }
14
15
  export function wakeAttentionKey(attention) {
15
- return JSON.stringify([canonicalSquarePath(attention.squarePath), `act_${attention.actIndex}`, nameKey(attention.recipient)]);
16
+ return JSON.stringify([canonicalSquarePath(attention.squarePath), formatActivityId(attention.actIndex), nameKey(attention.recipient)]);
16
17
  }
17
18
  function parseRow(raw, now) {
18
19
  let value;
@@ -27,7 +28,7 @@ function parseRow(raw, now) {
27
28
  const row = value;
28
29
  if (row.v !== 1 || typeof row.ts !== 'number' || !Number.isFinite(row.ts) || row.ts > now || now - row.ts > RETENTION_MS ||
29
30
  row.attention === undefined || typeof row.attention.square_path !== 'string' || row.attention.square_path === '' ||
30
- typeof row.attention.act_id !== 'string' || !/^act_\d+$/.test(row.attention.act_id) ||
31
+ typeof row.attention.act_id !== 'string' || parseActivityId(row.attention.act_id) === undefined ||
31
32
  typeof row.attention.recipient !== 'string' || row.attention.recipient === '' ||
32
33
  typeof row.outcome !== 'string' || !VALID_OUTCOMES.has(row.outcome) ||
33
34
  typeof row.attempt_n !== 'number' || !Number.isInteger(row.attempt_n) || row.attempt_n <= 0 ||
@@ -62,11 +63,14 @@ function writeRows(filePath, rows) {
62
63
  fs.renameSync(temporary, filePath);
63
64
  }
64
65
  function fromRow(row) {
66
+ const actIndex = parseActivityId(row.attention.act_id);
67
+ if (actIndex === undefined)
68
+ throw new Error(`Invalid wake activity id: ${row.attention.act_id}`);
65
69
  return {
66
70
  at: row.ts,
67
71
  attention: {
68
72
  squarePath: canonicalSquarePath(row.attention.square_path),
69
- actIndex: Number(row.attention.act_id.slice(4)),
73
+ actIndex,
70
74
  recipient: row.attention.recipient,
71
75
  },
72
76
  routeKind: row.route_kind,
@@ -121,7 +125,7 @@ function toRow(attempt, env) {
121
125
  ts: safe.at,
122
126
  attention: {
123
127
  square_path: canonicalSquarePath(safe.attention.squarePath),
124
- act_id: `act_${safe.attention.actIndex}`,
128
+ act_id: formatActivityId(safe.attention.actIndex),
125
129
  recipient: safe.attention.recipient,
126
130
  },
127
131
  route_kind: safe.routeKind,