@cleocode/contracts 2026.5.107 → 2026.5.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/db-inventory.test.d.ts +25 -0
- package/dist/__tests__/db-inventory.test.d.ts.map +1 -0
- package/dist/__tests__/db-inventory.test.js +148 -0
- package/dist/__tests__/db-inventory.test.js.map +1 -0
- package/dist/__tests__/task-type-narrowing.test.d.ts +14 -0
- package/dist/__tests__/task-type-narrowing.test.d.ts.map +1 -0
- package/dist/__tests__/task-type-narrowing.test.js +51 -0
- package/dist/__tests__/task-type-narrowing.test.js.map +1 -0
- package/dist/brain.d.ts +103 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/db-inventory.d.ts +193 -0
- package/dist/db-inventory.d.ts.map +1 -0
- package/dist/db-inventory.js +69 -0
- package/dist/db-inventory.js.map +1 -0
- package/dist/doctor.d.ts +152 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operations/tasks.d.ts +4 -4
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/task.d.ts +13 -2
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/db-inventory.test.ts +184 -0
- package/src/__tests__/task-type-narrowing.test.ts +56 -0
- package/src/brain.ts +114 -0
- package/src/db-inventory.ts +227 -0
- package/src/doctor.ts +162 -0
- package/src/index.ts +19 -0
- package/src/operations/tasks.ts +4 -4
- package/src/task.ts +13 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract tests for the CLEO database inventory SSoT.
|
|
3
|
+
*
|
|
4
|
+
* Asserts the structural invariants the inventory MUST hold:
|
|
5
|
+
*
|
|
6
|
+
* 1. Every {@link DbInventoryEntry.role} is unique across the array.
|
|
7
|
+
* 2. Every {@link DbInventoryEntry.tier} is a valid member of the
|
|
8
|
+
* {@link DbTier} union.
|
|
9
|
+
* 3. Every non-null {@link DbInventoryEntry.drizzleSchemaPath} resolves
|
|
10
|
+
* to a real file on disk relative to the monorepo root.
|
|
11
|
+
* 4. Every non-null {@link DbInventoryEntry.migrationsDir} resolves to a
|
|
12
|
+
* real directory on disk relative to the monorepo root.
|
|
13
|
+
* 5. The list is non-empty and covers the AC1-mandated role set.
|
|
14
|
+
*
|
|
15
|
+
* These invariants are the gating contract for downstream saga tasks
|
|
16
|
+
* (T10307 fleet survey, T10310 pragma drift, T10311 migration coverage,
|
|
17
|
+
* T10312 doctor integrity, T10320 cross-DB invariants). If this suite
|
|
18
|
+
* fails, the inventory has drifted from disk reality and CI MUST block.
|
|
19
|
+
*
|
|
20
|
+
* @task T10305
|
|
21
|
+
* @epic T10282
|
|
22
|
+
* @saga T10281
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, statSync } from 'node:fs';
|
|
26
|
+
import { dirname, resolve } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { describe, expect, it } from 'vitest';
|
|
29
|
+
import { DB_INVENTORY, type DbInventoryEntry, type DbTier } from '../db-inventory.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the monorepo root from this test file.
|
|
33
|
+
*
|
|
34
|
+
* `__tests__/db-inventory.test.ts` lives at
|
|
35
|
+
* `packages/contracts/src/__tests__/` — four levels above the repo root.
|
|
36
|
+
*/
|
|
37
|
+
const THIS_FILE = fileURLToPath(import.meta.url);
|
|
38
|
+
const REPO_ROOT = resolve(dirname(THIS_FILE), '..', '..', '..', '..');
|
|
39
|
+
|
|
40
|
+
const VALID_TIERS: ReadonlySet<DbTier> = new Set<DbTier>(['project', 'global', 'derived']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Roles AC1 of T10305 explicitly requires the inventory to enumerate.
|
|
44
|
+
* The set must be covered exactly; extras are acceptable when documented.
|
|
45
|
+
*/
|
|
46
|
+
const REQUIRED_ROLES: readonly string[] = [
|
|
47
|
+
'tasks',
|
|
48
|
+
'brain',
|
|
49
|
+
'conduit',
|
|
50
|
+
'manifest',
|
|
51
|
+
'llmtxt',
|
|
52
|
+
'nexus',
|
|
53
|
+
'signaldock-project',
|
|
54
|
+
'signaldock-global',
|
|
55
|
+
'telemetry',
|
|
56
|
+
'skills',
|
|
57
|
+
'global-brain',
|
|
58
|
+
'global-tasks',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
describe('CLEO database inventory (T10305 SSoT)', () => {
|
|
62
|
+
it('is non-empty', () => {
|
|
63
|
+
expect(DB_INVENTORY.length).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('covers every role required by T10305 AC1', () => {
|
|
67
|
+
const seen = new Set<string>(DB_INVENTORY.map((e) => e.role));
|
|
68
|
+
for (const role of REQUIRED_ROLES) {
|
|
69
|
+
expect(seen.has(role), `inventory missing role '${role}'`).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('has unique role IDs across all entries', () => {
|
|
74
|
+
const roles = DB_INVENTORY.map((e) => e.role);
|
|
75
|
+
const unique = new Set(roles);
|
|
76
|
+
expect(unique.size).toBe(roles.length);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('uses only valid tier literals', () => {
|
|
80
|
+
for (const entry of DB_INVENTORY) {
|
|
81
|
+
expect(
|
|
82
|
+
VALID_TIERS.has(entry.tier),
|
|
83
|
+
`role '${entry.role}' has invalid tier '${entry.tier}'`,
|
|
84
|
+
).toBe(true);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('uses valid path tokens in filePathTemplate', () => {
|
|
89
|
+
// Project tier MUST use <projectRoot>/ prefix; global tier MUST use
|
|
90
|
+
// $XDG_DATA_HOME/cleo/ prefix. Derived tier may use either.
|
|
91
|
+
for (const entry of DB_INVENTORY) {
|
|
92
|
+
const t = entry.filePathTemplate;
|
|
93
|
+
if (entry.tier === 'project') {
|
|
94
|
+
expect(
|
|
95
|
+
t.startsWith('<projectRoot>/'),
|
|
96
|
+
`project-tier entry '${entry.role}' uses non-canonical path '${t}'`,
|
|
97
|
+
).toBe(true);
|
|
98
|
+
} else if (entry.tier === 'global') {
|
|
99
|
+
expect(
|
|
100
|
+
t.startsWith('$XDG_DATA_HOME/cleo/'),
|
|
101
|
+
`global-tier entry '${entry.role}' uses non-canonical path '${t}'`,
|
|
102
|
+
).toBe(true);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('resolves every non-null drizzleSchemaPath to a real file', () => {
|
|
108
|
+
for (const entry of DB_INVENTORY) {
|
|
109
|
+
if (entry.drizzleSchemaPath === null) continue;
|
|
110
|
+
const absolutePath = resolve(REPO_ROOT, entry.drizzleSchemaPath);
|
|
111
|
+
expect(
|
|
112
|
+
existsSync(absolutePath),
|
|
113
|
+
`role '${entry.role}' schema path missing: ${entry.drizzleSchemaPath}`,
|
|
114
|
+
).toBe(true);
|
|
115
|
+
expect(
|
|
116
|
+
statSync(absolutePath).isFile(),
|
|
117
|
+
`role '${entry.role}' schema path is not a file: ${entry.drizzleSchemaPath}`,
|
|
118
|
+
).toBe(true);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('resolves every non-null migrationsDir to a real directory', () => {
|
|
123
|
+
for (const entry of DB_INVENTORY) {
|
|
124
|
+
if (entry.migrationsDir === null) continue;
|
|
125
|
+
const absolutePath = resolve(REPO_ROOT, entry.migrationsDir);
|
|
126
|
+
expect(
|
|
127
|
+
existsSync(absolutePath),
|
|
128
|
+
`role '${entry.role}' migrations dir missing: ${entry.migrationsDir}`,
|
|
129
|
+
).toBe(true);
|
|
130
|
+
expect(
|
|
131
|
+
statSync(absolutePath).isDirectory(),
|
|
132
|
+
`role '${entry.role}' migrations path is not a directory: ${entry.migrationsDir}`,
|
|
133
|
+
).toBe(true);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('omits migrationsDir only for derived or reserved roles', () => {
|
|
138
|
+
// A null migrationsDir is only valid for:
|
|
139
|
+
// - derived tier (schema owned by upstream lib — e.g. manifest)
|
|
140
|
+
// - reserved roles whose opener throws not-yet-implemented (e.g. llmtxt)
|
|
141
|
+
for (const entry of DB_INVENTORY) {
|
|
142
|
+
if (entry.migrationsDir !== null) continue;
|
|
143
|
+
const isDerived = entry.tier === 'derived';
|
|
144
|
+
const isReserved = /reserved|not yet implemented/i.test(entry.openedVia);
|
|
145
|
+
expect(
|
|
146
|
+
isDerived || isReserved,
|
|
147
|
+
`role '${entry.role}' has null migrationsDir but is neither derived nor reserved`,
|
|
148
|
+
).toBe(true);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('exposes a readonly array type at compile time', () => {
|
|
153
|
+
// Compile-time check: assigning to an index must be rejected.
|
|
154
|
+
// Runtime check: Object.isFrozen is not guaranteed by `readonly` but the
|
|
155
|
+
// type-system reject is the primary defence. We only assert the runtime
|
|
156
|
+
// shape is array-like here.
|
|
157
|
+
expect(Array.isArray(DB_INVENTORY)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('keeps each entry self-consistent (sample-fields populated)', () => {
|
|
161
|
+
// Sanity: every entry must populate the structural fields. The contract
|
|
162
|
+
// forbids `null` placeholders on non-optional fields.
|
|
163
|
+
const sampleRequiredKeys: ReadonlyArray<keyof DbInventoryEntry> = [
|
|
164
|
+
'role',
|
|
165
|
+
'tier',
|
|
166
|
+
'filePathTemplate',
|
|
167
|
+
'ownerPackage',
|
|
168
|
+
'openedVia',
|
|
169
|
+
'concurrency',
|
|
170
|
+
'privacy',
|
|
171
|
+
'backupPath',
|
|
172
|
+
'documentedIn',
|
|
173
|
+
];
|
|
174
|
+
for (const entry of DB_INVENTORY) {
|
|
175
|
+
for (const key of sampleRequiredKeys) {
|
|
176
|
+
const value = entry[key];
|
|
177
|
+
expect(
|
|
178
|
+
typeof value === 'string' && value.length > 0,
|
|
179
|
+
`role '${entry.role}' field '${String(key)}' is empty or non-string`,
|
|
180
|
+
).toBe(true);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskType discriminator narrowing tests.
|
|
3
|
+
*
|
|
4
|
+
* Pins the post-ADR-083 §2.5 union shape (`'saga' | 'epic' | 'task' |
|
|
5
|
+
* 'subtask'`) so that:
|
|
6
|
+
* - Adding/removing a tier in the canonical {@link TaskType} union breaks
|
|
7
|
+
* this test loudly (caught at compile time AND runtime).
|
|
8
|
+
* - Discriminated-union narrowing on `task.type === 'saga'` keeps working
|
|
9
|
+
* for downstream consumers.
|
|
10
|
+
*
|
|
11
|
+
* @since SAGA T10326 · Epic T10277 · Task T10328 (W1.A)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
15
|
+
import type { Task, TaskType } from '../task.js';
|
|
16
|
+
|
|
17
|
+
describe('TaskType discriminator', () => {
|
|
18
|
+
it('narrows when type === "saga"', () => {
|
|
19
|
+
const t: Task = {
|
|
20
|
+
id: 'T1',
|
|
21
|
+
title: 'demo',
|
|
22
|
+
description: 'demo task',
|
|
23
|
+
status: 'pending',
|
|
24
|
+
priority: 'medium',
|
|
25
|
+
type: 'saga',
|
|
26
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
27
|
+
};
|
|
28
|
+
if (t.type === 'saga') {
|
|
29
|
+
expectTypeOf(t.type).toEqualTypeOf<'saga'>();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('narrows when type === "epic"', () => {
|
|
34
|
+
const t: Task = {
|
|
35
|
+
id: 'T2',
|
|
36
|
+
title: 'demo',
|
|
37
|
+
description: 'demo task',
|
|
38
|
+
status: 'pending',
|
|
39
|
+
priority: 'medium',
|
|
40
|
+
type: 'epic',
|
|
41
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
42
|
+
};
|
|
43
|
+
if (t.type === 'epic') {
|
|
44
|
+
expectTypeOf(t.type).toEqualTypeOf<'epic'>();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts all 4 tier values', () => {
|
|
49
|
+
const types: TaskType[] = ['saga', 'epic', 'task', 'subtask'];
|
|
50
|
+
expectTypeOf(types).toEqualTypeOf<TaskType[]>();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('TaskType is the 4-element union per ADR-083 §2.5', () => {
|
|
54
|
+
expectTypeOf<TaskType>().toEqualTypeOf<'saga' | 'epic' | 'task' | 'subtask'>();
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/brain.ts
CHANGED
|
@@ -96,3 +96,117 @@ export interface SupersededEntry {
|
|
|
96
96
|
/** Topic or category grouping these entries together. */
|
|
97
97
|
grouping: string;
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// T10303 — Brain.db auto-recovery (Saga T10281 / Epic T10286)
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Result of a brain.db auto-recovery attempt.
|
|
106
|
+
*
|
|
107
|
+
* Returned by `recoverMalformedBrainDb()` (T10303) when the chokepoint
|
|
108
|
+
* detects malformation via `ERR_SQLITE_ERROR errcode=11` or a failing
|
|
109
|
+
* `PRAGMA integrity_check`/`PRAGMA quick_check`. The corrupt DB has been
|
|
110
|
+
* moved to a quarantine directory and the freshest validated snapshot has
|
|
111
|
+
* been copied to `.cleo/brain.db`.
|
|
112
|
+
*
|
|
113
|
+
* Consumers (the brain.db open chokepoint, telemetry, the upcoming T10302
|
|
114
|
+
* regression test) read this structure to decide whether to re-attempt the
|
|
115
|
+
* open and to emit user-facing diagnostics with the data-loss window.
|
|
116
|
+
*/
|
|
117
|
+
export interface BrainRecoveryResult {
|
|
118
|
+
/** Absolute path to the snapshot that was restored, or null if recovery failed. */
|
|
119
|
+
restoredFrom: string | null;
|
|
120
|
+
/**
|
|
121
|
+
* Approximate hours between the snapshot's timestamp and the recovery
|
|
122
|
+
* event. `null` when the snapshot timestamp could not be parsed.
|
|
123
|
+
* Used in the recovery warning to make the data-loss window legible.
|
|
124
|
+
*/
|
|
125
|
+
dataLossWindowHours: number | null;
|
|
126
|
+
/**
|
|
127
|
+
* Number of `brain_observations` rows in the restored DB. Best-effort —
|
|
128
|
+
* `null` when the count query failed (table missing, FK violation, …).
|
|
129
|
+
*/
|
|
130
|
+
observationsRecovered: number | null;
|
|
131
|
+
/** `true` if the restored DB passes `PRAGMA quick_check`. */
|
|
132
|
+
integrityOK: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Absolute path to the quarantine directory where the corrupt DB plus
|
|
135
|
+
* its `-wal`/`-shm` sidecars were moved before restore.
|
|
136
|
+
*/
|
|
137
|
+
quarantineDir: string | null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================
|
|
141
|
+
// T10304 — `cleo backup recover brain` CLI verb (Saga T10281 / Epic T10286)
|
|
142
|
+
// ============================================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Per-table row counts probed from the restored brain.db.
|
|
146
|
+
*
|
|
147
|
+
* Best-effort — any individual count may be `null` when the table is
|
|
148
|
+
* missing (very old snapshot predating that schema rev) or when the count
|
|
149
|
+
* query throws. The values feed both the `cleo backup recover brain`
|
|
150
|
+
* envelope and the recovery runbook's user-facing diagnostics.
|
|
151
|
+
*
|
|
152
|
+
* @task T10304
|
|
153
|
+
* @epic T10286
|
|
154
|
+
* @saga T10281
|
|
155
|
+
*/
|
|
156
|
+
export interface BrainRecoveredRowCounts {
|
|
157
|
+
/** `brain_observations` row count, or `null` when the count failed. */
|
|
158
|
+
observations: number | null;
|
|
159
|
+
/** `brain_decisions` row count, or `null` when the count failed. */
|
|
160
|
+
decisions: number | null;
|
|
161
|
+
/** `brain_learnings` row count, or `null` when the count failed. */
|
|
162
|
+
learnings: number | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Envelope payload returned by `cleo backup recover brain`.
|
|
167
|
+
*
|
|
168
|
+
* The CLI verb (T10304) wraps {@link BrainRecoveryResult} from
|
|
169
|
+
* {@link recoverMalformedBrainDb} (T10303) and enriches it with per-table
|
|
170
|
+
* row counts plus the operator-friendly snapshot/quarantine paths required
|
|
171
|
+
* by the AC envelope shape.
|
|
172
|
+
*
|
|
173
|
+
* Surfaces:
|
|
174
|
+
* - `--dry-run` plan envelopes (with `restoredFrom` being the snapshot that
|
|
175
|
+
* *would* be restored and `quarantinedTo` being the directory the corrupt
|
|
176
|
+
* DB *would* be moved to).
|
|
177
|
+
* - Real-recovery envelopes after the pipeline has executed.
|
|
178
|
+
*
|
|
179
|
+
* @task T10304
|
|
180
|
+
* @epic T10286
|
|
181
|
+
* @saga T10281
|
|
182
|
+
*/
|
|
183
|
+
export interface BackupRecoverBrainResult {
|
|
184
|
+
/**
|
|
185
|
+
* Absolute path to the snapshot that was (or would be) restored. Empty
|
|
186
|
+
* string when no snapshot was available — the envelope's caller surfaces
|
|
187
|
+
* that as a failure mode.
|
|
188
|
+
*/
|
|
189
|
+
restoredFrom: string;
|
|
190
|
+
/** Per-table row counts probed from the restored DB. */
|
|
191
|
+
rowsRecovered: BrainRecoveredRowCounts;
|
|
192
|
+
/**
|
|
193
|
+
* Approximate hours between the snapshot's timestamp and the recovery
|
|
194
|
+
* event. `null` when the snapshot timestamp could not be parsed (e.g.
|
|
195
|
+
* legacy `brain.db.PRE-DUP-FIX-*` fallback artifact).
|
|
196
|
+
*/
|
|
197
|
+
dataLossWindowHours: number | null;
|
|
198
|
+
/** `true` when the restored DB passes `PRAGMA quick_check`. */
|
|
199
|
+
integrityOK: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Absolute path to the quarantine directory where the corrupt DB plus
|
|
202
|
+
* its `-wal`/`-shm` sidecars were moved. Empty string in `--dry-run`
|
|
203
|
+
* mode where no files were touched.
|
|
204
|
+
*/
|
|
205
|
+
quarantinedTo: string;
|
|
206
|
+
/**
|
|
207
|
+
* `true` when invoked with `--dry-run` — indicates the envelope is a
|
|
208
|
+
* plan, NOT a record of mutations performed. Consumers MUST check this
|
|
209
|
+
* before treating `restoredFrom` as a post-condition.
|
|
210
|
+
*/
|
|
211
|
+
dryRun: boolean;
|
|
212
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLEO Database Inventory — SSoT type contract + immutable constant.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This module exports the typed shape and the runtime constant for every
|
|
6
|
+
* SQLite database that CLEO manages. The runtime data is sourced from the
|
|
7
|
+
* canonical JSON inventory at `packages/core/src/store/db-inventory.json`
|
|
8
|
+
* via ESM JSON import (`with { type: 'json' }`). The JSON file is the
|
|
9
|
+
* single source of truth; this module re-exposes it with a typed contract.
|
|
10
|
+
*
|
|
11
|
+
* Downstream consumers:
|
|
12
|
+
*
|
|
13
|
+
* - **T10307 — Fleet survey** (`cleo doctor db-substrate`) iterates `DB_INVENTORY`
|
|
14
|
+
* to enumerate every DB on disk across project + global tiers.
|
|
15
|
+
* - **T10310 — Pragma drift** validates that every open handle for each role
|
|
16
|
+
* applies the pragma SSoT (`specs/sqlite-pragmas.json`).
|
|
17
|
+
* - **T10311 — Migration coverage** asserts that each entry's
|
|
18
|
+
* `migrationsDir` exists and contains a `0000_*` baseline.
|
|
19
|
+
* - **T10312 — Doctor integrity** runs `PRAGMA integrity_check` against
|
|
20
|
+
* every entry whose tier resolves to a present file on disk.
|
|
21
|
+
* - **T10320 — Cross-DB invariants** uses `ownerPackage` + `tier` to
|
|
22
|
+
* classify which DBs must agree on shared identifiers (project_id,
|
|
23
|
+
* agent_id, session_id, ...).
|
|
24
|
+
*
|
|
25
|
+
* Adding a new entry to the inventory is a charter amendment — update
|
|
26
|
+
* `db-inventory.json` AND `.cleo/adrs/ADR-068-cleo-database-charter.md`
|
|
27
|
+
* in the same PR (gated by `cleo check arch` Gate-4 SSoT-EXEMPT lint
|
|
28
|
+
* once T10282 ships its drift check).
|
|
29
|
+
*
|
|
30
|
+
* @see ADR-068 — CLEO Database Charter (canonical narrative)
|
|
31
|
+
* @see Saga T10281 — SG-BRAIN-DB-RESILIENCE (provenance)
|
|
32
|
+
* @see Epic T10282 — E1-DB-INVENTORY (this artifact)
|
|
33
|
+
* @see Research note `sg-brain-db-resilience-deep-audit-2026-05-23`
|
|
34
|
+
*
|
|
35
|
+
* @task T10305
|
|
36
|
+
* @epic T10282
|
|
37
|
+
* @saga T10281
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import dbInventoryData from '../../core/src/store/db-inventory.json' with { type: 'json' };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Canonical role names for every SQLite database in the CLEO charter.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* Mirrors the `openCleoDb('<role>', cwd)` chokepoint (ADR-068 §3) plus the
|
|
47
|
+
* three audit-discovered roles (`global-brain`, `global-tasks`, `manifest`)
|
|
48
|
+
* that exist on disk today but do not yet have a registered opener.
|
|
49
|
+
*
|
|
50
|
+
* - `tasks` — project-tier task engine DB
|
|
51
|
+
* - `brain` — project-tier memory DB (PII)
|
|
52
|
+
* - `conduit` — project-tier agent message transport
|
|
53
|
+
* - `manifest` — project-tier derived blob index (rebuildable)
|
|
54
|
+
* - `llmtxt` — reserved project-tier llmtxt session store
|
|
55
|
+
* - `nexus` — global-tier cross-project knowledge graph (PII)
|
|
56
|
+
* - `signaldock-project` — historical project-tier signaldock (post-ADR-037, merged into conduit)
|
|
57
|
+
* - `signaldock-global` — global-tier canonical agent identity registry
|
|
58
|
+
* - `telemetry` — global-tier opt-in command telemetry
|
|
59
|
+
* - `skills` — global-tier per-user skill registry
|
|
60
|
+
* - `global-brain` — global-tier brain.db observed on disk (audit §1.2 — provenance under investigation)
|
|
61
|
+
* - `global-tasks` — global-tier tasks.db observed on disk (audit §1.2 — provenance under investigation)
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export type DbRole =
|
|
66
|
+
| 'tasks'
|
|
67
|
+
| 'brain'
|
|
68
|
+
| 'conduit'
|
|
69
|
+
| 'manifest'
|
|
70
|
+
| 'llmtxt'
|
|
71
|
+
| 'nexus'
|
|
72
|
+
| 'signaldock-project'
|
|
73
|
+
| 'signaldock-global'
|
|
74
|
+
| 'telemetry'
|
|
75
|
+
| 'skills'
|
|
76
|
+
| 'global-brain'
|
|
77
|
+
| 'global-tasks';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Lifecycle scope of a CLEO database.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* - `project` — file lives under `<projectRoot>/.cleo/`; lifecycle bound to one CLEO project.
|
|
84
|
+
* - `global` — file lives under `$XDG_DATA_HOME/cleo/` (env-paths via `getCleoHome()`).
|
|
85
|
+
* - `derived` — file lives under the project tier but is rebuildable from a
|
|
86
|
+
* non-DB source-of-truth (e.g. content-addressable blob store).
|
|
87
|
+
* MAY be excluded from backup-pack.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
export type DbTier = 'project' | 'global' | 'derived';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Privacy classification governing backup + cloud-export rules.
|
|
95
|
+
*
|
|
96
|
+
* @remarks
|
|
97
|
+
* - `local-only` — never cloud-exported.
|
|
98
|
+
* - `local-only-pii` — never cloud-exported; PII-class — requires
|
|
99
|
+
* explicit `cleo backup export --include-pii`.
|
|
100
|
+
* - `cloud-exportable-opt-in` — may be exported only after explicit opt-in
|
|
101
|
+
* (e.g. `cleo diagnostics enable`).
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
export type DbPrivacy = 'local-only' | 'local-only-pii' | 'cloud-exportable-opt-in';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Concurrency model for the database.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* Every CLEO database today is `single-writer` (WAL mode with in-process
|
|
112
|
+
* serialization via the `openCleoDb` singleton). The field is enumerated so
|
|
113
|
+
* a future read-replica or multi-writer DB MUST be declared explicitly.
|
|
114
|
+
*
|
|
115
|
+
* @public
|
|
116
|
+
*/
|
|
117
|
+
export type DbConcurrency = 'single-writer' | 'multi-writer';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One row in the CLEO database charter.
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* Every field is required (no `null` placeholders) EXCEPT `drizzleSchemaPath`
|
|
124
|
+
* and `migrationsDir` — those MAY be `null` for:
|
|
125
|
+
*
|
|
126
|
+
* - `derived` databases whose schema is owned by an upstream library (e.g.
|
|
127
|
+
* `manifest.db` is owned by `llmtxt/blob`'s `BlobFsAdapter` contract).
|
|
128
|
+
* - `reserved` roles whose opener throws `not yet implemented` (e.g.
|
|
129
|
+
* `llmtxt.db`).
|
|
130
|
+
*
|
|
131
|
+
* Path templates use the substitution tokens:
|
|
132
|
+
*
|
|
133
|
+
* - `<projectRoot>` — resolved at runtime from `CLEO_ROOT` env var or
|
|
134
|
+
* `process.cwd()` via `getCleoProjectDir()`.
|
|
135
|
+
* - `$XDG_DATA_HOME` — resolved via env-paths through `getCleoHome()`.
|
|
136
|
+
* Linux: `~/.local/share/cleo/`; macOS:
|
|
137
|
+
* `~/Library/Application Support/cleo/`.
|
|
138
|
+
*
|
|
139
|
+
* @public
|
|
140
|
+
*/
|
|
141
|
+
export interface DbInventoryEntry {
|
|
142
|
+
/** Canonical role identifier used by `openCleoDb` and audit tooling. */
|
|
143
|
+
readonly role: DbRole;
|
|
144
|
+
/** Lifecycle scope — project, global, or derived. */
|
|
145
|
+
readonly tier: DbTier;
|
|
146
|
+
/**
|
|
147
|
+
* Filesystem path template with `<projectRoot>` / `$XDG_DATA_HOME` tokens.
|
|
148
|
+
* Substitution convention is documented on {@link DbInventoryEntry}.
|
|
149
|
+
*/
|
|
150
|
+
readonly filePathTemplate: string;
|
|
151
|
+
/**
|
|
152
|
+
* Repo-relative path to the Drizzle schema TypeScript file. `null` when
|
|
153
|
+
* the schema is owned by an upstream library or the role is reserved.
|
|
154
|
+
*/
|
|
155
|
+
readonly drizzleSchemaPath: string | null;
|
|
156
|
+
/**
|
|
157
|
+
* Repo-relative path to the Drizzle migrations directory (with trailing
|
|
158
|
+
* slash). `null` for `derived` rows or reserved roles. The path MUST
|
|
159
|
+
* exist on disk when non-null — enforced by the contracts test suite.
|
|
160
|
+
*/
|
|
161
|
+
readonly migrationsDir: string | null;
|
|
162
|
+
/** npm package that owns the open + lifecycle for this database. */
|
|
163
|
+
readonly ownerPackage: string;
|
|
164
|
+
/**
|
|
165
|
+
* Human-readable description of HOW the database is opened. Typically
|
|
166
|
+
* a code excerpt like `openCleoDb('tasks', cwd)`. For databases without
|
|
167
|
+
* a registered opener, describes provenance / status.
|
|
168
|
+
*/
|
|
169
|
+
readonly openedVia: string;
|
|
170
|
+
/** Concurrency model — see {@link DbConcurrency}. */
|
|
171
|
+
readonly concurrency: DbConcurrency;
|
|
172
|
+
/** Privacy classification — see {@link DbPrivacy}. */
|
|
173
|
+
readonly privacy: DbPrivacy;
|
|
174
|
+
/**
|
|
175
|
+
* Canonical backup path template. For `derived` rows this is the
|
|
176
|
+
* sentinel string `rebuildable-from-blob-store` (or similar) and the
|
|
177
|
+
* row is excluded from backup-pack staging.
|
|
178
|
+
*/
|
|
179
|
+
readonly backupPath: string;
|
|
180
|
+
/**
|
|
181
|
+
* Provenance pointers — ADR row, related task IDs, audit section
|
|
182
|
+
* references. Comma-or-semicolon separated string of references.
|
|
183
|
+
*/
|
|
184
|
+
readonly documentedIn: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Shape of the raw JSON SSoT — a header plus the typed entry array.
|
|
189
|
+
*
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
interface DbInventoryFile {
|
|
193
|
+
readonly $schemaNote: string;
|
|
194
|
+
readonly $pathTokens: Readonly<Record<string, string>>;
|
|
195
|
+
readonly entries: readonly DbInventoryEntry[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Narrow the raw JSON to the typed shape. The runtime parity test
|
|
199
|
+
// (`db-inventory.test.ts`) asserts every field is valid for every entry.
|
|
200
|
+
const rawInventory = dbInventoryData as DbInventoryFile;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Immutable canonical inventory of every CLEO SQLite database.
|
|
204
|
+
*
|
|
205
|
+
* @remarks
|
|
206
|
+
* Sourced from `packages/core/src/store/db-inventory.json` — the SSoT.
|
|
207
|
+
* Adding a new entry is a charter amendment per ADR-068 §"How to add a
|
|
208
|
+
* new database". The {@link db-inventory.test.ts} suite asserts:
|
|
209
|
+
*
|
|
210
|
+
* 1. Every `role` is unique across the array.
|
|
211
|
+
* 2. Every `drizzleSchemaPath` (when non-null) exists on disk.
|
|
212
|
+
* 3. Every `migrationsDir` (when non-null) exists on disk.
|
|
213
|
+
* 4. Every `tier` is a member of the {@link DbTier} union.
|
|
214
|
+
*
|
|
215
|
+
* @example Iterate every project-tier database
|
|
216
|
+
* ```typescript
|
|
217
|
+
* import { DB_INVENTORY } from '@cleocode/contracts';
|
|
218
|
+
*
|
|
219
|
+
* const projectDbs = DB_INVENTORY.filter((entry) => entry.tier === 'project');
|
|
220
|
+
* for (const entry of projectDbs) {
|
|
221
|
+
* console.log(entry.role, entry.filePathTemplate);
|
|
222
|
+
* }
|
|
223
|
+
* ```
|
|
224
|
+
*
|
|
225
|
+
* @public
|
|
226
|
+
*/
|
|
227
|
+
export const DB_INVENTORY: readonly DbInventoryEntry[] = rawInventory.entries;
|