@astrosheep/square 0.3.69 → 0.3.71
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/activity-feed.d.ts +4 -5
- package/dist/activity-feed.js +14 -10
- package/dist/artifact.d.ts +26 -33
- package/dist/artifact.js +346 -317
- package/dist/attention-presentation.js +2 -9
- package/dist/automatic-session.js +6 -6
- package/dist/boundary-presentation.js +3 -3
- package/dist/catch-decisions.d.ts +19 -12
- package/dist/catch-decisions.js +32 -68
- package/dist/claude-delivery.js +7 -7
- package/dist/claude-mod.js +18 -7
- package/dist/cli/catch-presentation.d.ts +7 -0
- package/dist/cli/catch-presentation.js +55 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +5 -23
- package/dist/cli/express-presentation.d.ts +10 -0
- package/dist/cli/express-presentation.js +196 -0
- package/dist/cli/maintenance-commands.d.ts +4 -0
- package/dist/cli/maintenance-commands.js +29 -6
- package/dist/cli/observation-commands.d.ts +7 -5
- package/dist/cli/observation-commands.js +275 -262
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +43 -56
- package/dist/cli/registry.js +2 -1
- package/dist/cli/square-commands.d.ts +3 -9
- package/dist/cli/square-commands.js +134 -255
- package/dist/codex-queue.js +4 -1
- package/dist/decisions.d.ts +26 -74
- package/dist/decisions.js +190 -335
- package/dist/delivery-health.js +13 -22
- package/dist/delivery-operations.d.ts +0 -11
- package/dist/delivery-operations.js +44 -33
- package/dist/delivery.d.ts +33 -32
- package/dist/delivery.js +70 -81
- package/dist/harness-claude.js +1 -1
- package/dist/harness-codex.js +1 -1
- package/dist/harness-pi.js +1 -1
- package/dist/help.js +6 -1
- package/dist/inbox.d.ts +27 -0
- package/dist/inbox.js +55 -18
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -1
- package/dist/list.d.ts +7 -1
- package/dist/list.js +49 -122
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +7 -37
- package/dist/model.d.ts +4 -44
- package/dist/model.js +1 -3
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +13 -7
- package/dist/open-square.d.ts +16 -2
- package/dist/open-square.js +40 -1
- package/dist/perception-projection.d.ts +6 -15
- package/dist/perception-projection.js +6 -26
- package/dist/{opencode-delivery.d.ts → plugins/opencode/delivery.d.ts} +1 -1
- package/dist/{opencode-delivery.js → plugins/opencode/delivery.js} +18 -18
- package/dist/{opencode.d.ts → plugins/opencode/index.d.ts} +2 -2
- package/dist/{opencode.js → plugins/opencode/index.js} +9 -9
- package/dist/ports.d.ts +156 -11
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +35 -44
- package/dist/presentation.d.ts +12 -19
- package/dist/presentation.js +33 -54
- package/dist/routes.d.ts +7 -14
- package/dist/routes.js +33 -62
- package/dist/runtime.d.ts +12 -39
- package/dist/runtime.js +33 -164
- package/dist/square-actions.d.ts +11 -21
- package/dist/square-actions.js +171 -271
- package/dist/square-application.d.ts +31 -55
- package/dist/square-application.js +637 -141
- package/dist/square-core.d.ts +3 -94
- package/dist/square-core.js +8 -353
- package/dist/square-domain.d.ts +24 -0
- package/dist/square-domain.js +77 -0
- package/dist/square-facade.d.ts +360 -9
- package/dist/square-migration.d.ts +24 -0
- package/dist/square-migration.js +211 -0
- package/dist/square-projections.d.ts +2 -42
- package/dist/square-projections.js +1 -37
- package/dist/square-sqlite.d.ts +59 -0
- package/dist/square-sqlite.js +702 -0
- package/dist/stream.d.ts +3 -10
- package/dist/stream.js +81 -52
- package/dist/views.d.ts +27 -95
- package/dist/views.js +217 -87
- package/dist/wake-eligibility.d.ts +15 -5
- package/dist/wake-eligibility.js +32 -16
- package/dist/wakes.d.ts +3 -3
- package/dist/wakes.js +31 -27
- package/guides/application-facade-delivery-evidence.md +200 -0
- package/guides/application-facade-design.md +95 -0
- package/guides/change-waits.md +4 -4
- package/guides/migration.md +77 -0
- package/guides/pi-native-delivery.md +1 -1
- package/guides/primitive-catch-consolidation.md +85 -0
- package/guides/primitive-history-consolidation.md +76 -0
- package/guides/primitive-status-consolidation.md +87 -0
- package/guides/primitive-storage-consolidation.md +128 -0
- package/guides/row-runtime-delivery-evidence.md +160 -0
- package/guides/row-storage-design.md +567 -0
- package/package.json +5 -7
- package/{claude-plugin → plugins/claude}/.claude-plugin/plugin.json +1 -1
- package/{codex-plugin → plugins/codex}/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +149 -0
- package/plugins/opencode/index.ts +136 -0
- package/plugins/paseo/OVERVIEW.md +14 -0
- package/plugins/paseo/README.md +54 -0
- package/plugins/paseo/client/square-message.tsx +106 -0
- package/plugins/paseo/client/transform.ts +24 -0
- package/plugins/paseo/index.client.tsx +19 -0
- package/plugins/paseo/package.json +19 -0
- package/plugins/paseo/paseo-plugin.json +7 -0
- package/plugins/paseo/shared/message.ts +29 -0
- package/{extensions → plugins/pi}/square-pi.js +8 -5
- package/dist/activity.d.ts +0 -14
- package/dist/activity.js +0 -248
- package/dist/square-file-adapter.d.ts +0 -26
- package/dist/square-file-adapter.js +0 -85
- package/dist/square-state.d.ts +0 -6
- package/dist/square-state.js +0 -138
- package/dist/square-storage.d.ts +0 -14
- package/dist/square-storage.js +0 -188
- package/dist/square-wiring.d.ts +0 -33
- package/dist/square-wiring.js +0 -109
- package/dist/watch.d.ts +0 -8
- package/dist/watch.js +0 -252
- /package/{claude-plugin → plugins/claude}/hooks/hooks.json +0 -0
- /package/{claude-plugin → plugins/claude}/hooks/register.js +0 -0
- /package/{claude-plugin → plugins/claude}/skills/square/SKILL.md +0 -0
- /package/{claude-plugin → plugins/claude}/types.d.ts +0 -0
- /package/{codex-plugin → plugins/codex}/hooks/hooks.json +0 -0
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 {
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
29
|
-
|
|
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
|
|
32
|
-
return error instanceof Error && /
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
throw error;
|
|
184
|
+
database.close();
|
|
185
|
+
database = undefined;
|
|
186
|
+
throw storageError(error, location);
|
|
154
187
|
}
|
|
188
|
+
return database;
|
|
155
189
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
338
|
+
await handle.close();
|
|
185
339
|
}
|
|
186
340
|
}
|
|
187
|
-
/**
|
|
188
|
-
export async function createSquareFile(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
348
|
+
await initializeFile(temporary, state, signal);
|
|
349
|
+
abort(signal);
|
|
195
350
|
try {
|
|
196
|
-
await fs.promises.link(temporary,
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
throw signal.reason ?? closedError();
|
|
387
|
+
async function inspectFile(location, select, signal) {
|
|
388
|
+
const db = openExistingSquareDatabase(location);
|
|
228
389
|
try {
|
|
229
|
-
await
|
|
390
|
+
return await inSqliteTransaction(db, false, () => validateSqliteHeader(db, location), () => select(db), signal);
|
|
230
391
|
}
|
|
231
392
|
catch (error) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
393
|
+
throw storageError(error, location);
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
db.close();
|
|
235
397
|
}
|
|
236
398
|
}
|
|
237
|
-
/**
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
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: [],
|
|
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
|
}
|