@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
package/dist/artifact.js CHANGED
@@ -4,196 +4,351 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { setTimeout as sleep } from 'node:timers/promises';
7
- import { isDeepStrictEqual } from 'node:util';
8
7
  import { observeFileVersion } from './file-changes.js';
8
+ import { canonicalPath } from './canonical-path.js';
9
9
  import { InternalSquareError, SquareError } from './model.js';
10
- import { validateSquareState } from './square-state.js';
11
- const APPLICATION_ID = 0x53515245;
12
- const USER_VERSION = 1;
13
- const SNAPSHOT_TABLE = 'square_snapshot';
14
- const SNAPSHOT_DDL = `CREATE TABLE ${SNAPSHOT_TABLE} (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL CHECK(revision>=0), state TEXT NOT NULL)`;
15
- const SNAPSHOT_COLUMNS = [
16
- { cid: 0, name: 'id', type: 'INTEGER', notnull: 0, dflt_value: null, pk: 1, hidden: 0 },
17
- { cid: 1, name: 'revision', type: 'INTEGER', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
18
- { cid: 2, name: 'state', type: 'TEXT', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
19
- ];
20
- export { createSquareState, emptyRuntimeState } from './square-state.js';
21
- function invalidArtifact(detail) {
22
- return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
23
- }
24
- function requireSquareExtension(squarePath) {
25
- if (!squarePath.endsWith('.square'))
26
- throw new SquareError('invalid_args', `Square artifacts must use the .square extension: ${squarePath}`);
10
+ import { wrapSquareWrite } from './square-domain.js';
11
+ import { APPLICATION_ID, createSqliteDriver, initializeSqlite, exportSqliteState, inspectSqlite, invalidArtifact, validateSqliteHeader, emptyRuntimeState } from './square-sqlite.js';
12
+ const guideContents = new Map();
13
+ function readGuide(name) {
14
+ if (!['participant', 'architect', 'brainstorm'].includes(name))
15
+ throw new SquareError('invalid_args', `Unknown square guide: ${name}`);
16
+ let content = guideContents.get(name);
17
+ if (content === undefined) {
18
+ content = fs.readFileSync(new URL(`../guides/${name}.md`, import.meta.url), 'utf8').trim();
19
+ guideContents.set(name, content);
20
+ }
21
+ return content;
22
+ }
23
+ function normalizedLines(value) {
24
+ const normalized = value.replace(/\r\n/g, '\n').trim();
25
+ return normalized === '' ? [] : normalized.split('\n');
26
+ }
27
+ /** All build paths share configuration validation and lazy guide preparation. */
28
+ export function prepareSquareBuild(options, snippet) {
29
+ if (options.hardCap !== undefined && options.hardCap !== null
30
+ && (!Number.isSafeInteger(options.hardCap) || options.hardCap < 1))
31
+ throw new SquareError('invalid_args', 'hardCap must be a positive integer or null');
32
+ if (options.throttlePerMinute !== undefined
33
+ && (!Number.isSafeInteger(options.throttlePerMinute) || options.throttlePerMinute < 1))
34
+ throw new SquareError('invalid_args', 'throttlePerMinute must be a positive integer');
35
+ const guides = [readGuide('participant')];
36
+ if (options.template !== undefined)
37
+ guides.push(readGuide(options.template));
38
+ return {
39
+ hardCap: options.hardCap ?? null,
40
+ ...(options.throttlePerMinute === undefined ? {} : { throttlePerMinute: options.throttlePerMinute }),
41
+ preamble: normalizedLines(snippet), warmup: normalizedLines(guides.join('\n\n')),
42
+ acts: [], routes: [], runtime: emptyRuntimeState(),
43
+ };
44
+ }
45
+ /** File build owns publication and CLI conflict semantics, not host resources. */
46
+ export async function createSquare(location, options, snippet, signal) {
47
+ const state = prepareSquareBuild(options, snippet);
48
+ if (options.force) {
49
+ await writeSquareFile(location, state, signal);
50
+ }
51
+ else if (!await createSquareFile(location, state, signal)) {
52
+ throw new InternalSquareError('conflict', `Refusing to overwrite existing square: ${location}\nPass -f to overwrite.`);
53
+ }
54
+ return { hardCap: state.hardCap, ...(state.throttlePerMinute === undefined ? {} : { throttlePerMinute: state.throttlePerMinute }) };
27
55
  }
28
- function isNotFound(error) {
29
- return error instanceof Error && /unable to open database file|no such file|SQLITE_CANTOPEN/i.test(error.message);
56
+ function requireSquareExtension(location) {
57
+ if (!location.endsWith('.square'))
58
+ throw new SquareError('invalid_args', `Square artifacts must use the .square extension: ${location}`);
30
59
  }
31
- function isNotADatabase(error) {
32
- return error instanceof Error && /not a database|file is not a database|SQLITE_NOTADB/i.test(error.message);
60
+ function isBusy(error) {
61
+ return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
33
62
  }
34
- function openExistingDatabase(squarePath) {
35
- const uri = pathToFileURL(path.resolve(squarePath));
63
+ function storageError(error, location) {
64
+ if (!(error instanceof Error) || error instanceof SquareError || error instanceof InternalSquareError)
65
+ return error;
66
+ if (/unable to open database file|no such file|SQLITE_CANTOPEN/i.test(error.message))
67
+ return new InternalSquareError('not_found', `square file not found: ${location}`);
68
+ if (/not a database|SQLITE_NOTADB/i.test(error.message))
69
+ return invalidArtifact('not a SQLite database.');
70
+ return error;
71
+ }
72
+ /** Private artifact-boundary connection shared with explicit migration. Never creates a path. */
73
+ export function openExistingSquareDatabase(location) {
74
+ requireSquareExtension(location);
75
+ const uri = pathToFileURL(path.resolve(location));
36
76
  uri.searchParams.set('mode', 'rw');
37
77
  try {
38
78
  return new DatabaseSync(uri.href);
39
79
  }
40
80
  catch (error) {
41
- if (isNotFound(error))
42
- throw new InternalSquareError('not_found', `square file not found: ${squarePath}`);
43
- if (isNotADatabase(error))
44
- throw invalidArtifact('not a SQLite database.');
45
- throw error;
81
+ throw storageError(error, location);
46
82
  }
47
83
  }
48
- function closeQuietly(database) {
49
- try {
50
- database?.close();
51
- }
52
- catch { /* closing does not alter the primary failure */ }
84
+ function abort(signal) {
85
+ if (signal?.aborted)
86
+ throw signal.reason ?? new Error('Square artifact operation aborted');
53
87
  }
54
- function parseState(state) {
55
- if (typeof state !== 'string')
56
- throw invalidArtifact('snapshot state is malformed.');
57
- try {
58
- return validateSquareState(JSON.parse(state));
59
- }
60
- catch (error) {
61
- if (error instanceof SquareError)
62
- throw error;
63
- throw invalidArtifact('snapshot state is not valid JSON.');
88
+ export async function retrySqliteBusy(operation, signal) {
89
+ while (true) {
90
+ abort(signal);
91
+ try {
92
+ return operation();
93
+ }
94
+ catch (error) {
95
+ if (!isBusy(error))
96
+ throw error;
97
+ try {
98
+ await sleep(25, undefined, { signal });
99
+ }
100
+ catch (error) {
101
+ abort(signal);
102
+ throw error;
103
+ }
104
+ }
64
105
  }
65
106
  }
66
- function normalizedSql(sql) {
67
- return sql.trim().replace(/\s+/g, ' ').replace(/\s*(>=|[(),=])\s*/g, '$1');
68
- }
69
- function isOwnedSnapshotSchema(database) {
70
- const objects = database.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' ORDER BY type, name")
71
- .all();
72
- if (objects.length !== 1)
73
- return false;
74
- const [object] = objects;
75
- if (object.type !== 'table' || object.name !== SNAPSHOT_TABLE || object.tbl_name !== SNAPSHOT_TABLE
76
- || typeof object.sql !== 'string' || normalizedSql(object.sql) !== normalizedSql(SNAPSHOT_DDL))
77
- return false;
78
- const columns = database.prepare('SELECT cid, name, type, "notnull", dflt_value, pk, hidden FROM pragma_table_xinfo(?) ORDER BY cid')
79
- .all(SNAPSHOT_TABLE);
80
- return columns.length === SNAPSHOT_COLUMNS.length && columns.every((column, index) => {
81
- const expected = SNAPSHOT_COLUMNS[index];
82
- return column.cid === expected.cid && column.name === expected.name && column.type === expected.type
83
- && column.notnull === expected.notnull && column.dflt_value === expected.dflt_value
84
- && column.pk === expected.pk && column.hidden === expected.hidden;
85
- });
86
- }
87
- function validateDatabaseHeader(database) {
88
- const identity = database.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
89
- if (identity.application_id !== APPLICATION_ID)
90
- throw invalidArtifact('unrelated SQLite database.');
91
- if (identity.user_version !== USER_VERSION)
92
- throw invalidArtifact('unsupported SQLite format version.');
93
- if (!isOwnedSnapshotSchema(database))
94
- throw invalidArtifact('unsupported snapshot schema.');
95
- }
96
- function validateSnapshotRevision(rows) {
97
- if (rows.length !== 1 || rows[0].id !== 1 || !Number.isSafeInteger(rows[0].revision) || rows[0].revision < 0)
98
- throw invalidArtifact('snapshot row is malformed.');
99
- return rows[0].revision;
107
+ function synchronous(value) {
108
+ if (value !== null && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function') {
109
+ void Promise.resolve(value).catch(() => undefined);
110
+ throw new TypeError('Square artifact callbacks must be synchronous.');
111
+ }
100
112
  }
101
- function validateDatabase(database) {
113
+ /** Acquisition may retry; after change starts, only COMMIT retries, retaining its result. */
114
+ export async function inSqliteTransaction(db, write, validate, change, signal) {
115
+ let begun = false;
102
116
  try {
103
- validateDatabaseHeader(database);
104
- const rows = database.prepare(`SELECT id, revision, state FROM ${SNAPSHOT_TABLE}`).all();
105
- const revision = validateSnapshotRevision(rows);
106
- return { state: parseState(rows[0].state), revision };
117
+ await retrySqliteBusy(() => {
118
+ db.exec(write ? 'BEGIN IMMEDIATE' : 'BEGIN');
119
+ begun = true;
120
+ try {
121
+ validate();
122
+ }
123
+ catch (error) {
124
+ db.exec('ROLLBACK');
125
+ begun = false;
126
+ throw error;
127
+ }
128
+ }, signal);
129
+ abort(signal);
130
+ const result = change();
131
+ synchronous(result);
132
+ await retrySqliteBusy(() => db.exec('COMMIT'), signal);
133
+ begun = false;
134
+ return result;
107
135
  }
108
136
  catch (error) {
109
- if (error instanceof SquareError)
110
- throw error;
111
- if (isNotADatabase(error))
112
- throw invalidArtifact('not a SQLite database.');
137
+ if (begun) {
138
+ try {
139
+ db.exec('ROLLBACK');
140
+ }
141
+ catch { /* retain the original failure */ }
142
+ }
113
143
  throw error;
114
144
  }
115
145
  }
116
- /** Opens an existing artifact without permitting SQLite to create a missing path. */
117
- function readExistingArtifact(squarePath, read) {
118
- requireSquareExtension(squarePath);
146
+ const readMethods = [
147
+ 'header', 'participant', 'roster', 'attention', 'incomingAttention', 'hold',
148
+ 'activity', 'activityHeads', 'activities', 'activityCount', 'contextIndexes', 'sayOrdinals', 'recentSayTimes',
149
+ 'audience', 'directed', 'directedAt', 'directedCount', 'pendingAudience',
150
+ 'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'routes',
151
+ ];
152
+ const writeMethods = ['applyLanding', 'markSeen', 'putLease', 'removeLease', 'replaceRoutes'];
153
+ /** One connection, ordered operations, guarded synchronous transaction capabilities. */
154
+ export function createArtifactPort(location, initial, externalSignal) {
119
155
  let database;
120
- try {
121
- database = openExistingDatabase(squarePath);
122
- return read(database);
123
- }
124
- catch (error) {
125
- if (error instanceof SquareError)
156
+ let closed = false, inCallback = false;
157
+ let tail = Promise.resolve();
158
+ const cancel = new AbortController();
159
+ const canonical = initial === undefined ? canonicalPath(location) : Promise.resolve(location);
160
+ if (initial !== undefined) {
161
+ database = new DatabaseSync(':memory:');
162
+ try {
163
+ initializeSqlite(database, initial);
164
+ database.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=2`);
165
+ }
166
+ catch (error) {
167
+ database.close();
126
168
  throw error;
127
- if (isNotFound(error))
128
- throw new InternalSquareError('not_found', `square file not found: ${squarePath}`);
129
- if (isNotADatabase(error))
130
- throw invalidArtifact('not a SQLite database.');
131
- throw error;
132
- }
133
- finally {
134
- closeQuietly(database);
169
+ }
135
170
  }
136
- }
137
- function readSquareSnapshotOnce(squarePath) {
138
- return readExistingArtifact(squarePath, validateDatabase);
139
- }
140
- async function createTemporaryArtifact(squarePath) {
141
- const directory = path.dirname(squarePath);
142
- await fs.promises.mkdir(directory, { recursive: true });
143
- while (true) {
144
- const temporary = path.join(directory, `.${path.basename(squarePath)}.${process.pid}.${randomUUID()}.tmp`);
171
+ const combined = (signal) => AbortSignal.any([cancel.signal, ...(externalSignal ? [externalSignal] : []), ...(signal ? [signal] : [])]);
172
+ const validate = (db) => validateSqliteHeader(db, initial === undefined ? location : ':memory:');
173
+ async function connection(signal) {
174
+ if (database)
175
+ return database;
176
+ location = await canonical;
177
+ abort(signal);
178
+ database = openExistingSquareDatabase(location);
145
179
  try {
146
- const handle = await fs.promises.open(temporary, 'wx');
147
- await handle.close();
148
- return temporary;
180
+ await retrySqliteBusy(() => validate(database), signal);
181
+ database.exec('PRAGMA busy_timeout=0; PRAGMA synchronous=FULL');
149
182
  }
150
183
  catch (error) {
151
- if (error.code === 'EEXIST')
152
- continue;
153
- throw error;
184
+ database.close();
185
+ database = undefined;
186
+ throw storageError(error, location);
154
187
  }
188
+ return database;
155
189
  }
156
- }
157
- async function syncTemporaryArtifact(temporary) {
158
- // Windows FlushFileBuffers requires a writable handle; r+ never creates a missing file.
159
- const handle = await fs.promises.open(temporary, 'r+');
190
+ function queue(operation, signal) {
191
+ if (inCallback)
192
+ throw new TypeError('Square artifact operations cannot nest inside a callback.');
193
+ if (closed)
194
+ return Promise.reject(new Error('Square artifact is closed'));
195
+ const operationSignal = combined(signal);
196
+ const result = tail.then(() => {
197
+ abort(operationSignal);
198
+ return operation(operationSignal);
199
+ });
200
+ tail = result.then(() => undefined, () => undefined);
201
+ return result;
202
+ }
203
+ async function transaction(callback, write, signal) {
204
+ const db = await connection(signal);
205
+ return inSqliteTransaction(db, write, () => validate(db), () => {
206
+ const driver = createSqliteDriver(db), version = driver.header().revision;
207
+ let active = true, failed = false, failure;
208
+ function guard(fn) {
209
+ return ((...args) => {
210
+ if (!active)
211
+ throw new Error('Square artifact transaction capability expired');
212
+ abort(signal);
213
+ if (failed)
214
+ throw failure;
215
+ try {
216
+ return fn(...args);
217
+ }
218
+ catch (error) {
219
+ failed = true;
220
+ failure = storageError(error, location);
221
+ throw failure;
222
+ }
223
+ });
224
+ }
225
+ const reads = Object.fromEntries(readMethods.map(name => [name, guard(driver[name])]));
226
+ const writes = Object.fromEntries(writeMethods.map(name => [name, guard(driver[name])]));
227
+ const facts = write ? wrapSquareWrite(reads, writes) : reads;
228
+ let value;
229
+ inCallback = true;
230
+ try {
231
+ value = callback(facts);
232
+ synchronous(value);
233
+ }
234
+ finally {
235
+ inCallback = false;
236
+ active = false;
237
+ }
238
+ if (failed)
239
+ throw failure;
240
+ const changed = write && driver.changed;
241
+ if (changed) {
242
+ if (version === Number.MAX_SAFE_INTEGER)
243
+ throw invalidArtifact('revision cannot advance beyond the safe integer limit.');
244
+ db.prepare('UPDATE square_meta SET revision=? WHERE id=1').run(version + 1);
245
+ }
246
+ return {
247
+ value, version: version + Number(changed), changed
248
+ };
249
+ }, signal);
250
+ }
251
+ const revision = signal => queue(async (s) => {
252
+ const db = await connection(s);
253
+ return inSqliteTransaction(db, false, () => validate(db), () => createSqliteDriver(db).header().revision, s);
254
+ }, signal);
255
+ return {
256
+ read: (select, signal) => queue(async (s) => {
257
+ const { value, version } = await transaction(select, false, s);
258
+ return { value, version };
259
+ }, signal),
260
+ transact: (change, signal) => queue(s => transaction(change, true, s), signal),
261
+ revision,
262
+ async changed(since, timeout, signal) {
263
+ if (inCallback)
264
+ throw new TypeError('Square artifact operations cannot nest inside a callback.');
265
+ if (closed)
266
+ return false;
267
+ const s = combined(signal), deadline = Date.now() + Math.max(0, timeout);
268
+ if (initial !== undefined) {
269
+ try {
270
+ do {
271
+ if (await revision(s) > since)
272
+ return true;
273
+ if (Date.now() >= deadline)
274
+ return false;
275
+ await sleep(Math.min(25, deadline - Date.now()), undefined, { signal: s });
276
+ } while (!closed);
277
+ }
278
+ catch (error) {
279
+ if (closed)
280
+ return false;
281
+ abort(s);
282
+ throw error;
283
+ }
284
+ return false;
285
+ }
286
+ const observer = await observeSquareRevision(await canonical);
287
+ try {
288
+ return await observer.changed(since, Math.max(0, deadline - Date.now()), s);
289
+ }
290
+ catch (error) {
291
+ if (closed)
292
+ return false;
293
+ throw error;
294
+ }
295
+ finally {
296
+ observer.close();
297
+ }
298
+ },
299
+ async close() {
300
+ if (inCallback)
301
+ throw new TypeError('Square artifact cannot close inside a callback.');
302
+ if (closed) {
303
+ await tail;
304
+ return;
305
+ }
306
+ closed = true;
307
+ cancel.abort(new Error('Square artifact is closed'));
308
+ await tail;
309
+ database?.close();
310
+ database = undefined;
311
+ },
312
+ };
313
+ }
314
+ async function temporaryArtifact(location) {
315
+ await fs.promises.mkdir(path.dirname(location), { recursive: true });
316
+ const temporary = path.join(path.dirname(location), `.${path.basename(location)}.${process.pid}.${randomUUID()}.tmp`);
317
+ const handle = await fs.promises.open(temporary, 'wx');
318
+ await handle.close();
319
+ return temporary;
320
+ }
321
+ async function initializeFile(location, state, signal) {
322
+ const db = new DatabaseSync(location);
160
323
  try {
161
- await handle.sync();
324
+ db.exec('PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL');
325
+ await inSqliteTransaction(db, true, () => { }, () => {
326
+ initializeSqlite(db, state);
327
+ db.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=2`);
328
+ }, signal);
162
329
  }
163
330
  finally {
164
- await handle.close();
331
+ db.close();
165
332
  }
166
- }
167
- function initializeDatabase(squarePath, state) {
168
- const encoded = JSON.stringify(state);
169
- let database;
333
+ const handle = await fs.promises.open(location, 'r+');
170
334
  try {
171
- database = openExistingDatabase(squarePath);
172
- database.exec(`PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; BEGIN IMMEDIATE; PRAGMA application_id = ${APPLICATION_ID}; PRAGMA user_version = ${USER_VERSION}; ${SNAPSHOT_DDL};`);
173
- database.prepare(`INSERT INTO ${SNAPSHOT_TABLE} (id, revision, state) VALUES (1, 0, ?)`).run(encoded);
174
- database.exec('COMMIT;');
175
- }
176
- catch (error) {
177
- try {
178
- database?.exec('ROLLBACK;');
179
- }
180
- catch { /* retain initialization failure */ }
181
- throw error;
335
+ await handle.sync();
182
336
  }
183
337
  finally {
184
- closeQuietly(database);
338
+ await handle.close();
185
339
  }
186
340
  }
187
- /** Creates a new SQLite artifact only when its pathname did not already exist. */
188
- export async function createSquareFile(squarePath, state) {
189
- requireSquareExtension(squarePath);
190
- const validated = validateSquareState(state);
191
- const temporary = await createTemporaryArtifact(squarePath);
341
+ /** New-format creation; initial authority is accepted only at this explicit boundary. */
342
+ export async function createSquareFile(location, state, signal) {
343
+ abort(signal);
344
+ location = await canonicalPath(location);
345
+ requireSquareExtension(location);
346
+ const temporary = await temporaryArtifact(location);
192
347
  try {
193
- initializeDatabase(temporary, validated);
194
- await syncTemporaryArtifact(temporary);
348
+ await initializeFile(temporary, state, signal);
349
+ abort(signal);
195
350
  try {
196
- await fs.promises.link(temporary, squarePath);
351
+ await fs.promises.link(temporary, location);
197
352
  return true;
198
353
  }
199
354
  catch (error) {
@@ -203,197 +358,71 @@ export async function createSquareFile(squarePath, state) {
203
358
  }
204
359
  }
205
360
  finally {
206
- try {
207
- await fs.promises.unlink(temporary);
208
- }
209
- catch { /* cleanup cannot invalidate a published artifact */ }
361
+ await fs.promises.unlink(temporary).catch(() => undefined);
210
362
  }
211
363
  }
212
- function isBusy(error) {
213
- return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
214
- }
215
- function isThenable(value) {
216
- return (typeof value === 'object' || typeof value === 'function') && value !== null
217
- && typeof value.then === 'function';
218
- }
219
- function discardThenable(value) {
220
- void Promise.resolve(value).catch(() => undefined);
221
- }
222
- function closedError() {
223
- return new Error('Square artifact is closed');
364
+ /** Explicit build --force / fixture import, never a runtime state-replacement callback. */
365
+ export async function writeSquareFile(location, state, signal) {
366
+ location = await canonicalPath(location);
367
+ if (await createSquareFile(location, state, signal))
368
+ return;
369
+ abort(signal);
370
+ const db = openExistingSquareDatabase(location);
371
+ try {
372
+ await retrySqliteBusy(() => validateSqliteHeader(db, location), signal);
373
+ db.exec('PRAGMA busy_timeout=0; PRAGMA synchronous=FULL');
374
+ await inSqliteTransaction(db, true, () => validateSqliteHeader(db, location), () => {
375
+ const revision = createSqliteDriver(db).header().revision;
376
+ if (revision === Number.MAX_SAFE_INTEGER)
377
+ throw invalidArtifact('revision cannot advance beyond the safe integer limit.');
378
+ for (const table of ['landed_audience', 'attention', 'participants', 'routes', 'catch_leases', 'receipts', 'activities', 'square_meta'])
379
+ db.exec(`DROP TABLE ${table}`);
380
+ initializeSqlite(db, state, revision + 1);
381
+ }, signal);
382
+ }
383
+ finally {
384
+ db.close();
385
+ }
224
386
  }
225
- async function pauseForBusy(signal) {
226
- if (signal?.aborted)
227
- throw signal.reason ?? closedError();
387
+ async function inspectFile(location, select, signal) {
388
+ const db = openExistingSquareDatabase(location);
228
389
  try {
229
- await sleep(25, undefined, { signal });
390
+ return await inSqliteTransaction(db, false, () => validateSqliteHeader(db, location), () => select(db), signal);
230
391
  }
231
392
  catch (error) {
232
- if (signal?.aborted)
233
- throw signal.reason ?? closedError();
234
- throw error;
393
+ throw storageError(error, location);
394
+ }
395
+ finally {
396
+ db.close();
235
397
  }
236
398
  }
237
- /**
238
- * Reads a complete validated snapshot, yielding between transient SQLite lock
239
- * conflicts. The URI remains create-disabled throughout the retry loop.
240
- */
241
- export function readSquareSnapshot(squarePath, signal) {
242
- return readWithBusyRetry(() => readSquareSnapshotOnce(squarePath), signal);
399
+ /** Explicit test/export projection. Hot callers must use the typed port. */
400
+ export function readSquareSnapshot(location, signal) {
401
+ return inspectFile(location, db => ({ state: exportSqliteState(db), revision: createSqliteDriver(db).header().revision }), signal);
243
402
  }
244
- /** Check the artifact revision without loading or parsing the historical state. */
245
- export function readSquareRevision(squarePath, signal) {
246
- return readWithBusyRetry(() => readExistingArtifact(squarePath, (database) => {
247
- validateDatabaseHeader(database);
248
- const rows = database.prepare(`SELECT id, revision FROM ${SNAPSHOT_TABLE}`).all();
249
- return validateSnapshotRevision(rows);
250
- }), signal);
403
+ export function readSquareRevision(location, signal) {
404
+ return inspectFile(location, db => createSqliteDriver(db).header().revision, signal);
251
405
  }
252
- /** One reference-counted revision detector per canonical artifact in this process. */
253
- export function observeSquareRevision(squarePath) {
254
- return observeFileVersion(squarePath, 'square-revision', async (file, signal) => {
406
+ export function observeSquareRevision(location) {
407
+ return observeFileVersion(location, 'square-revision', async (file, signal) => {
255
408
  try {
256
409
  return await readSquareRevision(file, signal);
257
410
  }
258
411
  catch (error) {
259
- if (signal?.aborted)
260
- throw signal.reason ?? error;
412
+ abort(signal);
261
413
  if (error instanceof InternalSquareError && error.code === 'not_found')
262
414
  return null;
263
415
  throw error;
264
416
  }
265
417
  });
266
418
  }
267
- async function readWithBusyRetry(read, signal) {
268
- while (true) {
269
- if (signal?.aborted)
270
- throw signal.reason ?? closedError();
271
- try {
272
- return read();
273
- }
274
- catch (error) {
275
- if (!isBusy(error))
276
- throw error;
277
- await pauseForBusy(signal);
278
- }
279
- }
280
- }
281
- /**
282
- * Executes one synchronous SquareState transition under SQLite's write lock.
283
- * A busy COMMIT retries on this same transaction, so fn is never replayed.
284
- */
285
- export async function transactSquareSnapshot(squarePath, fn, signal, forceCommit = false) {
286
- requireSquareExtension(squarePath);
287
- let database;
288
- let begun = false;
289
- try {
290
- database = openExistingDatabase(squarePath);
291
- // Validate before any persistent pragma can touch an unsupported artifact.
292
- while (true) {
293
- try {
294
- validateDatabase(database);
295
- break;
296
- }
297
- catch (error) {
298
- if (!isBusy(error))
299
- throw error;
300
- await pauseForBusy(signal);
301
- }
302
- }
303
- while (true) {
304
- if (signal?.aborted)
305
- throw signal.reason ?? closedError();
306
- try {
307
- database.exec('PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL;');
308
- database.exec('BEGIN IMMEDIATE;');
309
- begun = true;
310
- break;
311
- }
312
- catch (error) {
313
- if (!isBusy(error))
314
- throw error;
315
- await pauseForBusy(signal);
316
- }
317
- }
318
- const current = validateDatabase(database);
319
- const outcome = fn(structuredClone(current.state), current.revision);
320
- if (isThenable(outcome)) {
321
- discardThenable(outcome);
322
- throw new TypeError('Square artifact transitions must be synchronous.');
323
- }
324
- if (typeof outcome !== 'object' || outcome === null || !Object.hasOwn(outcome, 'result')) {
325
- throw new TypeError('Square artifact transition must return { state?, result }.');
326
- }
327
- if (outcome.state === undefined || (!forceCommit && isDeepStrictEqual(outcome.state, current.state))) {
328
- database.exec('ROLLBACK;');
329
- begun = false;
330
- return { result: outcome.result, revision: current.revision, changed: false };
331
- }
332
- const state = validateSquareState(outcome.state);
333
- if (current.revision === Number.MAX_SAFE_INTEGER) {
334
- throw invalidArtifact('snapshot revision cannot advance beyond the safe integer limit.');
335
- }
336
- const revision = current.revision + 1;
337
- database.prepare(`UPDATE ${SNAPSHOT_TABLE} SET revision = ?, state = ? WHERE id = 1`)
338
- .run(revision, JSON.stringify(state));
339
- while (true) {
340
- if (signal?.aborted)
341
- throw signal.reason ?? closedError();
342
- try {
343
- database.exec('COMMIT;');
344
- begun = false;
345
- return { result: outcome.result, revision, changed: true };
346
- }
347
- catch (error) {
348
- if (!isBusy(error))
349
- throw error;
350
- await pauseForBusy(signal);
351
- }
352
- }
353
- }
354
- catch (error) {
355
- if (begun) {
356
- try {
357
- database?.exec('ROLLBACK;');
358
- }
359
- catch { /* retain callback and SQLite failures */ }
360
- }
361
- throw error;
362
- }
363
- finally {
364
- closeQuietly(database);
365
- }
366
- }
367
- /** Replaces an existing validated snapshot in place and always advances its revision. */
368
- export async function replaceSquareSnapshot(squarePath, state) {
369
- const replacement = structuredClone(validateSquareState(state));
370
- const committed = await transactSquareSnapshot(squarePath, () => ({ state: replacement, result: undefined }), undefined, true);
371
- return { state: replacement, revision: committed.revision };
372
- }
373
- export async function writeSquareFile(squarePath, squareState) {
374
- requireSquareExtension(squarePath);
375
- if (await createSquareFile(squarePath, squareState))
376
- return;
377
- await replaceSquareSnapshot(squarePath, squareState);
378
- }
379
- export async function loadSquare(squarePath) {
380
- return structuredClone((await readSquareSnapshot(squarePath)).state);
381
- }
382
- export async function probeSquare(squarePath) {
383
- if (!squarePath.endsWith('.square'))
384
- return undefined;
385
- try {
386
- return structuredClone(readSquareSnapshotOnce(squarePath).state);
387
- }
388
- catch {
389
- return undefined;
390
- }
391
- }
392
- export async function diagnoseSquareFile(squarePath) {
419
+ export async function diagnoseSquareFile(location) {
393
420
  try {
394
- return { problems: [], state: await loadSquare(squarePath) };
421
+ return { problems: [], ...await inspectFile(await canonicalPath(location), inspectSqlite) };
395
422
  }
396
423
  catch (error) {
424
+ if (error instanceof SquareError && error.facts?.reason === 'migration_required')
425
+ return { migrationRequired: error.message, problems: [] };
397
426
  return { unfixable: error instanceof Error ? error.message : String(error), problems: [] };
398
427
  }
399
428
  }