@ssobig/writer-cli 0.2.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.
- package/README.md +35 -0
- package/asset-repository.js +278 -0
- package/config.js +14 -0
- package/package.json +28 -0
- package/project-runtime.js +102 -0
- package/storage-path.js +110 -0
- package/templates/mystery-v1/authoring-view-preference.js +34 -0
- package/templates/mystery-v1/character-perspective-preview.js +61 -0
- package/templates/mystery-v1/component-asset-operations.js +103 -0
- package/templates/mystery-v1/component-autosave.js +121 -0
- package/templates/mystery-v1/component-catalog-contract.js +340 -0
- package/templates/mystery-v1/component-checkpoint-history.js +145 -0
- package/templates/mystery-v1/component-contract.js +90 -0
- package/templates/mystery-v1/component-draft-operations.js +313 -0
- package/templates/mystery-v1/component-field-contracts.js +595 -0
- package/templates/mystery-v1/component-id-policy.js +64 -0
- package/templates/mystery-v1/component-manager.js +396 -0
- package/templates/mystery-v1/component-navigation-counts.js +64 -0
- package/templates/mystery-v1/component-registry.js +205 -0
- package/templates/mystery-v1/component-renderers.js +139 -0
- package/templates/mystery-v1/component-storage-contract.js +237 -0
- package/templates/mystery-v1/external-update-coordinator.js +91 -0
- package/templates/mystery-v1/output-clue-card-layout.js +46 -0
- package/templates/mystery-v1/page-header.js +26 -0
- package/templates/mystery-v1/render-ui-state.js +76 -0
- package/templates/mystery-v1/runtime-snapshot-reconciler.js +40 -0
- package/templates/mystery-v1/tab-bar.js +87 -0
- package/templates/mystery-v1/view-component-contract.js +152 -0
- package/templates/mystery-v1/view-component-registry.js +44 -0
- package/templates/mystery-v1/view-component-runtime.js +95 -0
- package/tools/writer-cli/bin/ssobig-writer-daemon.cjs +34 -0
- package/tools/writer-cli/bin/ssobig-writer.cjs +12 -0
- package/tools/writer-cli/package-lock.json +121 -0
- package/tools/writer-cli/package.json +22 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +38 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/agents/openai.yaml +4 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +10 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +7 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +5 -0
- package/tools/writer-cli/src/agent-paths.cjs +114 -0
- package/tools/writer-cli/src/agent-service.cjs +496 -0
- package/tools/writer-cli/src/asset-policy.cjs +113 -0
- package/tools/writer-cli/src/auth.cjs +655 -0
- package/tools/writer-cli/src/checkpoint-diff.cjs +128 -0
- package/tools/writer-cli/src/command-registry.cjs +152 -0
- package/tools/writer-cli/src/commands.cjs +841 -0
- package/tools/writer-cli/src/corpus.cjs +83 -0
- package/tools/writer-cli/src/daemon-app.cjs +106 -0
- package/tools/writer-cli/src/daemon-client.cjs +187 -0
- package/tools/writer-cli/src/daemon-protocol.cjs +184 -0
- package/tools/writer-cli/src/daemon-runner.cjs +97 -0
- package/tools/writer-cli/src/daemon-server.cjs +378 -0
- package/tools/writer-cli/src/diagnostics.cjs +235 -0
- package/tools/writer-cli/src/domain.cjs +731 -0
- package/tools/writer-cli/src/errors.cjs +47 -0
- package/tools/writer-cli/src/gateway.cjs +357 -0
- package/tools/writer-cli/src/investigation-board-layout.cjs +328 -0
- package/tools/writer-cli/src/json-patch.cjs +98 -0
- package/tools/writer-cli/src/json.cjs +26 -0
- package/tools/writer-cli/src/local-index-cache.cjs +139 -0
- package/tools/writer-cli/src/local-index-lookup.cjs +98 -0
- package/tools/writer-cli/src/local-index-query.cjs +304 -0
- package/tools/writer-cli/src/local-index-snapshot.cjs +235 -0
- package/tools/writer-cli/src/local-index-storage.cjs +284 -0
- package/tools/writer-cli/src/local-index.cjs +199 -0
- package/tools/writer-cli/src/mutations.cjs +722 -0
- package/tools/writer-cli/src/platform-runner.cjs +55 -0
- package/tools/writer-cli/src/project-import.cjs +485 -0
- package/tools/writer-cli/src/skill-manager.cjs +255 -0
- package/tools/writer-cli/src/source-fingerprint.cjs +90 -0
- package/tools/writer-cli/src/update-gate.cjs +102 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const { canonicalJson } = require("./json.cjs");
|
|
5
|
+
const { cliError } = require("./errors.cjs");
|
|
6
|
+
const {
|
|
7
|
+
INDEX_SCHEMA_VERSION,
|
|
8
|
+
SERVER_SNAPSHOT_SOURCE,
|
|
9
|
+
trigramValues
|
|
10
|
+
} = require("./local-index-snapshot.cjs");
|
|
11
|
+
const { appendSqlIdentityFilters } = require("./local-index-query.cjs");
|
|
12
|
+
const { resolveStoragePaths, createJsonBackend } = require("./local-index-cache.cjs");
|
|
13
|
+
|
|
14
|
+
function metadataToRows(metadata) {
|
|
15
|
+
return Object.entries({
|
|
16
|
+
schema_version: metadata.schemaVersion,
|
|
17
|
+
source: metadata.source,
|
|
18
|
+
actor_json: JSON.stringify(metadata.actor),
|
|
19
|
+
synced_at: metadata.syncedAt,
|
|
20
|
+
last_verified_auth_at: metadata.lastVerifiedAuthAt || "",
|
|
21
|
+
snapshot_hash: metadata.snapshotHash,
|
|
22
|
+
project_version_count: String(metadata.projectVersionCount),
|
|
23
|
+
component_count: String(metadata.componentCount),
|
|
24
|
+
scalar_count: String(metadata.scalarCount)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rowsToMetadata(rows) {
|
|
29
|
+
const values = Object.fromEntries(rows.map(row => [row.key, row.value]));
|
|
30
|
+
if (!values.schema_version) return null;
|
|
31
|
+
// 로컬 인덱스는 언제든 재구축 가능한 파생물이라 형식이 어긋나면 막지 않고 미동기화로 취급한다.
|
|
32
|
+
if (values.schema_version !== INDEX_SCHEMA_VERSION || values.source !== SERVER_SNAPSHOT_SOURCE) return null;
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: values.schema_version,
|
|
35
|
+
source: values.source,
|
|
36
|
+
actor: JSON.parse(values.actor_json || "null"),
|
|
37
|
+
syncedAt: values.synced_at,
|
|
38
|
+
lastVerifiedAuthAt: values.last_verified_auth_at || null,
|
|
39
|
+
snapshotHash: values.snapshot_hash,
|
|
40
|
+
projectVersionCount: Number(values.project_version_count || 0),
|
|
41
|
+
componentCount: Number(values.component_count || 0),
|
|
42
|
+
scalarCount: Number(values.scalar_count || 0)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 현재 DDL이 기대하는 테이블 형태. 디스크의 실제 형태와 비교해 스키마 드리프트를 판정한다.
|
|
47
|
+
const EXPECTED_TABLE_COLUMNS = Object.freeze({
|
|
48
|
+
metadata: Object.freeze(["key", "value"]),
|
|
49
|
+
components: Object.freeze([
|
|
50
|
+
"component_checksum", "component_set_revision", "data_hash", "data_json", "instance_id",
|
|
51
|
+
"instance_revision", "project_id", "project_slug", "template_id", "version_number"
|
|
52
|
+
]),
|
|
53
|
+
scalar_nodes: Object.freeze([
|
|
54
|
+
"component_checksum", "component_set_revision", "data_hash", "field_name", "folded_text", "id",
|
|
55
|
+
"instance_id", "instance_revision", "match_id", "pointer", "project_id", "project_slug",
|
|
56
|
+
"search_text", "template_id", "value_json", "value_text", "value_type", "version_number"
|
|
57
|
+
]),
|
|
58
|
+
scalar_trigrams: Object.freeze(["node_id", "trigram"])
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function tableColumns(database, table) {
|
|
62
|
+
try {
|
|
63
|
+
return database.prepare(`PRAGMA table_info(${table})`).all().map(row => String(row.name)).sort();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
void error;
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hasSchemaDrift(database) {
|
|
71
|
+
for (const [table, expected] of Object.entries(EXPECTED_TABLE_COLUMNS)) {
|
|
72
|
+
const actual = tableColumns(database, table);
|
|
73
|
+
if (actual.length === 0) continue;
|
|
74
|
+
if (actual.length !== expected.length || actual.some((name, position) => name !== expected[position])) return true;
|
|
75
|
+
}
|
|
76
|
+
if (tableColumns(database, "metadata").length === 0) return false;
|
|
77
|
+
try {
|
|
78
|
+
const values = Object.fromEntries(database
|
|
79
|
+
.prepare("SELECT key, value FROM metadata WHERE key IN ('schema_version', 'source')")
|
|
80
|
+
.all()
|
|
81
|
+
.map(row => [row.key, row.value]));
|
|
82
|
+
if (!values.schema_version) return false;
|
|
83
|
+
return values.schema_version !== INDEX_SCHEMA_VERSION || values.source !== SERVER_SNAPSHOT_SOURCE;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
void error;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function dropIndexTables(database) {
|
|
91
|
+
try { database.exec("DROP TABLE IF EXISTS scalar_fts;"); } catch (ftsError) { void ftsError; }
|
|
92
|
+
database.exec(`
|
|
93
|
+
DROP TABLE IF EXISTS scalar_trigrams;
|
|
94
|
+
DROP TABLE IF EXISTS scalar_nodes;
|
|
95
|
+
DROP TABLE IF EXISTS components;
|
|
96
|
+
DROP TABLE IF EXISTS metadata;
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createSqliteBackend(options) {
|
|
101
|
+
let sqlite;
|
|
102
|
+
try {
|
|
103
|
+
sqlite = require("node:sqlite");
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw cliError("E_LOCAL_INDEX_SQLITE_UNAVAILABLE", "이 Node.js 런타임에서 node:sqlite를 사용할 수 없습니다.", null, error);
|
|
106
|
+
}
|
|
107
|
+
const storage = resolveStoragePaths(options, "sqlite");
|
|
108
|
+
let database;
|
|
109
|
+
let ftsEnabled = false;
|
|
110
|
+
try {
|
|
111
|
+
database = new sqlite.DatabaseSync(storage.filePath);
|
|
112
|
+
fs.chmodSync(storage.filePath, 0o600);
|
|
113
|
+
database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; PRAGMA secure_delete = ON; PRAGMA temp_store = MEMORY;");
|
|
114
|
+
// 구버전 CLI가 만든 테이블은 CREATE TABLE IF NOT EXISTS로 갱신되지 않으므로 드리프트가 있으면 통째로 재생성한다.
|
|
115
|
+
if (hasSchemaDrift(database)) dropIndexTables(database);
|
|
116
|
+
database.exec(`
|
|
117
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
118
|
+
key TEXT PRIMARY KEY,
|
|
119
|
+
value TEXT NOT NULL
|
|
120
|
+
) WITHOUT ROWID;
|
|
121
|
+
CREATE TABLE IF NOT EXISTS components (
|
|
122
|
+
project_id TEXT NOT NULL,
|
|
123
|
+
project_slug TEXT NOT NULL,
|
|
124
|
+
version_number INTEGER NOT NULL,
|
|
125
|
+
component_set_revision INTEGER NOT NULL,
|
|
126
|
+
component_checksum TEXT NOT NULL,
|
|
127
|
+
instance_id TEXT NOT NULL,
|
|
128
|
+
template_id TEXT NOT NULL,
|
|
129
|
+
instance_revision INTEGER NOT NULL,
|
|
130
|
+
data_hash TEXT NOT NULL,
|
|
131
|
+
data_json TEXT NOT NULL,
|
|
132
|
+
PRIMARY KEY (project_id, version_number, instance_id)
|
|
133
|
+
) WITHOUT ROWID;
|
|
134
|
+
CREATE TABLE IF NOT EXISTS scalar_nodes (
|
|
135
|
+
id INTEGER PRIMARY KEY,
|
|
136
|
+
match_id TEXT NOT NULL UNIQUE,
|
|
137
|
+
project_id TEXT NOT NULL,
|
|
138
|
+
project_slug TEXT NOT NULL,
|
|
139
|
+
version_number INTEGER NOT NULL,
|
|
140
|
+
component_set_revision INTEGER NOT NULL,
|
|
141
|
+
component_checksum TEXT NOT NULL,
|
|
142
|
+
instance_id TEXT NOT NULL,
|
|
143
|
+
template_id TEXT NOT NULL,
|
|
144
|
+
instance_revision INTEGER NOT NULL,
|
|
145
|
+
data_hash TEXT NOT NULL,
|
|
146
|
+
pointer TEXT NOT NULL,
|
|
147
|
+
field_name TEXT NOT NULL,
|
|
148
|
+
value_type TEXT NOT NULL,
|
|
149
|
+
value_json TEXT NOT NULL,
|
|
150
|
+
value_text TEXT NOT NULL,
|
|
151
|
+
search_text TEXT NOT NULL,
|
|
152
|
+
folded_text TEXT NOT NULL,
|
|
153
|
+
FOREIGN KEY (project_id, version_number, instance_id)
|
|
154
|
+
REFERENCES components(project_id, version_number, instance_id) ON DELETE CASCADE
|
|
155
|
+
);
|
|
156
|
+
CREATE TABLE IF NOT EXISTS scalar_trigrams (
|
|
157
|
+
trigram TEXT NOT NULL,
|
|
158
|
+
node_id INTEGER NOT NULL REFERENCES scalar_nodes(id) ON DELETE CASCADE,
|
|
159
|
+
PRIMARY KEY (trigram, node_id)
|
|
160
|
+
) WITHOUT ROWID;
|
|
161
|
+
CREATE INDEX IF NOT EXISTS scalar_nodes_project ON scalar_nodes(project_id, version_number, instance_id);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS scalar_nodes_slug ON scalar_nodes(project_slug, version_number, instance_id);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS scalar_nodes_template ON scalar_nodes(template_id);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS scalar_nodes_pointer ON scalar_nodes(pointer);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS scalar_nodes_field ON scalar_nodes(field_name);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS scalar_nodes_exact ON scalar_nodes(value_type, value_json);
|
|
167
|
+
`);
|
|
168
|
+
try {
|
|
169
|
+
database.exec("CREATE VIRTUAL TABLE IF NOT EXISTS scalar_fts USING fts5(folded_text, tokenize='trigram');");
|
|
170
|
+
database.exec("BEGIN IMMEDIATE; DELETE FROM scalar_fts; INSERT INTO scalar_fts(rowid, folded_text) SELECT id, folded_text FROM scalar_nodes; COMMIT;");
|
|
171
|
+
ftsEnabled = true;
|
|
172
|
+
} catch (ftsError) {
|
|
173
|
+
try { database.exec("ROLLBACK"); } catch (rollbackError) { void rollbackError; }
|
|
174
|
+
ftsEnabled = false;
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
try { database?.close(); } catch (closeError) { void closeError; }
|
|
178
|
+
throw cliError("E_LOCAL_INDEX_SQLITE_UNAVAILABLE", "SQLite 로컬 인덱스를 열 수 없습니다.", null, error);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const statements = {
|
|
182
|
+
insertMetadata: database.prepare("INSERT INTO metadata(key, value) VALUES(?, ?)"),
|
|
183
|
+
insertComponent: database.prepare(`INSERT INTO components(
|
|
184
|
+
project_id, project_slug, version_number, component_set_revision, component_checksum,
|
|
185
|
+
instance_id, template_id, instance_revision, data_hash, data_json
|
|
186
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
187
|
+
insertNode: database.prepare(`INSERT INTO scalar_nodes(
|
|
188
|
+
match_id, project_id, project_slug, version_number, component_set_revision, component_checksum,
|
|
189
|
+
instance_id, template_id, instance_revision, data_hash,
|
|
190
|
+
pointer, field_name, value_type, value_json, value_text, search_text, folded_text
|
|
191
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
192
|
+
insertTrigram: database.prepare("INSERT OR IGNORE INTO scalar_trigrams(trigram, node_id) VALUES(?, ?)"),
|
|
193
|
+
insertFts: ftsEnabled ? database.prepare("INSERT INTO scalar_fts(rowid, folded_text) VALUES(?, ?)") : null,
|
|
194
|
+
metadata: database.prepare("SELECT key, value FROM metadata ORDER BY key"),
|
|
195
|
+
counts: database.prepare("SELECT (SELECT COUNT(*) FROM components) AS components, (SELECT COUNT(*) FROM scalar_nodes) AS scalars")
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
function replaceState(state) {
|
|
199
|
+
database.exec("BEGIN IMMEDIATE");
|
|
200
|
+
try {
|
|
201
|
+
if (ftsEnabled) database.exec("DELETE FROM scalar_fts;");
|
|
202
|
+
database.exec("DELETE FROM scalar_trigrams; DELETE FROM scalar_nodes; DELETE FROM components; DELETE FROM metadata;");
|
|
203
|
+
for (const [key, value] of metadataToRows(state.metadata)) statements.insertMetadata.run(key, value);
|
|
204
|
+
for (const component of state.components) {
|
|
205
|
+
statements.insertComponent.run(
|
|
206
|
+
component.projectId, component.projectSlug, component.versionNumber,
|
|
207
|
+
component.componentSetRevision, component.componentChecksum, component.instanceId,
|
|
208
|
+
component.templateId, component.instanceRevision, component.dataHash, canonicalJson(component.data)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
for (const node of state.nodes) {
|
|
212
|
+
const result = statements.insertNode.run(
|
|
213
|
+
node.matchId, node.projectId, node.projectSlug, node.versionNumber,
|
|
214
|
+
node.componentSetRevision, node.componentChecksum, node.instanceId, node.templateId,
|
|
215
|
+
node.instanceRevision, node.dataHash,
|
|
216
|
+
node.pointer, node.field, node.valueType, node.valueJson, node.valueText,
|
|
217
|
+
node.searchText, node.foldedText
|
|
218
|
+
);
|
|
219
|
+
const nodeId = Number(result.lastInsertRowid);
|
|
220
|
+
statements.insertFts?.run(nodeId, node.foldedText);
|
|
221
|
+
for (const trigram of trigramValues(node.foldedText)) statements.insertTrigram.run(trigram, nodeId);
|
|
222
|
+
}
|
|
223
|
+
database.exec("COMMIT");
|
|
224
|
+
fs.chmodSync(storage.filePath, 0o600);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
try { database.exec("ROLLBACK"); } catch (rollbackError) { void rollbackError; }
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function metadata() {
|
|
232
|
+
return rowsToMetadata(statements.metadata.all());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function queryRows(sql, parameters) {
|
|
236
|
+
return database.prepare(sql).all(...parameters);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getMatch(matchId) {
|
|
240
|
+
return database.prepare("SELECT * FROM scalar_nodes WHERE match_id = ?").get(String(matchId || "")) || null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getComponent(filters) {
|
|
244
|
+
const where = [];
|
|
245
|
+
const parameters = [];
|
|
246
|
+
appendSqlIdentityFilters(where, parameters, filters, "components");
|
|
247
|
+
return queryRows(`SELECT * FROM components${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY project_slug, version_number, instance_id`, parameters);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function counts() {
|
|
251
|
+
const row = statements.counts.get();
|
|
252
|
+
return { components: Number(row?.components || 0), scalars: Number(row?.scalars || 0) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function close() {
|
|
256
|
+
database.close();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
kind: "sqlite",
|
|
261
|
+
searchEngine: ftsEnabled ? "sqlite-fts5-trigram" : "sqlite-trigram",
|
|
262
|
+
ftsEnabled,
|
|
263
|
+
...storage,
|
|
264
|
+
replaceState,
|
|
265
|
+
metadata,
|
|
266
|
+
queryRows,
|
|
267
|
+
getMatch,
|
|
268
|
+
getComponent,
|
|
269
|
+
counts,
|
|
270
|
+
close
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createStorageBackend(options, requestedBackend) {
|
|
275
|
+
if (requestedBackend === "json") return createJsonBackend(options);
|
|
276
|
+
try {
|
|
277
|
+
return createSqliteBackend(options);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (requestedBackend === "sqlite" || error?.code !== "E_LOCAL_INDEX_SQLITE_UNAVAILABLE") throw error;
|
|
280
|
+
return createJsonBackend({ ...options, filePath: options.filePath && String(options.filePath).replace(/\.sqlite3?$/u, ".json") });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = Object.freeze({ createStorageBackend });
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { cliError } = require("./errors.cjs");
|
|
4
|
+
const {
|
|
5
|
+
INDEX_SCHEMA_VERSION,
|
|
6
|
+
SERVER_SNAPSHOT_SOURCE,
|
|
7
|
+
cloneJson,
|
|
8
|
+
flattenJsonScalars,
|
|
9
|
+
escapeJsonPointerToken,
|
|
10
|
+
unescapeJsonPointerToken,
|
|
11
|
+
normalizeSnapshot
|
|
12
|
+
} = require("./local-index-snapshot.cjs");
|
|
13
|
+
const { createStorageBackend } = require("./local-index-storage.cjs");
|
|
14
|
+
const { purgeBackendFiles } = require("./local-index-cache.cjs");
|
|
15
|
+
const {
|
|
16
|
+
createQueryLimits,
|
|
17
|
+
normalizeFilters,
|
|
18
|
+
appendSqlNodeFilters,
|
|
19
|
+
identityMatches,
|
|
20
|
+
normalizeSearch,
|
|
21
|
+
appendSqlSearch,
|
|
22
|
+
nodeMatchesSearch
|
|
23
|
+
} = require("./local-index-query.cjs");
|
|
24
|
+
const { rowToNode, freshnessFor, publicMatch, componentFromRow } = require("./local-index-lookup.cjs");
|
|
25
|
+
|
|
26
|
+
function createLocalIndex(options = {}) {
|
|
27
|
+
const requestedBackend = String(options.backend || "auto");
|
|
28
|
+
if (!["auto", "sqlite", "json"].includes(requestedBackend)) {
|
|
29
|
+
throw cliError("E_LOCAL_INDEX_CONFIGURATION", `지원하지 않는 local index backend입니다: ${requestedBackend}`);
|
|
30
|
+
}
|
|
31
|
+
const backend = createStorageBackend(options, requestedBackend);
|
|
32
|
+
const limits = createQueryLimits(options, backend.ftsEnabled);
|
|
33
|
+
const nowProvider = typeof options.now === "function" ? options.now : Date.now;
|
|
34
|
+
let closed = false;
|
|
35
|
+
|
|
36
|
+
function assertOpen() {
|
|
37
|
+
if (closed) throw cliError("E_LOCAL_INDEX_CLOSED", "닫힌 로컬 인덱스입니다.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function responseBase(metadata, freshnessOptions = {}) {
|
|
41
|
+
return {
|
|
42
|
+
authoritative: false,
|
|
43
|
+
source: "local-disposable-read-index",
|
|
44
|
+
requiresServerReadBeforeWrite: true,
|
|
45
|
+
backend: backend.kind,
|
|
46
|
+
searchEngine: backend.searchEngine,
|
|
47
|
+
freshness: freshnessFor(metadata, freshnessOptions, nowProvider)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function replaceSnapshot(snapshot) {
|
|
52
|
+
assertOpen();
|
|
53
|
+
const state = normalizeSnapshot(snapshot);
|
|
54
|
+
backend.replaceState(state);
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
...responseBase(state.metadata),
|
|
57
|
+
snapshotHash: state.metadata.snapshotHash,
|
|
58
|
+
projectVersionCount: state.metadata.projectVersionCount,
|
|
59
|
+
componentCount: state.metadata.componentCount,
|
|
60
|
+
scalarCount: state.metadata.scalarCount
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function search(request = {}) {
|
|
65
|
+
assertOpen();
|
|
66
|
+
const metadata = backend.metadata();
|
|
67
|
+
if (!metadata) {
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
...responseBase(null, request),
|
|
70
|
+
total: 0,
|
|
71
|
+
returned: 0,
|
|
72
|
+
truncated: false,
|
|
73
|
+
matches: Object.freeze([])
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const normalized = normalizeSearch(request, limits);
|
|
77
|
+
const candidateLimit = normalized.mode === "regex" ? limits.maxRegexScanRows : limits.maxCandidateRows;
|
|
78
|
+
let rawRows;
|
|
79
|
+
if (backend.kind === "sqlite") {
|
|
80
|
+
const where = [];
|
|
81
|
+
const parameters = [];
|
|
82
|
+
appendSqlNodeFilters(where, parameters, normalized.filters);
|
|
83
|
+
appendSqlSearch(where, parameters, normalized, limits);
|
|
84
|
+
rawRows = backend.queryRows(`SELECT scalar_nodes.* FROM scalar_nodes${where.length ? ` WHERE ${where.join(" AND ")}` : ""}
|
|
85
|
+
ORDER BY project_slug, version_number, instance_id, pointer LIMIT ?`, [...parameters, candidateLimit + 1]);
|
|
86
|
+
} else {
|
|
87
|
+
rawRows = backend.queryRows().map(rowToNode).filter(node => identityMatches(node, normalized.filters));
|
|
88
|
+
if (normalized.mode !== "regex") rawRows = rawRows.filter(node => nodeMatchesSearch(node, normalized));
|
|
89
|
+
}
|
|
90
|
+
let scanTruncated = rawRows.length > candidateLimit;
|
|
91
|
+
if (scanTruncated) rawRows = rawRows.slice(0, candidateLimit);
|
|
92
|
+
const matches = [];
|
|
93
|
+
let total = 0;
|
|
94
|
+
let omittedOversizedScalars = 0;
|
|
95
|
+
for (const row of rawRows) {
|
|
96
|
+
const node = rowToNode(row);
|
|
97
|
+
if (normalized.mode === "regex" && node.valueText.length > limits.maxRegexValueLength) {
|
|
98
|
+
omittedOversizedScalars += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
normalized.regex?.lastIndex && (normalized.regex.lastIndex = 0);
|
|
102
|
+
if (!nodeMatchesSearch(node, normalized)) continue;
|
|
103
|
+
total += 1;
|
|
104
|
+
if (matches.length < normalized.limit) matches.push(publicMatch(node));
|
|
105
|
+
}
|
|
106
|
+
const truncated = scanTruncated || omittedOversizedScalars > 0 || total > matches.length;
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
...responseBase(metadata, request),
|
|
109
|
+
total,
|
|
110
|
+
returned: matches.length,
|
|
111
|
+
truncated,
|
|
112
|
+
scanTruncated,
|
|
113
|
+
omittedOversizedScalars,
|
|
114
|
+
matches: Object.freeze(matches)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getMatch(matchId, freshnessOptions = {}) {
|
|
119
|
+
assertOpen();
|
|
120
|
+
const metadata = backend.metadata();
|
|
121
|
+
const node = rowToNode(backend.getMatch(String(matchId || "")));
|
|
122
|
+
return Object.freeze({ ...responseBase(metadata, freshnessOptions), match: node ? publicMatch(node) : null });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getComponent(request = {}) {
|
|
126
|
+
assertOpen();
|
|
127
|
+
const metadata = backend.metadata();
|
|
128
|
+
const filters = normalizeFilters(request);
|
|
129
|
+
const rows = backend.getComponent(filters).map(componentFromRow).filter(row => identityMatches({
|
|
130
|
+
...row,
|
|
131
|
+
field: "",
|
|
132
|
+
pointer: "",
|
|
133
|
+
valueType: ""
|
|
134
|
+
}, { ...filters, fields: [], valueTypes: [], pathFilters: [] }));
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
...responseBase(metadata, request),
|
|
137
|
+
components: Object.freeze(rows.map(row => Object.freeze({
|
|
138
|
+
project: Object.freeze({ id: row.projectId, slug: row.projectSlug }),
|
|
139
|
+
versionNumber: row.versionNumber,
|
|
140
|
+
componentSet: Object.freeze({ revision: row.componentSetRevision, checksum: row.componentChecksum }),
|
|
141
|
+
instanceId: row.instanceId,
|
|
142
|
+
templateId: row.templateId,
|
|
143
|
+
revision: row.instanceRevision,
|
|
144
|
+
dataHash: row.dataHash,
|
|
145
|
+
data: cloneJson(row.data)
|
|
146
|
+
})))
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function status(freshnessOptions = {}) {
|
|
151
|
+
assertOpen();
|
|
152
|
+
const metadata = backend.metadata();
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
...responseBase(metadata, freshnessOptions),
|
|
155
|
+
ready: Boolean(metadata),
|
|
156
|
+
filePath: backend.filePath,
|
|
157
|
+
snapshotHash: metadata?.snapshotHash || null,
|
|
158
|
+
actor: metadata?.actor || null,
|
|
159
|
+
projectVersionCount: metadata?.projectVersionCount || 0,
|
|
160
|
+
...backend.counts()
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function close() {
|
|
165
|
+
if (closed) return;
|
|
166
|
+
backend.close();
|
|
167
|
+
closed = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function purge() {
|
|
171
|
+
if (!closed) close();
|
|
172
|
+
const removedFiles = purgeBackendFiles(backend);
|
|
173
|
+
return Object.freeze({ authoritative: false, purged: true, removedFiles, filePath: backend.filePath });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
backend: backend.kind,
|
|
178
|
+
searchEngine: backend.searchEngine,
|
|
179
|
+
filePath: backend.filePath,
|
|
180
|
+
authoritative: false,
|
|
181
|
+
requiresServerReadBeforeWrite: true,
|
|
182
|
+
replaceSnapshot,
|
|
183
|
+
search,
|
|
184
|
+
getMatch,
|
|
185
|
+
getComponent,
|
|
186
|
+
status,
|
|
187
|
+
close,
|
|
188
|
+
purge
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = Object.freeze({
|
|
193
|
+
INDEX_SCHEMA_VERSION,
|
|
194
|
+
SERVER_SNAPSHOT_SOURCE,
|
|
195
|
+
createLocalIndex,
|
|
196
|
+
flattenJsonScalars,
|
|
197
|
+
escapeJsonPointerToken,
|
|
198
|
+
unescapeJsonPointerToken
|
|
199
|
+
});
|