@astrosheep/square 0.3.70 → 0.3.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/packages/agent-delivery/src/claude.d.ts +2 -11
  51. package/dist/packages/agent-delivery/src/index.d.ts +2 -42
  52. package/dist/packages/agent-delivery/src/opencode.d.ts +2 -11
  53. package/dist/packages/agent-delivery/src/paseo.d.ts +2 -13
  54. package/dist/packages/agent-delivery/src/pi-client.d.ts +2 -9
  55. package/dist/packages/agent-delivery/src/types.d.ts +42 -0
  56. package/dist/participant-host.d.ts +6 -3
  57. package/dist/participant-host.js +44 -38
  58. package/dist/perception-projection.d.ts +6 -15
  59. package/dist/perception-projection.js +6 -26
  60. package/dist/plugins/opencode/delivery.js +9 -9
  61. package/dist/plugins/opencode/index.js +3 -3
  62. package/dist/ports.d.ts +156 -11
  63. package/dist/presentation-operations.d.ts +1 -1
  64. package/dist/presentation-operations.js +35 -44
  65. package/dist/presentation.d.ts +12 -19
  66. package/dist/presentation.js +33 -54
  67. package/dist/registry.d.ts +2 -0
  68. package/dist/registry.js +1 -1
  69. package/dist/routes.d.ts +7 -14
  70. package/dist/routes.js +34 -72
  71. package/dist/runtime.d.ts +12 -39
  72. package/dist/runtime.js +33 -164
  73. package/dist/square-actions.d.ts +11 -21
  74. package/dist/square-actions.js +171 -275
  75. package/dist/square-application.d.ts +31 -55
  76. package/dist/square-application.js +625 -141
  77. package/dist/square-core.d.ts +3 -94
  78. package/dist/square-core.js +8 -353
  79. package/dist/square-domain.d.ts +24 -0
  80. package/dist/square-domain.js +77 -0
  81. package/dist/square-facade.d.ts +360 -9
  82. package/dist/square-migration.d.ts +24 -0
  83. package/dist/square-migration.js +211 -0
  84. package/dist/square-projections.d.ts +2 -42
  85. package/dist/square-projections.js +1 -37
  86. package/dist/square-sqlite.d.ts +59 -0
  87. package/dist/square-sqlite.js +702 -0
  88. package/dist/stream.d.ts +3 -10
  89. package/dist/stream.js +81 -52
  90. package/dist/views.d.ts +27 -95
  91. package/dist/views.js +217 -87
  92. package/dist/wake-eligibility.d.ts +15 -5
  93. package/dist/wake-eligibility.js +32 -16
  94. package/dist/wakes.d.ts +3 -3
  95. package/dist/wakes.js +31 -27
  96. package/guides/application-facade-delivery-evidence.md +200 -0
  97. package/guides/application-facade-design.md +95 -0
  98. package/guides/change-waits.md +4 -4
  99. package/guides/migration.md +77 -0
  100. package/guides/postcommit-simplification-performance.md +36 -0
  101. package/guides/primitive-catch-consolidation.md +85 -0
  102. package/guides/primitive-history-consolidation.md +76 -0
  103. package/guides/primitive-status-consolidation.md +87 -0
  104. package/guides/primitive-storage-consolidation.md +128 -0
  105. package/guides/row-runtime-delivery-evidence.md +160 -0
  106. package/guides/row-storage-design.md +567 -0
  107. package/package.json +1 -1
  108. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  109. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  110. package/plugins/opencode/delivery.ts +11 -11
  111. package/plugins/opencode/index.ts +3 -3
  112. package/plugins/paseo/client/square-message.tsx +77 -64
  113. package/plugins/paseo/shared/message.ts +16 -0
  114. package/plugins/pi/square-pi.js +5 -1
  115. package/dist/activity.d.ts +0 -14
  116. package/dist/activity.js +0 -248
  117. package/dist/square-file-adapter.d.ts +0 -26
  118. package/dist/square-file-adapter.js +0 -85
  119. package/dist/square-state.d.ts +0 -6
  120. package/dist/square-state.js +0 -138
  121. package/dist/square-storage.d.ts +0 -14
  122. package/dist/square-storage.js +0 -188
  123. package/dist/square-wiring.d.ts +0 -33
  124. package/dist/square-wiring.js +0 -109
  125. package/dist/watch.d.ts +0 -8
  126. package/dist/watch.js +0 -252
@@ -4,18 +4,13 @@ export type Audience = {
4
4
  readonly kind: 'bell';
5
5
  } | {
6
6
  readonly kind: 'mentions';
7
- readonly names: readonly string[];
7
+ readonly names: readonly Participant[];
8
8
  };
9
9
  export type ActivityId = `act/${number}`;
10
+ export type Perception = 'full' | 'presence';
10
11
  export declare const MAX_IDENTITY_SET_SIZE = 10;
11
12
  export declare function formatActivityId(index: number): ActivityId;
12
13
  export declare function parseActivityId(value: string): number | undefined;
13
- export interface Hold {
14
- active: boolean;
15
- at?: number;
16
- reason?: string;
17
- actor?: Participant;
18
- }
19
14
  export type Act = {
20
15
  kind: 'join';
21
16
  actor: Participant;
@@ -30,7 +25,7 @@ export type Act = {
30
25
  actor: Participant;
31
26
  at?: number;
32
27
  body: string;
33
- mentions: readonly Participant[];
28
+ mentions?: readonly Participant[];
34
29
  reach?: Reach;
35
30
  reply?: number;
36
31
  } | {
@@ -58,95 +53,9 @@ export type Act = {
58
53
  target: Participant;
59
54
  at?: number;
60
55
  };
61
- export interface SquareValidationOptions {
62
- hardCap?: number | null;
63
- throttlePerMinute?: number;
64
- throttleWindowMs?: number;
65
- }
66
- export type Perception = 'full' | 'presence';
67
- interface ParticipantSnapshot {
68
- name: Participant;
69
- key: string;
70
- joined: boolean;
71
- done: boolean;
72
- activityCount: number;
73
- lastActiveAt?: number;
74
- lastReadThrough: number;
75
- }
76
- export interface FoldedSquareState {
77
- participants: ParticipantSnapshot[];
78
- hold: Hold;
79
- joined: Participant[];
80
- done: Participant[];
81
- throttleActivityAts: number[];
82
- bellSayAtsByActor: Map<string, number[]>;
83
- listening: Map<string, Participant[]>;
84
- /** Derived sender blocks; kept outside the artifact schema. */
85
- ignored: Map<string, Participant[]>;
86
- }
87
- export interface LandedAudienceReplay {
88
- readonly participants: readonly Participant[];
89
- readonly joined: readonly Participant[];
90
- readonly replayedActivityCount: number;
91
- recipientsFor(activity: Act): readonly Participant[];
92
- includes(activity: Act, participant: Participant): boolean;
93
- lastJoinIndex(participant: Participant): number | undefined;
94
- resolveParticipant(name: Participant): Participant | undefined;
95
- }
96
- export type ValidationResult = {
97
- ok: true;
98
- } | {
99
- ok: false;
100
- reason: 'already_joined';
101
- } | {
102
- ok: false;
103
- reason: 'not_joined';
104
- } | {
105
- ok: false;
106
- reason: 'done';
107
- } | {
108
- ok: false;
109
- reason: 'held';
110
- hold: Hold;
111
- } | {
112
- ok: false;
113
- reason: 'hard_cap';
114
- count: number;
115
- hardCap: number;
116
- } | {
117
- ok: false;
118
- reason: 'throttled';
119
- delayMs: number;
120
- } | {
121
- ok: false;
122
- reason: 'bell_quota';
123
- nextAt: number;
124
- } | {
125
- ok: false;
126
- reason: 'mention_limit';
127
- limit: number;
128
- } | {
129
- ok: false;
130
- reason: 'listening_limit';
131
- limit: number;
132
- };
133
56
  export declare function audienceOf(say: {
134
57
  body: string;
135
58
  mentions?: readonly Participant[];
136
59
  reach?: Reach;
137
60
  }): Audience;
138
61
  export declare function audienceIncludes(audience: Audience, name: string): boolean;
139
- export declare function resolveAudience(audience: Audience, candidateNames: readonly string[]): string[];
140
- export declare function activeListeners(state: FoldedSquareState, sender: string): string[];
141
- export declare function isIgnored(state: FoldedSquareState, listener: string, sender: string): boolean;
142
- /** The author of every landed activity, keyed by index; a reply audience derives from this. */
143
- export declare function activityAuthors(acts: readonly Act[]): Map<number, Participant>;
144
- export declare function listeningTo(state: FoldedSquareState, listener: string): string[];
145
- export declare function isListening(state: FoldedSquareState, listener: string, sender: string): boolean;
146
- export declare function actorOf(act: Act): Participant | undefined;
147
- export declare function fold(acts: readonly Act[]): FoldedSquareState;
148
- /** Replay the activity stream once to fix every say's audience at landing. */
149
- export declare function replayLandedAudiences(acts: readonly Act[]): LandedAudienceReplay;
150
- export declare function validate(state: FoldedSquareState, act: Act, options?: SquareValidationOptions): ValidationResult;
151
- export declare function perceive(act: Act, viewer: Participant | string): Perception;
152
- export {};
@@ -1,8 +1,8 @@
1
+ import { sameName } from './model.js';
1
2
  export const MAX_IDENTITY_SET_SIZE = 10;
2
3
  export function formatActivityId(index) {
3
- if (!Number.isSafeInteger(index) || index < 0) {
4
+ if (!Number.isSafeInteger(index) || index < 0)
4
5
  throw new Error(`Invalid activity index: ${index}`);
5
- }
6
6
  return `act/${index}`;
7
7
  }
8
8
  export function parseActivityId(value) {
@@ -13,360 +13,15 @@ export function parseActivityId(value) {
13
13
  const index = Number(value.slice(4));
14
14
  return Number.isSafeInteger(index) ? index : undefined;
15
15
  }
16
- function nameKey(name) {
17
- return name.toLocaleLowerCase();
18
- }
19
- function sameName(a, b) {
20
- return nameKey(a) === nameKey(b);
21
- }
22
- function uniqueMentionNames(names) {
23
- const unique = [];
24
- for (const name of names) {
25
- if (unique.some((existing) => sameName(existing, name)))
26
- continue;
27
- unique.push(name);
28
- }
29
- return unique;
30
- }
31
16
  export function audienceOf(say) {
32
17
  if (say.reach === 'bell')
33
18
  return { kind: 'bell' };
34
- return { kind: 'mentions', names: uniqueMentionNames(say.mentions ?? []) };
19
+ const names = [];
20
+ for (const name of say.mentions ?? [])
21
+ if (!names.some((existing) => sameName(existing, name)))
22
+ names.push(name);
23
+ return { kind: 'mentions', names };
35
24
  }
36
25
  export function audienceIncludes(audience, name) {
37
- if (audience.kind === 'bell')
38
- return true;
39
- return audience.names.some((mentioned) => sameName(mentioned, name));
40
- }
41
- export function resolveAudience(audience, candidateNames) {
42
- if (audience.kind === 'bell')
43
- return [...candidateNames];
44
- const resolved = [];
45
- for (const mention of audience.names) {
46
- const known = candidateNames.find((candidate) => sameName(candidate, mention));
47
- if (known !== undefined && !resolved.some((existing) => sameName(existing, known))) {
48
- resolved.push(known);
49
- }
50
- }
51
- return resolved;
52
- }
53
- export function activeListeners(state, sender) {
54
- return state.participants
55
- .filter((participant) => participant.joined
56
- && !(state.ignored.get(nameKey(participant.name)) ?? []).some((target) => sameName(target, sender))
57
- && (state.listening.get(nameKey(participant.name)) ?? []).some((target) => sameName(target, sender)))
58
- .map((participant) => participant.name);
59
- }
60
- export function isIgnored(state, listener, sender) {
61
- return (state.ignored.get(nameKey(listener)) ?? []).some((target) => sameName(target, sender));
62
- }
63
- /** The author of every landed activity, keyed by index; a reply audience derives from this. */
64
- export function activityAuthors(acts) {
65
- const authors = new Map();
66
- for (const act of acts) {
67
- const actor = actorOf(act);
68
- if (actor === undefined || !('index' in act) || typeof act.index !== 'number')
69
- continue;
70
- authors.set(act.index, actor);
71
- }
72
- return authors;
73
- }
74
- function recipientsAtLanding(before, say, authors) {
75
- const audience = audienceOf(say);
76
- const standing = before.participants.filter((participant) => participant.joined).map((participant) => participant.name);
77
- const mentionTargets = resolveAudience(audience, standing);
78
- // A reply is directed attention in its own right: it reaches the author of the
79
- // replied-to activity under the same standing and ignore rules as a mention.
80
- const replyAuthor = say.reply === undefined ? undefined : authors.get(say.reply);
81
- const replyTargets = replyAuthor === undefined ? [] : resolveAudience({ kind: 'mentions', names: [replyAuthor] }, standing);
82
- const listeners = activeListeners(before, say.actor);
83
- const recipients = [];
84
- for (const name of [...mentionTargets, ...replyTargets, ...listeners]) {
85
- if (sameName(name, say.actor) || recipients.some((existing) => sameName(existing, name)))
86
- continue;
87
- if (audience.kind !== 'bell' && isIgnored(before, name, say.actor))
88
- continue;
89
- recipients.push(name);
90
- }
91
- return recipients;
92
- }
93
- export function listeningTo(state, listener) {
94
- return [...(state.listening.get(nameKey(listener)) ?? [])];
95
- }
96
- export function isListening(state, listener, sender) {
97
- return listeningTo(state, listener).some((target) => sameName(target, sender));
98
- }
99
- export function actorOf(act) {
100
- if ('actor' in act && typeof act.actor === 'string')
101
- return act.actor;
102
- return undefined;
103
- }
104
- function touchParticipant(byKey, ordered, actor) {
105
- const key = nameKey(actor);
106
- const existing = byKey.get(key);
107
- if (existing !== undefined)
108
- return existing;
109
- const created = {
110
- name: actor,
111
- key,
112
- joined: false,
113
- done: false,
114
- activityCount: 0,
115
- lastReadThrough: -1,
116
- };
117
- byKey.set(key, created);
118
- ordered.push(created);
119
- return created;
120
- }
121
- function pushThrottleAt(state, at) {
122
- if (typeof at === 'number' && Number.isFinite(at))
123
- state.throttleActivityAts.push(at);
124
- }
125
- function currentParticipant(state, participant) {
126
- return state.participants.find((item) => sameName(item.name, participant));
127
- }
128
- function pushBellAt(state, actor, at) {
129
- if (typeof at !== 'number' || !Number.isFinite(at))
130
- return;
131
- const key = nameKey(actor);
132
- const current = state.bellSayAtsByActor.get(key) ?? [];
133
- current.push(at);
134
- state.bellSayAtsByActor.set(key, current);
135
- }
136
- function bellRecentAt(state, actor, at, windowMs) {
137
- const events = state.bellSayAtsByActor.get(nameKey(actor)) ?? [];
138
- let latest;
139
- for (const eventAt of events) {
140
- if (at - eventAt >= windowMs)
141
- continue;
142
- latest = latest === undefined ? eventAt : Math.max(latest, eventAt);
143
- }
144
- return latest;
145
- }
146
- function createFoldAccumulator() {
147
- const ordered = [];
148
- const byKey = new Map();
149
- const hold = { active: false };
150
- return {
151
- byKey,
152
- ordered,
153
- state: {
154
- participants: ordered,
155
- hold,
156
- joined: [],
157
- done: [],
158
- throttleActivityAts: [],
159
- bellSayAtsByActor: new Map(),
160
- listening: new Map(),
161
- ignored: new Map(),
162
- },
163
- };
164
- }
165
- function applyActivity(accumulator, act) {
166
- const { state, byKey, ordered } = accumulator;
167
- const actor = actorOf(act);
168
- const snapshot = actor === undefined ? undefined : touchParticipant(byKey, ordered, actor);
169
- if (snapshot !== undefined && act.at !== undefined) {
170
- snapshot.lastActiveAt = Math.max(snapshot.lastActiveAt ?? -Infinity, act.at);
171
- }
172
- switch (act.kind) {
173
- case 'join':
174
- if (snapshot !== undefined) {
175
- snapshot.joined = true;
176
- snapshot.done = false;
177
- }
178
- break;
179
- case 'done':
180
- if (snapshot !== undefined) {
181
- snapshot.joined = false;
182
- snapshot.done = true;
183
- state.listening.delete(nameKey(snapshot.name));
184
- }
185
- break;
186
- case 'listen': {
187
- const key = nameKey(act.actor);
188
- const targets = state.listening.get(key) ?? [];
189
- if (!targets.some((target) => sameName(target, act.target)))
190
- targets.push(act.target);
191
- state.listening.set(key, targets);
192
- const ignored = (state.ignored.get(key) ?? []).filter((target) => !sameName(target, act.target));
193
- if (ignored.length === 0)
194
- state.ignored.delete(key);
195
- else
196
- state.ignored.set(key, ignored);
197
- break;
198
- }
199
- case 'ignore': {
200
- const key = nameKey(act.actor);
201
- const targets = (state.listening.get(key) ?? []).filter((target) => !sameName(target, act.target));
202
- if (targets.length === 0)
203
- state.listening.delete(key);
204
- else
205
- state.listening.set(key, targets);
206
- const ignored = state.ignored.get(key) ?? [];
207
- if (!ignored.some((target) => sameName(target, act.target)))
208
- ignored.push(act.target);
209
- state.ignored.set(key, ignored);
210
- break;
211
- }
212
- case 'say':
213
- if (snapshot !== undefined) {
214
- snapshot.activityCount += 1;
215
- }
216
- pushThrottleAt(state, act.at);
217
- if (act.reach === 'bell')
218
- pushBellAt(state, act.actor, act.at);
219
- break;
220
- case 'hold':
221
- state.hold.active = true;
222
- state.hold.at = act.at;
223
- state.hold.reason = act.body;
224
- state.hold.actor = act.actor;
225
- break;
226
- case 'resume':
227
- state.hold.active = false;
228
- delete state.hold.at;
229
- delete state.hold.reason;
230
- delete state.hold.actor;
231
- break;
232
- case 'read':
233
- if (snapshot !== undefined) {
234
- snapshot.lastReadThrough = Math.max(snapshot.lastReadThrough, act.through);
235
- }
236
- break;
237
- }
238
- }
239
- function finishFold(accumulator) {
240
- const { state, ordered } = accumulator;
241
- state.joined = ordered.filter((item) => item.joined).map((item) => item.name);
242
- state.done = ordered.filter((item) => item.done).map((item) => item.name);
243
- return state;
244
- }
245
- export function fold(acts) {
246
- const accumulator = createFoldAccumulator();
247
- for (const act of acts)
248
- applyActivity(accumulator, act);
249
- return finishFold(accumulator);
250
- }
251
- /** Replay the activity stream once to fix every say's audience at landing. */
252
- export function replayLandedAudiences(acts) {
253
- const accumulator = createFoldAccumulator();
254
- const byActivity = new Map();
255
- const byIndex = new Map();
256
- const lastJoinByKey = new Map();
257
- const authors = activityAuthors(acts);
258
- for (const act of acts) {
259
- if (act.kind === 'say') {
260
- const recipients = recipientsAtLanding(accumulator.state, act, authors);
261
- byActivity.set(act, recipients);
262
- const index = 'index' in act ? act.index : undefined;
263
- if (typeof index === 'number')
264
- byIndex.set(index, recipients);
265
- }
266
- if (act.kind === 'join' && 'index' in act && typeof act.index === 'number') {
267
- lastJoinByKey.set(nameKey(act.actor), act.index);
268
- }
269
- applyActivity(accumulator, act);
270
- }
271
- const state = finishFold(accumulator);
272
- const participants = state.participants.map((participant) => participant.name);
273
- function recipientsFor(activity) {
274
- const direct = byActivity.get(activity);
275
- if (direct !== undefined)
276
- return direct;
277
- const index = 'index' in activity ? activity.index : undefined;
278
- return typeof index === 'number' ? byIndex.get(index) ?? [] : [];
279
- }
280
- function resolveParticipant(name) {
281
- return participants.find((participant) => sameName(participant, name));
282
- }
283
- return {
284
- participants,
285
- joined: state.joined,
286
- replayedActivityCount: acts.length,
287
- recipientsFor,
288
- includes: (activity, participant) => activity.kind === 'say'
289
- && !sameName(activity.actor, participant)
290
- && recipientsFor(activity).some((recipient) => sameName(recipient, participant)),
291
- lastJoinIndex: (participant) => lastJoinByKey.get(nameKey(participant)),
292
- resolveParticipant,
293
- };
294
- }
295
- export function validate(state, act, options = {}) {
296
- const actor = actorOf(act);
297
- const current = actor === undefined ? undefined : currentParticipant(state, actor);
298
- switch (act.kind) {
299
- case 'join':
300
- return current?.joined ? { ok: false, reason: 'already_joined' } : { ok: true };
301
- case 'done':
302
- if (current?.done)
303
- return { ok: false, reason: 'done' };
304
- if (current?.joined !== true)
305
- return { ok: false, reason: 'not_joined' };
306
- return { ok: true };
307
- case 'listen':
308
- case 'ignore':
309
- if (current?.done)
310
- return { ok: false, reason: 'done' };
311
- if (current?.joined !== true)
312
- return { ok: false, reason: 'not_joined' };
313
- if (act.kind === 'listen'
314
- && !isListening(state, act.actor, act.target)
315
- && listeningTo(state, act.actor).length >= MAX_IDENTITY_SET_SIZE) {
316
- return { ok: false, reason: 'listening_limit', limit: MAX_IDENTITY_SET_SIZE };
317
- }
318
- return { ok: true };
319
- case 'say': {
320
- if (current?.done)
321
- return { ok: false, reason: 'done' };
322
- if (current?.joined !== true)
323
- return { ok: false, reason: 'not_joined' };
324
- const audience = audienceOf(act);
325
- const mentionCount = audience.kind === 'mentions' ? audience.names.length : 0;
326
- if (mentionCount > MAX_IDENTITY_SET_SIZE) {
327
- return { ok: false, reason: 'mention_limit', limit: MAX_IDENTITY_SET_SIZE };
328
- }
329
- if (state.hold.active)
330
- return { ok: false, reason: 'held', hold: state.hold };
331
- const activityCount = current?.activityCount ?? 0;
332
- if (options.hardCap !== undefined && options.hardCap !== null && activityCount >= options.hardCap) {
333
- return { ok: false, reason: 'hard_cap', count: activityCount, hardCap: options.hardCap };
334
- }
335
- const limit = options.throttlePerMinute;
336
- const at = act.at;
337
- if (act.reach === 'bell' && typeof at === 'number' && Number.isFinite(at)) {
338
- const bellWindowMs = 60 * 60 * 1000;
339
- const latestBellAt = bellRecentAt(state, act.actor, at, bellWindowMs);
340
- if (latestBellAt !== undefined) {
341
- return { ok: false, reason: 'bell_quota', nextAt: latestBellAt + bellWindowMs };
342
- }
343
- }
344
- if (limit !== undefined && typeof at === 'number' && Number.isFinite(at)) {
345
- const windowMs = options.throttleWindowMs ?? 60000;
346
- const recent = state.throttleActivityAts.filter((eventAt) => at - eventAt < windowMs).sort((a, b) => a - b);
347
- if (recent.length >= limit) {
348
- const releaseAt = recent[recent.length - limit] + windowMs;
349
- const delayMs = Math.max(1, releaseAt - at);
350
- return { ok: false, reason: 'throttled', delayMs };
351
- }
352
- }
353
- return { ok: true };
354
- }
355
- case 'hold':
356
- case 'resume':
357
- if (current?.done)
358
- return { ok: false, reason: 'done' };
359
- if (current?.joined !== true)
360
- return { ok: false, reason: 'not_joined' };
361
- return { ok: true };
362
- case 'read':
363
- return { ok: true };
364
- }
365
- }
366
- export function perceive(act, viewer) {
367
- if (act.kind !== 'say')
368
- return 'full';
369
- if (sameName(act.actor, viewer))
370
- return 'full';
371
- return audienceIncludes(audienceOf(act), viewer) ? 'full' : 'presence';
26
+ return audience.kind === 'bell' || audience.names.some((mentioned) => sameName(mentioned, name));
372
27
  }
@@ -0,0 +1,24 @@
1
+ import { type StoredAct } from './model.js';
2
+ import type { AttentionFact, LandedRecipient, ParticipantFact, SquareRead, SquareWrite } from './ports.js';
3
+ export interface LandingEffect {
4
+ readonly activity: StoredAct;
5
+ readonly participant?: ParticipantFact;
6
+ readonly removeListeningOf?: string;
7
+ readonly replaceAttention?: AttentionFact;
8
+ readonly audience: readonly LandedRecipient[];
9
+ }
10
+ export interface LandingFacts {
11
+ readonly actor?: ParticipantFact;
12
+ readonly standing: readonly ParticipantFact[];
13
+ readonly incoming: readonly AttentionFact[];
14
+ readonly outgoing: readonly AttentionFact[];
15
+ readonly replyAuthor?: string;
16
+ }
17
+ /** The single landing transition, shared by both adapters and explicit rebuild. */
18
+ export declare function deriveLanding(activity: StoredAct, facts: LandingFacts): LandingEffect;
19
+ export declare function landingFacts(read: SquareRead, activity: StoredAct): LandingFacts;
20
+ export interface LandingWriter extends Pick<SquareWrite, 'markSeen' | 'putLease' | 'removeLease' | 'replaceRoutes'> {
21
+ applyLanding(effect: LandingEffect): void;
22
+ }
23
+ /** Driver-owned methods validate rows/writes and track changes; the domain owns allocation. */
24
+ export declare function wrapSquareWrite(read: SquareRead, writes: LandingWriter): SquareWrite;
@@ -0,0 +1,77 @@
1
+ import { SquareError, sameName } from './model.js';
2
+ /** The single landing transition, shared by both adapters and explicit rebuild. */
3
+ export function deriveLanding(activity, facts) {
4
+ const audience = [];
5
+ if (activity.kind === 'say') {
6
+ const add = (requested, route) => {
7
+ const recipient = facts.standing.find((item) => sameName(item.name, requested))?.name;
8
+ if (recipient === undefined || sameName(recipient, activity.actor)
9
+ || audience.some((item) => sameName(item.recipient, recipient)))
10
+ return;
11
+ if (activity.reach !== 'bell' && facts.incoming.some((item) => item.kind === 'ignore' && sameName(item.listener, recipient)))
12
+ return;
13
+ audience.push({ recipient, route });
14
+ };
15
+ if (activity.reach === 'bell') {
16
+ for (const peer of facts.standing)
17
+ add(peer.name, 'bell');
18
+ }
19
+ else {
20
+ for (const mention of activity.mentions ?? [])
21
+ add(mention, 'mention');
22
+ if (facts.replyAuthor !== undefined)
23
+ add(facts.replyAuthor, 'reply');
24
+ for (const peer of facts.standing) {
25
+ if (facts.incoming.some((item) => item.kind === 'listen' && sameName(item.listener, peer.name)))
26
+ add(peer.name, 'attention');
27
+ }
28
+ }
29
+ }
30
+ const actor = activity.actor;
31
+ if (actor === undefined)
32
+ return { activity, audience };
33
+ const previous = facts.actor ?? { name: actor, firstIndex: activity.index, joined: false, done: false, sayCount: 0 };
34
+ const participant = {
35
+ ...previous,
36
+ lastActiveAt: Math.max(previous.lastActiveAt ?? -Infinity, activity.at),
37
+ ...(activity.kind === 'join' ? { joined: true, done: false, lastJoinIndex: activity.index } : {}),
38
+ ...(activity.kind === 'done' ? { joined: false, done: true } : {}),
39
+ ...(activity.kind === 'say' ? { sayCount: previous.sayCount + 1 } : {}),
40
+ };
41
+ let replaceAttention;
42
+ if (activity.kind === 'listen' || activity.kind === 'ignore') {
43
+ const existing = facts.outgoing.find((item) => sameName(item.target, activity.target));
44
+ if (existing?.kind !== activity.kind) {
45
+ replaceAttention = { listener: participant.name, target: activity.target, kind: activity.kind, sinceIndex: activity.index };
46
+ }
47
+ }
48
+ return {
49
+ activity, participant, audience,
50
+ ...(activity.kind === 'done' ? { removeListeningOf: participant.name } : {}),
51
+ ...(replaceAttention === undefined ? {} : { replaceAttention }),
52
+ };
53
+ }
54
+ export function landingFacts(read, activity) {
55
+ return {
56
+ actor: activity.actor === undefined ? undefined : read.participant(activity.actor),
57
+ standing: activity.kind === 'say' ? read.roster().filter((item) => item.joined) : [],
58
+ incoming: activity.kind === 'say' ? read.incomingAttention(activity.actor) : [],
59
+ outgoing: activity.kind === 'listen' || activity.kind === 'ignore' ? read.attention(activity.actor) : [],
60
+ replyAuthor: activity.kind === 'say' && activity.reply !== undefined ? read.activity(activity.reply)?.actor : undefined,
61
+ };
62
+ }
63
+ /** Driver-owned methods validate rows/writes and track changes; the domain owns allocation. */
64
+ export function wrapSquareWrite(read, writes) {
65
+ const { applyLanding, ...mutations } = writes;
66
+ return Object.assign({}, read, mutations, {
67
+ land(activity) {
68
+ const index = read.header().nextActIndex;
69
+ if (!Number.isSafeInteger(index) || index < 0 || index >= Number.MAX_SAFE_INTEGER) {
70
+ throw new SquareError('invalid_args', 'Square activity index is exhausted.');
71
+ }
72
+ const stored = { ...activity, index };
73
+ applyLanding(deriveLanding(stored, landingFacts(read, stored)));
74
+ return read.activity(index);
75
+ },
76
+ });
77
+ }