hikoutei 0.6.0 → 0.7.0

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 (34) hide show
  1. package/README.ja.md +37 -18
  2. package/README.ko.md +36 -18
  3. package/README.md +73 -31
  4. package/dist/adapter/persistence/contracts/scalar.d.ts +62 -12
  5. package/dist/adapter/persistence/contracts/scalar.d.ts.map +1 -1
  6. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.d.ts +5 -1
  7. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.d.ts.map +1 -1
  8. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.js +83 -7
  9. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.js.map +1 -1
  10. package/dist/api/EntityManager.d.ts +6 -7
  11. package/dist/api/EntityManager.d.ts.map +1 -1
  12. package/dist/api/errors.d.ts +2 -0
  13. package/dist/api/errors.d.ts.map +1 -1
  14. package/dist/api/errors.js +2 -0
  15. package/dist/api/errors.js.map +1 -1
  16. package/dist/api/index.d.ts +1 -1
  17. package/dist/api/index.d.ts.map +1 -1
  18. package/dist/api/index.js.map +1 -1
  19. package/dist/api/internalEntityManager.d.ts.map +1 -1
  20. package/dist/api/internalEntityManager.js +54 -65
  21. package/dist/api/internalEntityManager.js.map +1 -1
  22. package/dist/api/query.d.ts +62 -0
  23. package/dist/api/query.d.ts.map +1 -0
  24. package/dist/api/query.js +18 -0
  25. package/dist/api/query.js.map +1 -0
  26. package/dist/api/queryNormalization.d.ts +7 -0
  27. package/dist/api/queryNormalization.d.ts.map +1 -0
  28. package/dist/api/queryNormalization.js +262 -0
  29. package/dist/api/queryNormalization.js.map +1 -0
  30. package/dist/internal/syncStatus.d.ts +111 -0
  31. package/dist/internal/syncStatus.d.ts.map +1 -0
  32. package/dist/internal/syncStatus.js +265 -0
  33. package/dist/internal/syncStatus.js.map +1 -0
  34. package/package.json +5 -1
@@ -0,0 +1,262 @@
1
+ import { HIKOUTEI_ERROR_CODES, HikouteiError } from "./errors.js";
2
+ import { HIKOUTEI_QUERY_OPERATORS, HIKOUTEI_SORT_DIRECTIONS, } from "./query.js";
3
+ /** Normalizes a public collection query after validating descriptor-owned fields and values. */
4
+ export function normalizeEntityQuery(descriptor, where, options) {
5
+ const normalizedOptions = normalizeFindOptions(options, true);
6
+ return {
7
+ tableName: descriptor.tableName,
8
+ primaryKeyColumn: descriptor.primaryKey,
9
+ predicate: normalizeFilter(descriptor, where),
10
+ orderBy: normalizeOrderBy(descriptor, normalizedOptions.orderBy, {
11
+ paged: normalizedOptions.limit !== undefined || normalizedOptions.offset !== undefined,
12
+ }),
13
+ ...(normalizedOptions.limit === undefined ? {} : { limit: normalizedOptions.limit }),
14
+ ...(normalizedOptions.offset === undefined ? {} : { offset: normalizedOptions.offset }),
15
+ };
16
+ }
17
+ /** Normalizes a public single-row query and applies an internal limit of one. */
18
+ export function normalizeEntityFindOneQuery(descriptor, where, options) {
19
+ const normalizedOptions = normalizeFindOptions(options, false);
20
+ return {
21
+ tableName: descriptor.tableName,
22
+ primaryKeyColumn: descriptor.primaryKey,
23
+ predicate: normalizeFilter(descriptor, where),
24
+ orderBy: normalizeOrderBy(descriptor, normalizedOptions.orderBy, { paged: false }),
25
+ limit: 1,
26
+ };
27
+ }
28
+ function normalizeFindOptions(input, allowPaging) {
29
+ if (input === undefined) {
30
+ return { orderBy: undefined, limit: undefined, offset: undefined };
31
+ }
32
+ if (!isPlainRecord(input)) {
33
+ throwInvalidQuery("query options must be an object.");
34
+ }
35
+ const allowed = allowPaging
36
+ ? new Set(["orderBy", "limit", "offset"])
37
+ : new Set(["orderBy"]);
38
+ for (const key of Object.keys(input)) {
39
+ if (!allowed.has(key)) {
40
+ throwInvalidQuery(`query option "${key}" is not supported here.`);
41
+ }
42
+ }
43
+ const limit = input.limit;
44
+ const offset = input.offset;
45
+ requirePageValue(limit, "limit");
46
+ requirePageValue(offset, "offset");
47
+ return {
48
+ orderBy: input.orderBy,
49
+ limit: limit,
50
+ offset: offset,
51
+ };
52
+ }
53
+ function normalizeFilter(descriptor, input) {
54
+ if (!isPlainRecord(input)) {
55
+ throwInvalidQuery("query filter must be an object.");
56
+ }
57
+ const predicates = [];
58
+ for (const [field, rawCondition] of Object.entries(input)) {
59
+ const property = requireProperty(descriptor, field);
60
+ if (isOperatorObject(rawCondition)) {
61
+ predicates.push(...normalizeOperatorObject(descriptor, property, rawCondition));
62
+ }
63
+ else {
64
+ const value = requirePropertyValue(descriptor, property, rawCondition, { allowNull: true });
65
+ predicates.push(value === null
66
+ ? { kind: "null", field, operator: "is_null" }
67
+ : { kind: "comparison", field, operator: "eq", value });
68
+ }
69
+ }
70
+ return combineAll(predicates);
71
+ }
72
+ function normalizeOperatorObject(descriptor, property, input) {
73
+ const entries = Object.entries(input);
74
+ if (entries.length === 0) {
75
+ throwInvalidQuery(`filter operator object for "${property.name}" must not be empty.`);
76
+ }
77
+ const predicates = [];
78
+ for (const [operator, operand] of entries) {
79
+ if (!isQueryOperator(operator)) {
80
+ throwInvalidQuery(`filter "${property.name}" has unknown operator "${operator}".`);
81
+ }
82
+ if (!isOperatorAllowed(property.type, operator)) {
83
+ throwInvalidQuery(`operator "${operator}" is not valid for ${property.type} field "${property.name}".`);
84
+ }
85
+ if (operator === "in" || operator === "nin") {
86
+ predicates.push(normalizeSetPredicate(descriptor, property, operator, operand));
87
+ continue;
88
+ }
89
+ if (operator === "like") {
90
+ const value = requirePropertyValue(descriptor, property, operand, { allowNull: false });
91
+ if (typeof value !== "string") {
92
+ throwInvalidQuery(`operator "like" requires a string pattern for "${property.name}".`);
93
+ }
94
+ predicates.push({ kind: "like", field: property.name, pattern: value });
95
+ continue;
96
+ }
97
+ const allowNull = operator === "eq" || operator === "ne";
98
+ const value = requirePropertyValue(descriptor, property, operand, { allowNull });
99
+ if (value === null) {
100
+ predicates.push({
101
+ kind: "null",
102
+ field: property.name,
103
+ operator: operator === "eq" ? "is_null" : "is_not_null",
104
+ });
105
+ }
106
+ else if (operator === "ne" && property.nullable) {
107
+ predicates.push({
108
+ kind: "any",
109
+ predicates: [
110
+ { kind: "null", field: property.name, operator: "is_null" },
111
+ { kind: "comparison", field: property.name, operator, value },
112
+ ],
113
+ });
114
+ }
115
+ else {
116
+ predicates.push({ kind: "comparison", field: property.name, operator, value });
117
+ }
118
+ }
119
+ return predicates;
120
+ }
121
+ function normalizeSetPredicate(descriptor, property, operator, operand) {
122
+ if (!Array.isArray(operand)) {
123
+ throwInvalidQuery(`operator "${operator}" requires an array for "${property.name}".`);
124
+ }
125
+ if (operand.length === 0)
126
+ return { kind: "constant", value: operator === "nin" };
127
+ const values = [];
128
+ let containsNull = false;
129
+ for (const rawValue of [...operand]) {
130
+ const value = requirePropertyValue(descriptor, property, rawValue, { allowNull: true });
131
+ if (value === null)
132
+ containsNull = true;
133
+ else
134
+ values.push(value);
135
+ }
136
+ const setPredicate = values.length === 0
137
+ ? undefined
138
+ : { kind: "set", field: property.name, operator, values: [...values] };
139
+ if (!containsNull) {
140
+ if (operator === "nin" && property.nullable && setPredicate !== undefined) {
141
+ return {
142
+ kind: "any",
143
+ predicates: [
144
+ { kind: "null", field: property.name, operator: "is_null" },
145
+ setPredicate,
146
+ ],
147
+ };
148
+ }
149
+ return setPredicate ?? { kind: "constant", value: operator === "nin" };
150
+ }
151
+ const nullPredicate = {
152
+ kind: "null",
153
+ field: property.name,
154
+ operator: operator === "in" ? "is_null" : "is_not_null",
155
+ };
156
+ if (setPredicate === undefined)
157
+ return nullPredicate;
158
+ return operator === "in"
159
+ ? { kind: "any", predicates: [nullPredicate, setPredicate] }
160
+ : { kind: "all", predicates: [nullPredicate, setPredicate] };
161
+ }
162
+ function normalizeOrderBy(descriptor, input, options) {
163
+ if (input === undefined) {
164
+ return options.paged
165
+ ? [{ field: descriptor.primaryKey, direction: HIKOUTEI_SORT_DIRECTIONS.ASC }]
166
+ : [];
167
+ }
168
+ if (!isPlainRecord(input))
169
+ throwInvalidQuery("orderBy must be an object.");
170
+ const entries = Object.entries(input);
171
+ if (entries.length === 0)
172
+ throwInvalidQuery("orderBy must not be empty.");
173
+ const orderBy = [];
174
+ for (const [field, direction] of entries) {
175
+ requireProperty(descriptor, field);
176
+ if (!isSortDirection(direction)) {
177
+ throwInvalidQuery(`orderBy direction for "${field}" must be "asc" or "desc".`);
178
+ }
179
+ orderBy.push({ field, direction });
180
+ }
181
+ if (!orderBy.some((order) => order.field === descriptor.primaryKey)) {
182
+ orderBy.push({ field: descriptor.primaryKey, direction: HIKOUTEI_SORT_DIRECTIONS.ASC });
183
+ }
184
+ return orderBy;
185
+ }
186
+ function combineAll(predicates) {
187
+ if (predicates.length === 0)
188
+ return { kind: "constant", value: true };
189
+ if (predicates.length === 1)
190
+ return predicates[0];
191
+ return { kind: "all", predicates: [...predicates] };
192
+ }
193
+ function requireProperty(descriptor, field) {
194
+ const property = descriptor.properties.find((candidate) => candidate.name === field);
195
+ if (property === undefined) {
196
+ throw new HikouteiError(HIKOUTEI_ERROR_CODES.INVALID_SCALAR_VALUE, `"${field}" is not a declared property of entity "${descriptor.name}".`);
197
+ }
198
+ return property;
199
+ }
200
+ function requirePropertyValue(descriptor, property, value, options) {
201
+ if (value === undefined) {
202
+ throw new HikouteiError(HIKOUTEI_ERROR_CODES.INVALID_SCALAR_VALUE, `filter "${property.name}" must not be undefined in entity "${descriptor.name}".`);
203
+ }
204
+ if (value === null) {
205
+ if (options.allowNull && property.nullable)
206
+ return null;
207
+ throw new HikouteiError(HIKOUTEI_ERROR_CODES.INVALID_SCALAR_VALUE, `filter "${property.name}" cannot be null in this query for entity "${descriptor.name}".`);
208
+ }
209
+ switch (property.type) {
210
+ case "date":
211
+ if (value instanceof Date && Number.isFinite(value.getTime()))
212
+ return new Date(value.getTime());
213
+ break;
214
+ case "string":
215
+ if (typeof value === "string")
216
+ return value;
217
+ break;
218
+ case "number":
219
+ if (typeof value === "number" && Number.isFinite(value))
220
+ return value;
221
+ break;
222
+ case "boolean":
223
+ if (typeof value === "boolean")
224
+ return value;
225
+ break;
226
+ }
227
+ throw new HikouteiError(HIKOUTEI_ERROR_CODES.INVALID_SCALAR_VALUE, `filter "${property.name}" expected ${property.type} in entity "${descriptor.name}".`);
228
+ }
229
+ function isOperatorAllowed(type, operator) {
230
+ if (operator === "eq" || operator === "ne" || operator === "in" || operator === "nin") {
231
+ return true;
232
+ }
233
+ if (operator === "like")
234
+ return type === "string";
235
+ return type === "string" || type === "number" || type === "date";
236
+ }
237
+ function isQueryOperator(value) {
238
+ return Object.values(HIKOUTEI_QUERY_OPERATORS).some((operator) => operator === value);
239
+ }
240
+ function isSortDirection(value) {
241
+ return value === HIKOUTEI_SORT_DIRECTIONS.ASC || value === HIKOUTEI_SORT_DIRECTIONS.DESC;
242
+ }
243
+ function isOperatorObject(value) {
244
+ return isPlainRecord(value) && !(value instanceof Date);
245
+ }
246
+ function isPlainRecord(value) {
247
+ if (value === null || typeof value !== "object" || Array.isArray(value))
248
+ return false;
249
+ const prototype = Object.getPrototypeOf(value);
250
+ return prototype === Object.prototype || prototype === null;
251
+ }
252
+ function requirePageValue(value, label) {
253
+ if (value === undefined)
254
+ return;
255
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
256
+ throw new HikouteiError(HIKOUTEI_ERROR_CODES.INVALID_SCALAR_VALUE, `${label} must be a non-negative safe integer.`);
257
+ }
258
+ }
259
+ function throwInvalidQuery(message) {
260
+ throw new HikouteiError(HIKOUTEI_ERROR_CODES.INVALID_QUERY, message);
261
+ }
262
+ //# sourceMappingURL=queryNormalization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryNormalization.js","sourceRoot":"","sources":["../../src/api/queryNormalization.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Read-only observability over the local SQLite authority for first-party
3
+ * tooling (the `hikoutei-mcp` server).
4
+ *
5
+ * This module is deliberately NOT part of the public application contract:
6
+ * it is exposed only through the unstable `hikoutei/internal/sync-status`
7
+ * subpath so the MCP layer can report outbox delivery state and unresolved
8
+ * conflicts without widening `src/index.ts`. It opens its own read-only
9
+ * `node:sqlite` connection and never mutates the database, so it coexists
10
+ * with a running sync worker (the WAL lets multiple readers share the file).
11
+ *
12
+ * Failure model: malformed arguments raise a structured
13
+ * {@link HikouteiSyncStatusError}; a database file that does not exist yet or
14
+ * a database without sync tables (local-only mode) is not an error — it maps
15
+ * to `{ mode: "local" }` and an empty conflict list respectively.
16
+ */
17
+ import { CONFLICT_STATUSES } from "../domain/model/constants.js";
18
+ /** Stable machine-readable codes raised by the internal status reader. */
19
+ export declare const HIKOUTEI_SYNC_STATUS_ERROR_CODES: {
20
+ /** The dbName argument is not a non-empty string. */
21
+ readonly INVALID_DB_NAME: "invalid_db_name";
22
+ /** The SQLite file exists but could not be opened read-only. */
23
+ readonly OPEN_FAILED: "open_failed";
24
+ /** A status query failed against an unexpected storage state. */
25
+ readonly READ_FAILED: "read_failed";
26
+ };
27
+ /** Closed set of internal status-reader error codes. */
28
+ export type HikouteiSyncStatusErrorCode = (typeof HIKOUTEI_SYNC_STATUS_ERROR_CODES)[keyof typeof HIKOUTEI_SYNC_STATUS_ERROR_CODES];
29
+ /** Structured error raised by this module; never part of the public API. */
30
+ export declare class HikouteiSyncStatusError extends Error {
31
+ readonly code: HikouteiSyncStatusErrorCode;
32
+ constructor(code: HikouteiSyncStatusErrorCode, message: string, cause?: unknown);
33
+ }
34
+ /** Plain scalar value extracted from a stored normalized cell. */
35
+ export type HikouteiStoredValue = string | number | boolean | null;
36
+ /** Unresolved conflict lifecycle states surfaced by `listHikouteiConflicts`. */
37
+ export type HikouteiOpenConflictStatus = (typeof CONFLICT_STATUSES)["OPEN"] | (typeof CONFLICT_STATUSES)["NEEDS_REBASE"];
38
+ /** Delivery-focused counts over the durable Sheet effect outbox. */
39
+ export interface HikouteiSyncEffectCounts {
40
+ readonly pending: number;
41
+ readonly processing: number;
42
+ readonly deliveryUncertain: number;
43
+ readonly failed: number;
44
+ }
45
+ /** Unresolved conflict counts. */
46
+ export interface HikouteiSyncConflictCounts {
47
+ readonly open: number;
48
+ readonly needsRebase: number;
49
+ }
50
+ /**
51
+ * Sync observability snapshot.
52
+ *
53
+ * - `{ mode: "local" }`: no sync tables exist (local-only runtime, or the
54
+ * database file has not been created yet).
55
+ * - `{ mode: "sync" }`: sync state exists; `spreadsheetId` is the bound
56
+ * spreadsheet's ID (full URLs are never returned) or `null` when the
57
+ * authority row has not been written yet.
58
+ */
59
+ export type HikouteiSyncStatus = {
60
+ readonly mode: "local";
61
+ } | {
62
+ readonly mode: "sync";
63
+ readonly spreadsheetId: string | null;
64
+ readonly effects: HikouteiSyncEffectCounts;
65
+ readonly conflicts: HikouteiSyncConflictCounts;
66
+ };
67
+ /** One unresolved human-edit conflict, values decoded to plain scalars. */
68
+ export interface HikouteiConflictSummary {
69
+ readonly conflictId: string;
70
+ readonly entityId: string;
71
+ readonly fieldName: string;
72
+ readonly userValue: HikouteiStoredValue;
73
+ readonly currentCanonicalValue: HikouteiStoredValue;
74
+ readonly userBaseRevision: number;
75
+ readonly currentCanonicalRevision: number;
76
+ readonly candidateEpoch: number;
77
+ readonly status: HikouteiOpenConflictStatus;
78
+ readonly updatedAt: number;
79
+ }
80
+ /** Options accepted by the read-only status queries. */
81
+ export interface HikouteiSyncStatusOptions {
82
+ /** SQLite database path, URI, or `:memory:` used by the runtime. */
83
+ readonly dbName: string;
84
+ }
85
+ /** Options accepted by `listHikouteiConflicts`. */
86
+ export interface HikouteiConflictListOptions extends HikouteiSyncStatusOptions {
87
+ /** Maximum number of conflicts to return; default 50, capped at 500. */
88
+ readonly limit?: number;
89
+ }
90
+ /** Default conflict page size when `limit` is omitted. */
91
+ export declare const DEFAULT_CONFLICT_LIST_LIMIT = 50;
92
+ /** Hard upper bound for one conflict page; protects caller context windows. */
93
+ export declare const MAX_CONFLICT_LIST_LIMIT = 500;
94
+ /**
95
+ * Reads a delivery/conflict snapshot from the SQLite authority.
96
+ *
97
+ * Returns `{ mode: "local" }` when the database file does not exist yet or
98
+ * was created by a local-only runtime (no sync tables). Never writes, never
99
+ * opens the network, and never returns spreadsheet URLs — only the ID.
100
+ */
101
+ export declare function readHikouteiSyncStatus(options: HikouteiSyncStatusOptions): Promise<HikouteiSyncStatus>;
102
+ /**
103
+ * Lists unresolved (OPEN / NEEDS_REBASE) conflicts, newest first.
104
+ *
105
+ * Stored cell values are decoded from normalized cells into plain scalars;
106
+ * a value that fails decoding falls back to its raw stored text so a corrupt
107
+ * row can still be inspected instead of failing the whole listing. Returns
108
+ * an empty list for local-only databases.
109
+ */
110
+ export declare function listHikouteiConflicts(options: HikouteiConflictListOptions): Promise<readonly HikouteiConflictSummary[]>;
111
+ //# sourceMappingURL=syncStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncStatus.d.ts","sourceRoot":"","sources":["../../src/internal/syncStatus.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Read-only observability over the local SQLite authority for first-party
3
+ * tooling (the `hikoutei-mcp` server).
4
+ *
5
+ * This module is deliberately NOT part of the public application contract:
6
+ * it is exposed only through the unstable `hikoutei/internal/sync-status`
7
+ * subpath so the MCP layer can report outbox delivery state and unresolved
8
+ * conflicts without widening `src/index.ts`. It opens its own read-only
9
+ * `node:sqlite` connection and never mutates the database, so it coexists
10
+ * with a running sync worker (the WAL lets multiple readers share the file).
11
+ *
12
+ * Failure model: malformed arguments raise a structured
13
+ * {@link HikouteiSyncStatusError}; a database file that does not exist yet or
14
+ * a database without sync tables (local-only mode) is not an error — it maps
15
+ * to `{ mode: "local" }` and an empty conflict list respectively.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import { EFFECT_STATUSES } from "@hikoutei/ikisaki";
19
+ import { CONFLICT_STATUSES } from "../domain/model/constants.js";
20
+ import { isNormalizedCell } from "../shared/encoding/index.js";
21
+ /** Stable machine-readable codes raised by the internal status reader. */
22
+ export const HIKOUTEI_SYNC_STATUS_ERROR_CODES = {
23
+ /** The dbName argument is not a non-empty string. */
24
+ INVALID_DB_NAME: "invalid_db_name",
25
+ /** The SQLite file exists but could not be opened read-only. */
26
+ OPEN_FAILED: "open_failed",
27
+ /** A status query failed against an unexpected storage state. */
28
+ READ_FAILED: "read_failed",
29
+ };
30
+ /** Structured error raised by this module; never part of the public API. */
31
+ export class HikouteiSyncStatusError extends Error {
32
+ code;
33
+ constructor(code, message, cause) {
34
+ super(message);
35
+ this.name = "HikouteiSyncStatusError";
36
+ this.code = code;
37
+ if (cause !== undefined) {
38
+ // `Error.cause` (ES2022) carries the underlying storage failure for
39
+ // diagnostics; optional parameter properties cannot use `override`.
40
+ this.cause = cause;
41
+ }
42
+ }
43
+ }
44
+ /** Default conflict page size when `limit` is omitted. */
45
+ export const DEFAULT_CONFLICT_LIST_LIMIT = 50;
46
+ /** Hard upper bound for one conflict page; protects caller context windows. */
47
+ export const MAX_CONFLICT_LIST_LIMIT = 500;
48
+ /** In-memory SQLite marker; read-only observability cannot cross connections. */
49
+ const MEMORY_DB_NAME = ":memory:";
50
+ /**
51
+ * Reads a delivery/conflict snapshot from the SQLite authority.
52
+ *
53
+ * Returns `{ mode: "local" }` when the database file does not exist yet or
54
+ * was created by a local-only runtime (no sync tables). Never writes, never
55
+ * opens the network, and never returns spreadsheet URLs — only the ID.
56
+ */
57
+ export async function readHikouteiSyncStatus(options) {
58
+ const dbName = requireValidDbName(options);
59
+ if (isMemoryDb(dbName) || !existsSync(dbName)) {
60
+ return { mode: "local" };
61
+ }
62
+ return withReadOnlyConnection(dbName, (database) => {
63
+ if (!syncTablesExist(database))
64
+ return { mode: "local" };
65
+ return {
66
+ mode: "sync",
67
+ spreadsheetId: readSpreadsheetId(database),
68
+ effects: readEffectCounts(database),
69
+ conflicts: readConflictCounts(database),
70
+ };
71
+ });
72
+ }
73
+ /**
74
+ * Lists unresolved (OPEN / NEEDS_REBASE) conflicts, newest first.
75
+ *
76
+ * Stored cell values are decoded from normalized cells into plain scalars;
77
+ * a value that fails decoding falls back to its raw stored text so a corrupt
78
+ * row can still be inspected instead of failing the whole listing. Returns
79
+ * an empty list for local-only databases.
80
+ */
81
+ export async function listHikouteiConflicts(options) {
82
+ const dbName = requireValidDbName(options);
83
+ // Input validation runs before the existence check so malformed arguments
84
+ // always reject, independent of database state.
85
+ const limit = resolveLimit(options.limit);
86
+ if (isMemoryDb(dbName) || !existsSync(dbName)) {
87
+ return [];
88
+ }
89
+ return withReadOnlyConnection(dbName, (database) => {
90
+ if (!tableExists(database, "sync_conflict"))
91
+ return [];
92
+ const rows = database
93
+ .prepare(`SELECT conflict_id, entity_id, field_name, user_value, current_canonical_value,
94
+ user_base_revision, current_canonical_revision, candidate_epoch, status, updated_at
95
+ FROM sync_conflict
96
+ WHERE status IN (?, ?)
97
+ ORDER BY updated_at DESC
98
+ LIMIT ?`)
99
+ .all(CONFLICT_STATUSES.OPEN, CONFLICT_STATUSES.NEEDS_REBASE, limit);
100
+ return rows.map(toConflictSummary);
101
+ });
102
+ }
103
+ /** Validates the options object and returns the non-empty dbName. */
104
+ function requireValidDbName(options) {
105
+ if (options === null || typeof options !== "object") {
106
+ throw new HikouteiSyncStatusError(HIKOUTEI_SYNC_STATUS_ERROR_CODES.INVALID_DB_NAME, "sync-status options must be an object with a dbName string.");
107
+ }
108
+ const dbName = options.dbName;
109
+ if (typeof dbName !== "string" || dbName.trim() === "") {
110
+ throw new HikouteiSyncStatusError(HIKOUTEI_SYNC_STATUS_ERROR_CODES.INVALID_DB_NAME, "sync-status dbName must be a non-empty string.");
111
+ }
112
+ return dbName;
113
+ }
114
+ /** Clamps and validates the conflict page size. */
115
+ function resolveLimit(limit) {
116
+ if (limit === undefined)
117
+ return DEFAULT_CONFLICT_LIST_LIMIT;
118
+ if (!Number.isInteger(limit) || limit < 1) {
119
+ throw new HikouteiSyncStatusError(HIKOUTEI_SYNC_STATUS_ERROR_CODES.READ_FAILED, "sync-status limit must be a positive integer.");
120
+ }
121
+ return Math.min(limit, MAX_CONFLICT_LIST_LIMIT);
122
+ }
123
+ /** True when the path names an in-memory database. */
124
+ function isMemoryDb(dbName) {
125
+ return dbName === MEMORY_DB_NAME || dbName.startsWith("file::memory:");
126
+ }
127
+ /**
128
+ * Loads the `node:sqlite` built-in without a module specifier.
129
+ *
130
+ * `process.getBuiltinModule` (Node 22.3+) keeps bundlers and test runners
131
+ * from trying to resolve `node:sqlite` as a package; the same approach is
132
+ * used by MikroORM's NodeSqliteDialect. Returns null when the runtime
133
+ * lacks the built-in.
134
+ */
135
+ function loadNodeSqlite() {
136
+ const candidate = process.getBuiltinModule;
137
+ if (typeof candidate !== "function")
138
+ return null;
139
+ const module = candidate.call(process, "node:sqlite");
140
+ return module ?? null;
141
+ }
142
+ /**
143
+ * Opens a read-only connection, runs the callback, and always closes it.
144
+ *
145
+ * A second connection to the same file is safe because it never writes;
146
+ * open failures are classified as `open_failed` instead of surfacing the
147
+ * raw node:sqlite error.
148
+ */
149
+ function withReadOnlyConnection(dbName, read) {
150
+ const nodeSqlite = loadNodeSqlite();
151
+ if (nodeSqlite === null) {
152
+ throw new HikouteiSyncStatusError(HIKOUTEI_SYNC_STATUS_ERROR_CODES.OPEN_FAILED, "this Node runtime does not provide the node:sqlite built-in (Node 22.5+ required)");
153
+ }
154
+ let database;
155
+ try {
156
+ database = new nodeSqlite.DatabaseSync(dbName, { readOnly: true });
157
+ }
158
+ catch (error) {
159
+ throw new HikouteiSyncStatusError(HIKOUTEI_SYNC_STATUS_ERROR_CODES.OPEN_FAILED, `could not open ${dbName} read-only: ${messageOf(error)}`, error);
160
+ }
161
+ try {
162
+ return read(database);
163
+ }
164
+ catch (error) {
165
+ if (error instanceof HikouteiSyncStatusError)
166
+ throw error;
167
+ throw new HikouteiSyncStatusError(HIKOUTEI_SYNC_STATUS_ERROR_CODES.READ_FAILED, `status query failed on ${dbName}: ${messageOf(error)}`, error);
168
+ }
169
+ finally {
170
+ database.close();
171
+ }
172
+ }
173
+ /** True when the durable sync tables are present in this database. */
174
+ function syncTablesExist(database) {
175
+ return tableExists(database, "sheet_effect_outbox");
176
+ }
177
+ /** Checks sqlite_master for one table name. */
178
+ function tableExists(database, tableName) {
179
+ const row = database
180
+ .prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?")
181
+ .get(tableName);
182
+ return row !== undefined;
183
+ }
184
+ /** Reads the bound spreadsheet ID, or null when no registry row exists yet. */
185
+ function readSpreadsheetId(database) {
186
+ // Provisioning registers physical sheets eagerly, so the registry knows
187
+ // the bound spreadsheet even before the writer claims an authority row.
188
+ const registryRow = database
189
+ .prepare("SELECT spreadsheet_id FROM physical_sheet_registry LIMIT 1")
190
+ .get();
191
+ if (registryRow !== undefined)
192
+ return registryRow.spreadsheet_id;
193
+ const authorityRow = database
194
+ .prepare("SELECT spreadsheet_id FROM spreadsheet_authority LIMIT 1")
195
+ .get();
196
+ return authorityRow === undefined ? null : authorityRow.spreadsheet_id;
197
+ }
198
+ /** Counts outbox effects by the delivery-relevant statuses. */
199
+ function readEffectCounts(database) {
200
+ const rows = database
201
+ .prepare("SELECT status, COUNT(*) AS count FROM sheet_effect_outbox GROUP BY status")
202
+ .all();
203
+ const byStatus = new Map(rows.map((row) => [row.status, row.count]));
204
+ return {
205
+ pending: byStatus.get(EFFECT_STATUSES.PENDING) ?? 0,
206
+ processing: byStatus.get(EFFECT_STATUSES.PROCESSING) ?? 0,
207
+ deliveryUncertain: byStatus.get(EFFECT_STATUSES.DELIVERY_UNCERTAIN) ?? 0,
208
+ failed: byStatus.get(EFFECT_STATUSES.FAILED) ?? 0,
209
+ };
210
+ }
211
+ /** Counts unresolved conflicts by status. */
212
+ function readConflictCounts(database) {
213
+ const rows = database
214
+ .prepare("SELECT status, COUNT(*) AS count FROM sync_conflict WHERE status IN (?, ?) GROUP BY status")
215
+ .all(CONFLICT_STATUSES.OPEN, CONFLICT_STATUSES.NEEDS_REBASE);
216
+ const byStatus = new Map(rows.map((row) => [row.status, row.count]));
217
+ return {
218
+ open: byStatus.get(CONFLICT_STATUSES.OPEN) ?? 0,
219
+ needsRebase: byStatus.get(CONFLICT_STATUSES.NEEDS_REBASE) ?? 0,
220
+ };
221
+ }
222
+ /** Maps one raw conflict row to the summary contract, decoding stored cells. */
223
+ function toConflictSummary(row) {
224
+ const status = row.status === CONFLICT_STATUSES.NEEDS_REBASE
225
+ ? CONFLICT_STATUSES.NEEDS_REBASE
226
+ : CONFLICT_STATUSES.OPEN;
227
+ return {
228
+ conflictId: String(row.conflict_id),
229
+ entityId: String(row.entity_id),
230
+ fieldName: String(row.field_name),
231
+ userValue: decodeStoredValue(row.user_value),
232
+ currentCanonicalValue: decodeStoredValue(row.current_canonical_value),
233
+ userBaseRevision: Number(row.user_base_revision),
234
+ currentCanonicalRevision: Number(row.current_canonical_revision),
235
+ candidateEpoch: Number(row.candidate_epoch),
236
+ status,
237
+ updatedAt: Number(row.updated_at),
238
+ };
239
+ }
240
+ /**
241
+ * Decodes one stored normalized-cell JSON value into a plain scalar.
242
+ *
243
+ * Falls back to the raw stored text when the payload is not valid JSON or
244
+ * not a normalized cell, so a partially corrupt row remains inspectable.
245
+ */
246
+ function decodeStoredValue(raw) {
247
+ if (raw === null || raw === undefined)
248
+ return null;
249
+ const text = String(raw);
250
+ try {
251
+ const parsed = JSON.parse(text);
252
+ if (isNormalizedCell(parsed)) {
253
+ return parsed === null ? null : parsed.value;
254
+ }
255
+ }
256
+ catch {
257
+ // fall through to the raw text
258
+ }
259
+ return text;
260
+ }
261
+ /** Best-effort human-readable message from an unknown error. */
262
+ function messageOf(error) {
263
+ return error instanceof Error ? error.message : String(error);
264
+ }
265
+ //# sourceMappingURL=syncStatus.js.map