@opensip-cli/datastore 0.1.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 (102) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +31 -0
  4. package/dist/__tests__/baseline-repo.test.d.ts +2 -0
  5. package/dist/__tests__/baseline-repo.test.d.ts.map +1 -0
  6. package/dist/__tests__/baseline-repo.test.js +85 -0
  7. package/dist/__tests__/baseline-repo.test.js.map +1 -0
  8. package/dist/__tests__/data-store.test.d.ts +10 -0
  9. package/dist/__tests__/data-store.test.d.ts.map +1 -0
  10. package/dist/__tests__/data-store.test.js +68 -0
  11. package/dist/__tests__/data-store.test.js.map +1 -0
  12. package/dist/__tests__/factory.test.d.ts +2 -0
  13. package/dist/__tests__/factory.test.d.ts.map +1 -0
  14. package/dist/__tests__/factory.test.js +168 -0
  15. package/dist/__tests__/factory.test.js.map +1 -0
  16. package/dist/__tests__/migration-integrity.test.d.ts +2 -0
  17. package/dist/__tests__/migration-integrity.test.d.ts.map +1 -0
  18. package/dist/__tests__/migration-integrity.test.js +78 -0
  19. package/dist/__tests__/migration-integrity.test.js.map +1 -0
  20. package/dist/__tests__/tool-state-repo.test.d.ts +7 -0
  21. package/dist/__tests__/tool-state-repo.test.d.ts.map +1 -0
  22. package/dist/__tests__/tool-state-repo.test.js +54 -0
  23. package/dist/__tests__/tool-state-repo.test.js.map +1 -0
  24. package/dist/__tests__/version-guard.test.d.ts +2 -0
  25. package/dist/__tests__/version-guard.test.d.ts.map +1 -0
  26. package/dist/__tests__/version-guard.test.js +110 -0
  27. package/dist/__tests__/version-guard.test.js.map +1 -0
  28. package/dist/backends/memory.d.ts +3 -0
  29. package/dist/backends/memory.d.ts.map +1 -0
  30. package/dist/backends/memory.js +5 -0
  31. package/dist/backends/memory.js.map +1 -0
  32. package/dist/backends/shared.d.ts +3 -0
  33. package/dist/backends/shared.d.ts.map +1 -0
  34. package/dist/backends/shared.js +31 -0
  35. package/dist/backends/shared.js.map +1 -0
  36. package/dist/backends/sqlite.d.ts +5 -0
  37. package/dist/backends/sqlite.d.ts.map +1 -0
  38. package/dist/backends/sqlite.js +8 -0
  39. package/dist/backends/sqlite.js.map +1 -0
  40. package/dist/baseline-repo.d.ts +50 -0
  41. package/dist/baseline-repo.d.ts.map +1 -0
  42. package/dist/baseline-repo.js +159 -0
  43. package/dist/baseline-repo.js.map +1 -0
  44. package/dist/data-store.d.ts +78 -0
  45. package/dist/data-store.d.ts.map +1 -0
  46. package/dist/data-store.js +71 -0
  47. package/dist/data-store.js.map +1 -0
  48. package/dist/factory.d.ts +47 -0
  49. package/dist/factory.d.ts.map +1 -0
  50. package/dist/factory.js +151 -0
  51. package/dist/factory.js.map +1 -0
  52. package/dist/index.d.ts +10 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +11 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/schema/baseline.d.ts +178 -0
  57. package/dist/schema/baseline.d.ts.map +1 -0
  58. package/dist/schema/baseline.js +31 -0
  59. package/dist/schema/baseline.js.map +1 -0
  60. package/dist/schema/tool-state.d.ts +110 -0
  61. package/dist/schema/tool-state.d.ts.map +1 -0
  62. package/dist/schema/tool-state.js +20 -0
  63. package/dist/schema/tool-state.js.map +1 -0
  64. package/dist/schema-version.d.ts +23 -0
  65. package/dist/schema-version.d.ts.map +1 -0
  66. package/dist/schema-version.js +55 -0
  67. package/dist/schema-version.js.map +1 -0
  68. package/dist/tool-state-repo.d.ts +51 -0
  69. package/dist/tool-state-repo.d.ts.map +1 -0
  70. package/dist/tool-state-repo.js +110 -0
  71. package/dist/tool-state-repo.js.map +1 -0
  72. package/migrations/.gitkeep +0 -0
  73. package/migrations/0000_sticky_white_tiger.sql +39 -0
  74. package/migrations/0001_easy_harry_osborn.sql +18 -0
  75. package/migrations/0002_plain_amazoness.sql +5 -0
  76. package/migrations/0003_mysterious_khan.sql +6 -0
  77. package/migrations/0004_narrow_bloodscream.sql +3 -0
  78. package/migrations/0005_lying_luke_cage.sql +7 -0
  79. package/migrations/0006_mean_photon.sql +12 -0
  80. package/migrations/0007_parallel_chamber.sql +3 -0
  81. package/migrations/0008_flaky_victor_mancha.sql +7 -0
  82. package/migrations/0009_stable_tool_identity.sql +9 -0
  83. package/migrations/0010_add_timestamp_iso_and_payload_version.sql +11 -0
  84. package/migrations/0011_payload_version_safety_and_notes.sql +21 -0
  85. package/migrations/0012_overrated_talon.sql +21 -0
  86. package/migrations/0013_lovely_zarda.sql +1 -0
  87. package/migrations/meta/0000_snapshot.json +269 -0
  88. package/migrations/meta/0001_snapshot.json +369 -0
  89. package/migrations/meta/0002_snapshot.json +400 -0
  90. package/migrations/meta/0003_snapshot.json +441 -0
  91. package/migrations/meta/0004_snapshot.json +270 -0
  92. package/migrations/meta/0005_snapshot.json +315 -0
  93. package/migrations/meta/0006_snapshot.json +382 -0
  94. package/migrations/meta/0007_snapshot.json +303 -0
  95. package/migrations/meta/0008_snapshot.json +346 -0
  96. package/migrations/meta/0009_snapshot.json +367 -0
  97. package/migrations/meta/0010_snapshot.json +382 -0
  98. package/migrations/meta/0011_snapshot.json +382 -0
  99. package/migrations/meta/0012_snapshot.json +512 -0
  100. package/migrations/meta/0013_snapshot.json +458 -0
  101. package/migrations/meta/_journal.json +104 -0
  102. package/package.json +56 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Generic host-owned keyed tool state (ADR-0042 — the third-party persistence
3
+ * parity mechanism). ONE table serves every tool: each row is one opaque JSON
4
+ * payload under a `(tool, key)` identity, so tools share the table but never
5
+ * see each other's rows — the exact pattern the ADR-0036 baseline pair proved.
6
+ * Tools reach it ONLY through the `ToolStateRepo` / the `cli.toolState` seams;
7
+ * they never own schema.
8
+ *
9
+ * Durability note: unlike baselines (drop-and-recapture, CI-ephemeral), tool
10
+ * state is DURABLE tool data — a release never drops these rows.
11
+ */
12
+ export declare const toolState: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
13
+ name: "tool_state";
14
+ schema: undefined;
15
+ columns: {
16
+ tool: import("drizzle-orm/sqlite-core").SQLiteColumn<{
17
+ name: "tool";
18
+ tableName: "tool_state";
19
+ dataType: "string";
20
+ columnType: "SQLiteText";
21
+ data: string;
22
+ driverParam: string;
23
+ notNull: true;
24
+ hasDefault: false;
25
+ isPrimaryKey: false;
26
+ isAutoincrement: false;
27
+ hasRuntimeDefault: false;
28
+ enumValues: [string, ...string[]];
29
+ baseColumn: never;
30
+ identity: undefined;
31
+ generated: undefined;
32
+ }, {}, {
33
+ length: number | undefined;
34
+ }>;
35
+ stableId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
36
+ name: "stable_id";
37
+ tableName: "tool_state";
38
+ dataType: "string";
39
+ columnType: "SQLiteText";
40
+ data: string;
41
+ driverParam: string;
42
+ notNull: false;
43
+ hasDefault: false;
44
+ isPrimaryKey: false;
45
+ isAutoincrement: false;
46
+ hasRuntimeDefault: false;
47
+ enumValues: [string, ...string[]];
48
+ baseColumn: never;
49
+ identity: undefined;
50
+ generated: undefined;
51
+ }, {}, {
52
+ length: number | undefined;
53
+ }>;
54
+ key: import("drizzle-orm/sqlite-core").SQLiteColumn<{
55
+ name: "key";
56
+ tableName: "tool_state";
57
+ dataType: "string";
58
+ columnType: "SQLiteText";
59
+ data: string;
60
+ driverParam: string;
61
+ notNull: true;
62
+ hasDefault: false;
63
+ isPrimaryKey: false;
64
+ isAutoincrement: false;
65
+ hasRuntimeDefault: false;
66
+ enumValues: [string, ...string[]];
67
+ baseColumn: never;
68
+ identity: undefined;
69
+ generated: undefined;
70
+ }, {}, {
71
+ length: number | undefined;
72
+ }>;
73
+ payload: import("drizzle-orm/sqlite-core").SQLiteColumn<{
74
+ name: "payload";
75
+ tableName: "tool_state";
76
+ dataType: "json";
77
+ columnType: "SQLiteTextJson";
78
+ data: unknown;
79
+ driverParam: string;
80
+ notNull: false;
81
+ hasDefault: false;
82
+ isPrimaryKey: false;
83
+ isAutoincrement: false;
84
+ hasRuntimeDefault: false;
85
+ enumValues: undefined;
86
+ baseColumn: never;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }, {}, {}>;
90
+ updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
91
+ name: "updated_at";
92
+ tableName: "tool_state";
93
+ dataType: "number";
94
+ columnType: "SQLiteInteger";
95
+ data: number;
96
+ driverParam: number;
97
+ notNull: true;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ };
108
+ dialect: "sqlite";
109
+ }>;
110
+ //# sourceMappingURL=tool-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-state.d.ts","sourceRoot":"","sources":["../../src/schema/tool-state.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUrB,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+ /**
3
+ * Generic host-owned keyed tool state (ADR-0042 — the third-party persistence
4
+ * parity mechanism). ONE table serves every tool: each row is one opaque JSON
5
+ * payload under a `(tool, key)` identity, so tools share the table but never
6
+ * see each other's rows — the exact pattern the ADR-0036 baseline pair proved.
7
+ * Tools reach it ONLY through the `ToolStateRepo` / the `cli.toolState` seams;
8
+ * they never own schema.
9
+ *
10
+ * Durability note: unlike baselines (drop-and-recapture, CI-ephemeral), tool
11
+ * state is DURABLE tool data — a release never drops these rows.
12
+ */
13
+ export const toolState = sqliteTable('tool_state', {
14
+ tool: text('tool').notNull(), // human `name` value (for compat + current queries)
15
+ stableId: text('stable_id'), // tool stable UUID (additive per ADR-0048; null for legacy)
16
+ key: text('key').notNull(),
17
+ payload: text('payload', { mode: 'json' }),
18
+ updatedAt: integer('updated_at').notNull(),
19
+ }, (t) => [primaryKey({ columns: [t.tool, t.key] })]);
20
+ //# sourceMappingURL=tool-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-state.js","sourceRoot":"","sources":["../../src/schema/tool-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEjF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAClC,YAAY,EACZ;IACE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,oDAAoD;IAClF,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,4DAA4D;IACzF,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;IAC1B,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE;CAC3C,EACD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAClD,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The DB schema version this CLI supports = the count of migrations it bundles.
3
+ *
4
+ * Returns `undefined` when the bundled journal cannot be read or parsed (a
5
+ * broken install). Callers treat `undefined` as "skip the version guard": the
6
+ * subsequent `migrate()` reads the same journal and will surface the canonical
7
+ * {@link DataStoreMigrationError} loudly, so skipping here hides nothing — it
8
+ * just declines to invent a version we cannot determine. We still warn so the
9
+ * anomaly is observable rather than silent.
10
+ */
11
+ export declare function readSupportedDbVersion(migrationsFolder: string): number | undefined;
12
+ /**
13
+ * True when the on-disk database was stamped by a CLI that knew MORE migrations
14
+ * than this one (`dbVersion > supportedVersion`) — i.e. the user downgraded the
15
+ * CLI after a newer version advanced the schema.
16
+ *
17
+ * The forward direction (`dbVersion <= supportedVersion`, including the `0` of a
18
+ * fresh or pre-guard "legacy" database) is always safe: Drizzle's migrator
19
+ * applies any pending migrations and we re-stamp afterward. Only the future
20
+ * database is blocked.
21
+ */
22
+ export declare function isDbNewerThanCli(dbVersion: number, supportedVersion: number): boolean;
23
+ //# sourceMappingURL=schema-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-version.d.ts","sourceRoot":"","sources":["../src/schema-version.ts"],"names":[],"mappings":"AAsBA;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAErF"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * schema-version — derives the DB schema version this CLI supports from the
3
+ * bundled Drizzle migration journal, and owns the version-guard math.
4
+ *
5
+ * The "supported version" is simply the number of migrations this build ships
6
+ * (the journal's entry count). It is monotonic: every new migration increments
7
+ * it automatically, so there is NO constant to hand-bump when a migration is
8
+ * added. We stamp this integer into the SQLite header (`PRAGMA user_version`)
9
+ * after a successful migrate, and compare it on the next open to detect a
10
+ * database written by a NEWER CLI than the one now opening it (the downgrade
11
+ * direction Drizzle's own migrator cannot detect — see {@link isDbNewerThanCli}).
12
+ */
13
+ import { readFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { logger } from '@opensip-cli/core';
16
+ /**
17
+ * The DB schema version this CLI supports = the count of migrations it bundles.
18
+ *
19
+ * Returns `undefined` when the bundled journal cannot be read or parsed (a
20
+ * broken install). Callers treat `undefined` as "skip the version guard": the
21
+ * subsequent `migrate()` reads the same journal and will surface the canonical
22
+ * {@link DataStoreMigrationError} loudly, so skipping here hides nothing — it
23
+ * just declines to invent a version we cannot determine. We still warn so the
24
+ * anomaly is observable rather than silent.
25
+ */
26
+ export function readSupportedDbVersion(migrationsFolder) {
27
+ const journalPath = join(migrationsFolder, 'meta', '_journal.json');
28
+ try {
29
+ const parsed = JSON.parse(readFileSync(journalPath, 'utf8'));
30
+ return Array.isArray(parsed.entries) ? parsed.entries.length : undefined;
31
+ }
32
+ catch (error) {
33
+ logger.warn({
34
+ evt: 'datastore.schema-version.journal-unreadable',
35
+ module: 'datastore:schema-version',
36
+ journalPath,
37
+ err: error,
38
+ });
39
+ return undefined;
40
+ }
41
+ }
42
+ /**
43
+ * True when the on-disk database was stamped by a CLI that knew MORE migrations
44
+ * than this one (`dbVersion > supportedVersion`) — i.e. the user downgraded the
45
+ * CLI after a newer version advanced the schema.
46
+ *
47
+ * The forward direction (`dbVersion <= supportedVersion`, including the `0` of a
48
+ * fresh or pre-guard "legacy" database) is always safe: Drizzle's migrator
49
+ * applies any pending migrations and we re-stamp afterward. Only the future
50
+ * database is blocked.
51
+ */
52
+ export function isDbNewerThanCli(dbVersion, supportedVersion) {
53
+ return dbVersion > supportedVersion;
54
+ }
55
+ //# sourceMappingURL=schema-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-version.js","sourceRoot":"","sources":["../src/schema-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAO3C;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,gBAAwB;IAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,gBAAgB,EAAE,MAAM,EAAE,eAAe,CAAC,CAAC;IACpE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAmB,CAAC;QAC/E,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,IAAI,CAAC;YACV,GAAG,EAAE,6CAA6C;YAClD,MAAM,EAAE,0BAA0B;YAClC,WAAW;YACX,GAAG,EAAE,KAAK;SACX,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,gBAAwB;IAC1E,OAAO,SAAS,GAAG,gBAAgB,CAAC;AACtC,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { type DataStore } from './data-store.js';
2
+ /**
3
+ * Per-payload size ceiling (UTF-8 bytes of the JSON serialization). Exceeding
4
+ * it ERRORS (a typed `ValidationError`) rather than evicting — silent eviction
5
+ * hides bugs; an explicit cap teaches the tool author to shard or summarize.
6
+ * Documented on the `cli.toolState` seam JSDoc (ADR-0042).
7
+ */
8
+ export declare const TOOL_STATE_MAX_PAYLOAD_BYTES: number;
9
+ /**
10
+ * The generic host-owned keyed tool-state repository (ADR-0042). One repo over
11
+ * the shared `tool_state` table, scoped by the `tool` column — every tool gets
12
+ * durable keyed JSON persistence without owning schema, the same generic-table
13
+ * pattern the ADR-0036 baseline pair proved. Tools consume it through the
14
+ * `cli.toolState` seams; the payload is opaque to the host.
15
+ *
16
+ * ## Versioning convention for toolState values
17
+ *
18
+ * For any key whose value shape a tool treats as versioned, the tool SHOULD
19
+ * include a top-level numeric `"__version": N` (starting at 1) inside the
20
+ * object it puts. The host never reads or enforces it — use
21
+ * `extractPayloadVersion` (from '@opensip-cli/core') inside your tool code
22
+ * when you `get` a key to decide projection/migration.
23
+ *
24
+ * Keys that are truly schemaless (arbitrary user data) may omit `__version`.
25
+ * See the payload schema evolution plan / ADR-0050 for safe vs. major rules.
26
+ */
27
+ export declare class ToolStateRepo {
28
+ private readonly datastore;
29
+ constructor(datastore: DataStore);
30
+ /** Read one payload, or undefined when the key has never been put. */
31
+ get(tool: string, key: string): unknown;
32
+ /**
33
+ * Upsert one payload under `(tool, key)`.
34
+ *
35
+ * Callers that version the state for a stable key SHOULD put an object
36
+ * whose top level contains `"__version": 1` (or higher after a breaking change
37
+ * per the evolution rules). The host stores it opaquely; on read the caller
38
+ * uses `extractPayloadVersion(payload)` to inspect.
39
+ *
40
+ * @throws {ValidationError} when the JSON serialization exceeds
41
+ * {@link TOOL_STATE_MAX_PAYLOAD_BYTES} (error, never evict).
42
+ */
43
+ put(tool: string, key: string, payload: unknown): void;
44
+ /** Delete one key (no-op when absent). */
45
+ delete(tool: string, key: string): void;
46
+ /** List this tool's keys, sorted (never another tool's). */
47
+ list(tool: string): readonly string[];
48
+ /** Delete ALL of this tool's state rows; returns the deleted count. */
49
+ clear(tool: string): number;
50
+ }
51
+ //# sourceMappingURL=tool-state-repo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-state-repo.d.ts","sourceRoot":"","sources":["../src/tool-state-repo.ts"],"names":[],"mappings":"AAGA,OAAO,EAA2B,KAAK,SAAS,EAAyB,MAAM,iBAAiB,CAAC;AAKjG;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,QAAa,CAAC;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;gBAEjC,SAAS,EAAE,SAAS;IAIhC,sEAAsE;IACtE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAUvC;;;;;;;;;;OAUG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IA4BtD,0CAA0C;IAC1C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAOvC,4DAA4D;IAC5D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE;IAUrC,uEAAuE;IACvE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAU5B"}
@@ -0,0 +1,110 @@
1
+ import { logger, ValidationError } from '@opensip-cli/core';
2
+ import { and, asc, eq } from 'drizzle-orm';
3
+ import { requireDrizzleDataStore } from './data-store.js';
4
+ import { toolState } from './schema/tool-state.js';
5
+ const MODULE_NAME = 'datastore:tool-state-repo';
6
+ /**
7
+ * Per-payload size ceiling (UTF-8 bytes of the JSON serialization). Exceeding
8
+ * it ERRORS (a typed `ValidationError`) rather than evicting — silent eviction
9
+ * hides bugs; an explicit cap teaches the tool author to shard or summarize.
10
+ * Documented on the `cli.toolState` seam JSDoc (ADR-0042).
11
+ */
12
+ export const TOOL_STATE_MAX_PAYLOAD_BYTES = 256 * 1024;
13
+ /**
14
+ * The generic host-owned keyed tool-state repository (ADR-0042). One repo over
15
+ * the shared `tool_state` table, scoped by the `tool` column — every tool gets
16
+ * durable keyed JSON persistence without owning schema, the same generic-table
17
+ * pattern the ADR-0036 baseline pair proved. Tools consume it through the
18
+ * `cli.toolState` seams; the payload is opaque to the host.
19
+ *
20
+ * ## Versioning convention for toolState values
21
+ *
22
+ * For any key whose value shape a tool treats as versioned, the tool SHOULD
23
+ * include a top-level numeric `"__version": N` (starting at 1) inside the
24
+ * object it puts. The host never reads or enforces it — use
25
+ * `extractPayloadVersion` (from '@opensip-cli/core') inside your tool code
26
+ * when you `get` a key to decide projection/migration.
27
+ *
28
+ * Keys that are truly schemaless (arbitrary user data) may omit `__version`.
29
+ * See the payload schema evolution plan / ADR-0050 for safe vs. major rules.
30
+ */
31
+ export class ToolStateRepo {
32
+ datastore;
33
+ constructor(datastore) {
34
+ this.datastore = requireDrizzleDataStore(datastore);
35
+ }
36
+ /** Read one payload, or undefined when the key has never been put. */
37
+ get(tool, key) {
38
+ const row = this.datastore.db
39
+ .select({ payload: toolState.payload })
40
+ .from(toolState)
41
+ .where(and(eq(toolState.tool, tool), eq(toolState.key, key)))
42
+ .limit(1)
43
+ .get();
44
+ return row?.payload ?? undefined;
45
+ }
46
+ /**
47
+ * Upsert one payload under `(tool, key)`.
48
+ *
49
+ * Callers that version the state for a stable key SHOULD put an object
50
+ * whose top level contains `"__version": 1` (or higher after a breaking change
51
+ * per the evolution rules). The host stores it opaquely; on read the caller
52
+ * uses `extractPayloadVersion(payload)` to inspect.
53
+ *
54
+ * @throws {ValidationError} when the JSON serialization exceeds
55
+ * {@link TOOL_STATE_MAX_PAYLOAD_BYTES} (error, never evict).
56
+ */
57
+ put(tool, key, payload) {
58
+ const bytes = Buffer.byteLength(JSON.stringify(payload) ?? 'null', 'utf8');
59
+ if (bytes > TOOL_STATE_MAX_PAYLOAD_BYTES) {
60
+ // @fitness-ignore-next-line result-pattern-consistency -- documented @throws boundary (the cap errors, never evicts); exceptions propagate to the public Result/CommandOutcome boundary, matching the BaselineRepo/file-cache precedent
61
+ throw new ValidationError(`tool_state payload for '${tool}/${key}' is ${bytes} bytes — over the ` +
62
+ `${TOOL_STATE_MAX_PAYLOAD_BYTES}-byte cap (ADR-0042). Shard or summarize the payload.`, { code: 'VALIDATION.TOOL_STATE.PAYLOAD_TOO_LARGE' });
63
+ }
64
+ const updatedAt = Date.now();
65
+ this.datastore.db
66
+ .insert(toolState)
67
+ .values({ tool, key, payload, updatedAt })
68
+ .onConflictDoUpdate({
69
+ target: [toolState.tool, toolState.key],
70
+ set: { payload, updatedAt },
71
+ })
72
+ .run();
73
+ logger.debug({
74
+ evt: 'datastore.tool_state.put',
75
+ module: MODULE_NAME,
76
+ tool,
77
+ key,
78
+ bytes,
79
+ });
80
+ }
81
+ /** Delete one key (no-op when absent). */
82
+ delete(tool, key) {
83
+ this.datastore.db
84
+ .delete(toolState)
85
+ .where(and(eq(toolState.tool, tool), eq(toolState.key, key)))
86
+ .run();
87
+ }
88
+ /** List this tool's keys, sorted (never another tool's). */
89
+ list(tool) {
90
+ return this.datastore.db
91
+ .select({ key: toolState.key })
92
+ .from(toolState)
93
+ .where(eq(toolState.tool, tool))
94
+ .orderBy(asc(toolState.key))
95
+ .all()
96
+ .map((r) => r.key);
97
+ }
98
+ /** Delete ALL of this tool's state rows; returns the deleted count. */
99
+ clear(tool) {
100
+ const result = this.datastore.db.delete(toolState).where(eq(toolState.tool, tool)).run();
101
+ logger.info({
102
+ evt: 'datastore.tool_state.clear.complete',
103
+ module: MODULE_NAME,
104
+ tool,
105
+ count: result.changes,
106
+ });
107
+ return result.changes;
108
+ }
109
+ }
110
+ //# sourceMappingURL=tool-state-repo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-state-repo.js","sourceRoot":"","sources":["../src/tool-state-repo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,uBAAuB,EAAyC,MAAM,iBAAiB,CAAC;AACjG,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,WAAW,GAAG,2BAA2B,CAAC;AAEhD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,GAAG,IAAI,CAAC;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,aAAa;IACP,SAAS,CAAmB;IAE7C,YAAY,SAAoB;QAC9B,IAAI,CAAC,SAAS,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;IAED,sEAAsE;IACtE,GAAG,CAAC,IAAY,EAAE,GAAW;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE;aAC1B,MAAM,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,CAAC;aACtC,IAAI,CAAC,SAAS,CAAC;aACf,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;aAC5D,KAAK,CAAC,CAAC,CAAC;aACR,GAAG,EAAE,CAAC;QACT,OAAO,GAAG,EAAE,OAAO,IAAI,SAAS,CAAC;IACnC,CAAC;IAED;;;;;;;;;;OAUG;IACH,GAAG,CAAC,IAAY,EAAE,GAAW,EAAE,OAAgB;QAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,MAAM,EAAE,MAAM,CAAC,CAAC;QAC3E,IAAI,KAAK,GAAG,4BAA4B,EAAE,CAAC;YACzC,wOAAwO;YACxO,MAAM,IAAI,eAAe,CACvB,2BAA2B,IAAI,IAAI,GAAG,QAAQ,KAAK,oBAAoB;gBACrE,GAAG,4BAA4B,uDAAuD,EACxF,EAAE,IAAI,EAAE,yCAAyC,EAAE,CACpD,CAAC;QACJ,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,SAAS,CAAC,EAAE;aACd,MAAM,CAAC,SAAS,CAAC;aACjB,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;aACzC,kBAAkB,CAAC;YAClB,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,GAAG,CAAC;YACvC,GAAG,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE;SAC5B,CAAC;aACD,GAAG,EAAE,CAAC;QACT,MAAM,CAAC,KAAK,CAAC;YACX,GAAG,EAAE,0BAA0B;YAC/B,MAAM,EAAE,WAAW;YACnB,IAAI;YACJ,GAAG;YACH,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED,0CAA0C;IAC1C,MAAM,CAAC,IAAY,EAAE,GAAW;QAC9B,IAAI,CAAC,SAAS,CAAC,EAAE;aACd,MAAM,CAAC,SAAS,CAAC;aACjB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;aAC5D,GAAG,EAAE,CAAC;IACX,CAAC;IAED,4DAA4D;IAC5D,IAAI,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE;aACrB,MAAM,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC;aAC9B,IAAI,CAAC,SAAS,CAAC;aACf,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;aAC/B,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;aAC3B,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,IAAY;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QACzF,MAAM,CAAC,IAAI,CAAC;YACV,GAAG,EAAE,qCAAqC;YAC1C,MAAM,EAAE,WAAW;YACnB,IAAI;YACJ,KAAK,EAAE,MAAM,CAAC,OAAO;SACtB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;CACF"}
File without changes
@@ -0,0 +1,39 @@
1
+ CREATE TABLE `session_checks` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `session_id` text NOT NULL,
4
+ `check_slug` text NOT NULL,
5
+ `passed` integer NOT NULL,
6
+ `violation_count` integer,
7
+ `duration_ms` integer NOT NULL,
8
+ FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE cascade
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE INDEX `session_checks_session_idx` ON `session_checks` (`session_id`);--> statement-breakpoint
12
+ CREATE TABLE `session_findings` (
13
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
14
+ `session_check_id` integer NOT NULL,
15
+ `rule_id` text NOT NULL,
16
+ `severity` text NOT NULL,
17
+ `message` text NOT NULL,
18
+ `file_path` text,
19
+ `line` integer,
20
+ `column` integer,
21
+ `suggestion` text,
22
+ `category` text,
23
+ FOREIGN KEY (`session_check_id`) REFERENCES `session_checks`(`id`) ON UPDATE no action ON DELETE cascade
24
+ );
25
+ --> statement-breakpoint
26
+ CREATE INDEX `session_findings_check_idx` ON `session_findings` (`session_check_id`);--> statement-breakpoint
27
+ CREATE TABLE `sessions` (
28
+ `id` text PRIMARY KEY NOT NULL,
29
+ `tool` text NOT NULL,
30
+ `timestamp` integer NOT NULL,
31
+ `cwd` text NOT NULL,
32
+ `recipe` text,
33
+ `score` integer NOT NULL,
34
+ `passed` integer NOT NULL,
35
+ `summary` text NOT NULL,
36
+ `duration_ms` integer NOT NULL
37
+ );
38
+ --> statement-breakpoint
39
+ CREATE INDEX `sessions_tool_timestamp_idx` ON `sessions` (`tool`,"timestamp" DESC);
@@ -0,0 +1,18 @@
1
+ CREATE TABLE `graph_baseline_meta` (
2
+ `id` integer PRIMARY KEY NOT NULL,
3
+ `captured_at` integer NOT NULL
4
+ );
5
+ --> statement-breakpoint
6
+ CREATE TABLE `graph_baseline_signals` (
7
+ `fingerprint` text PRIMARY KEY NOT NULL,
8
+ `captured_at` integer NOT NULL
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE TABLE `graph_catalog` (
12
+ `id` integer PRIMARY KEY NOT NULL,
13
+ `language` text NOT NULL,
14
+ `cache_key` text NOT NULL,
15
+ `files_fingerprint` text NOT NULL,
16
+ `built_at` text NOT NULL,
17
+ `payload` text NOT NULL
18
+ );
@@ -0,0 +1,5 @@
1
+ CREATE TABLE `fit_baseline` (
2
+ `id` integer PRIMARY KEY NOT NULL,
3
+ `captured_at` integer NOT NULL,
4
+ `payload` text NOT NULL
5
+ );
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `session_tool_payload` (
2
+ `session_id` text PRIMARY KEY NOT NULL,
3
+ `tool` text NOT NULL,
4
+ `payload` text NOT NULL,
5
+ FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE cascade
6
+ );
@@ -0,0 +1,3 @@
1
+ DROP TABLE `session_checks`;--> statement-breakpoint
2
+ DROP TABLE `session_findings`;--> statement-breakpoint
3
+ ALTER TABLE `sessions` DROP COLUMN `summary`;
@@ -0,0 +1,7 @@
1
+ CREATE TABLE `graph_shard_fragment` (
2
+ `shard_id` text PRIMARY KEY NOT NULL,
3
+ `language` text NOT NULL,
4
+ `cache_key` text NOT NULL,
5
+ `shard_fingerprint` text NOT NULL,
6
+ `payload` text NOT NULL
7
+ );
@@ -0,0 +1,12 @@
1
+ CREATE TABLE `tool_baseline_entries` (
2
+ `tool` text NOT NULL,
3
+ `fingerprint` text NOT NULL,
4
+ `payload` text,
5
+ `captured_at` integer NOT NULL,
6
+ PRIMARY KEY(`tool`, `fingerprint`)
7
+ );
8
+ --> statement-breakpoint
9
+ CREATE TABLE `tool_baseline_meta` (
10
+ `tool` text PRIMARY KEY NOT NULL,
11
+ `captured_at` integer NOT NULL
12
+ );
@@ -0,0 +1,3 @@
1
+ DROP TABLE `graph_baseline_meta`;--> statement-breakpoint
2
+ DROP TABLE `graph_baseline_signals`;--> statement-breakpoint
3
+ DROP TABLE `fit_baseline`;
@@ -0,0 +1,7 @@
1
+ CREATE TABLE `tool_state` (
2
+ `tool` text NOT NULL,
3
+ `key` text NOT NULL,
4
+ `payload` text,
5
+ `updated_at` integer NOT NULL,
6
+ PRIMARY KEY(`tool`, `key`)
7
+ );
@@ -0,0 +1,9 @@
1
+ -- Additive columns for tool stable UUID (ADR-0048). Legacy rows get NULL;
2
+ -- new writes from provenance (when stableId present in manifest) or future
3
+ -- ratchet paths can populate it. The `tool` column retains the human name
4
+ -- for current queries/compat.
5
+ ALTER TABLE `tool_state` ADD COLUMN `stable_id` text;
6
+ --> statement-breakpoint
7
+ ALTER TABLE `tool_baseline_entries` ADD COLUMN `stable_id` text;
8
+ --> statement-breakpoint
9
+ ALTER TABLE `tool_baseline_meta` ADD COLUMN `stable_id` text;
@@ -0,0 +1,11 @@
1
+ -- Add columns for timestamp fidelity (original ISO) and basic payload versioning.
2
+ -- Existing rows: timestamp_iso null (hydrate falls back to reconstructed), payload_version defaults to 1.
3
+ -- NOTE: statements are split by drizzle's breakpoint marker so each runs as its
4
+ -- own prepared statement (better-sqlite3 rejects multi-statement strings).
5
+ ALTER TABLE sessions ADD COLUMN timestamp_iso TEXT;
6
+ --> statement-breakpoint
7
+ ALTER TABLE session_tool_payload ADD COLUMN payload_version INTEGER NOT NULL DEFAULT 1;
8
+ --> statement-breakpoint
9
+ -- Backfill timestamp_iso for old rows (approximate from ms epoch).
10
+ -- Note: for exact original, re-persist sessions after upgrade.
11
+ UPDATE sessions SET timestamp_iso = strftime('%Y-%m-%dT%H:%M:%fZ', timestamp / 1000, 'unixepoch') WHERE timestamp_iso IS NULL;
@@ -0,0 +1,21 @@
1
+ -- Phase 7: Payload schema evolution — outer column safety + notes.
2
+ -- The `payload_version` column is created by migration 0010 (NOT NULL DEFAULT 1).
3
+ -- This migration intentionally carries NO further DDL: SQLite has no
4
+ -- `ADD COLUMN IF NOT EXISTS`, and drizzle's migrator does not emulate it, so
5
+ -- re-adding the column here would fail with "duplicate column name". The
6
+ -- migration exists to document the two-level versioning model and to run a
7
+ -- harmless, idempotent backfill. No JSON rewrite of historical payloads is
8
+ -- performed or required (projection on read is the strategy; see plan + ADR-0050).
9
+
10
+ -- Notes for operators / future migrations:
11
+ -- * payload_version (created in 0010) = outer storage contract version (bumped
12
+ -- only for host-visible changes between session-store and tools).
13
+ -- * The tool-owned inner version lives as "__version" (number) at the top level
14
+ -- of the JSON blob in the `payload` column.
15
+ -- * Legacy rows (column=1 or absent, or no __version in JSON) are treated as v1
16
+ -- with `fidelity: 'projection'`.
17
+ -- * Migrations on these tables must remain append-only. Backfills only for safe
18
+ -- host columns (timestamp_iso precedent); never rewrite tool JSON.
19
+
20
+ -- Backfill any rows that somehow missed the DEFAULT (belt-and-suspenders).
21
+ UPDATE session_tool_payload SET payload_version = 1 WHERE payload_version IS NULL;
@@ -0,0 +1,21 @@
1
+ CREATE TABLE `session_dashboard_contributions` (
2
+ `session_id` text NOT NULL,
3
+ `tool` text NOT NULL,
4
+ `contribution` text NOT NULL,
5
+ `version` integer DEFAULT 1 NOT NULL,
6
+ PRIMARY KEY(`session_id`, `tool`),
7
+ FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE cascade
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE TABLE `session_host_metrics` (
11
+ `session_id` text PRIMARY KEY NOT NULL,
12
+ `tty_busy_ms` integer,
13
+ `render_ms` integer,
14
+ `persist_ms` integer,
15
+ `egress_ms` integer,
16
+ `total_command_ms` integer,
17
+ FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE cascade
18
+ );
19
+ --> statement-breakpoint
20
+ ALTER TABLE `sessions` ADD `completed_at` integer;--> statement-breakpoint
21
+ ALTER TABLE `sessions` ADD `completed_at_iso` text;
@@ -0,0 +1 @@
1
+ DROP TABLE `session_dashboard_contributions`;