@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.14
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +2 -1
- package/dist/derive.d.ts +6 -0
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +29 -5
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +156 -0
- package/dist/draft.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/topo-saves.d.ts +47 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +310 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +67 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +222 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +12 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +571 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- package/dist/topo-store.d.ts +48 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +175 -0
- package/dist/topo-store.js.map +1 -0
- package/dist/validate-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +5 -3
- package/dist/validate-topo.js.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/derive.test.ts +58 -1
- package/src/__tests__/topo-store-read.test.ts +251 -0
- package/src/__tests__/topo-store.test.ts +469 -0
- package/src/__tests__/trails-db.test.ts +191 -0
- package/src/__tests__/validate-topo.test.ts +167 -0
- package/src/derive.ts +39 -8
- package/src/draft.ts +334 -0
- package/src/index.ts +30 -1
- package/src/internal/topo-saves.ts +429 -0
- package/src/internal/topo-store-read.ts +473 -0
- package/src/internal/topo-store.ts +1087 -0
- package/src/internal/trails-db.ts +189 -0
- package/src/topo-store.ts +301 -0
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +7 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { NotFoundError } from '../errors.js';
|
|
6
|
+
|
|
7
|
+
const TRAILS_DIR = '.trails';
|
|
8
|
+
const TRAILS_DB_FILE = 'trails.db';
|
|
9
|
+
const SCHEMA_VERSION_TABLE = 'meta_schema_versions';
|
|
10
|
+
const WORKSPACE_SUBDIRS = ['config', 'dev', 'generated'] as const;
|
|
11
|
+
const REQUIRED_GITIGNORE_LINES = [
|
|
12
|
+
'# Local config overrides',
|
|
13
|
+
'config/',
|
|
14
|
+
'',
|
|
15
|
+
'# Development state',
|
|
16
|
+
'dev/',
|
|
17
|
+
'',
|
|
18
|
+
'# Generated artifacts',
|
|
19
|
+
'generated/',
|
|
20
|
+
'',
|
|
21
|
+
'# Shared Trails database',
|
|
22
|
+
'trails.db',
|
|
23
|
+
'trails.db-shm',
|
|
24
|
+
'trails.db-wal',
|
|
25
|
+
'',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export interface TrailsDbLocationOptions {
|
|
29
|
+
readonly path?: string;
|
|
30
|
+
readonly rootDir?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EnsureSubsystemSchemaOptions {
|
|
34
|
+
readonly migrate: (currentVersion: number) => void;
|
|
35
|
+
readonly subsystem: string;
|
|
36
|
+
readonly version: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SchemaVersionRow {
|
|
40
|
+
readonly version: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolveRootDir = (rootDir?: string): string =>
|
|
44
|
+
resolve(rootDir ?? process.cwd());
|
|
45
|
+
|
|
46
|
+
export const resolveTrailsDir = (options?: TrailsDbLocationOptions): string =>
|
|
47
|
+
join(resolveRootDir(options?.rootDir), TRAILS_DIR);
|
|
48
|
+
|
|
49
|
+
export const resolveTrailsDbPath = (
|
|
50
|
+
options?: TrailsDbLocationOptions
|
|
51
|
+
): string =>
|
|
52
|
+
options?.path
|
|
53
|
+
? resolve(options.path)
|
|
54
|
+
: join(resolveTrailsDir(options), TRAILS_DB_FILE);
|
|
55
|
+
|
|
56
|
+
const ensureDbParentDir = (dbPath: string): void => {
|
|
57
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const GITIGNORE_TEMPLATE = `${REQUIRED_GITIGNORE_LINES.join('\n').trimEnd()}\n`;
|
|
61
|
+
|
|
62
|
+
const appendMissingGitignoreLines = (
|
|
63
|
+
gitignorePath: string,
|
|
64
|
+
content: string
|
|
65
|
+
): void => {
|
|
66
|
+
const existingLines = new Set(content.split('\n').map((l) => l.trim()));
|
|
67
|
+
const missing = REQUIRED_GITIGNORE_LINES.filter(
|
|
68
|
+
(line) => line !== '' && !existingLines.has(line)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (missing.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const next = `${content.trimEnd()}\n\n${missing.join('\n')}`;
|
|
76
|
+
writeFileSync(gitignorePath, `${next.trimEnd()}\n`);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const ensureWorkspaceGitignore = (trailsDir: string): void => {
|
|
80
|
+
const gitignorePath = join(trailsDir, '.gitignore');
|
|
81
|
+
|
|
82
|
+
if (!existsSync(gitignorePath)) {
|
|
83
|
+
writeFileSync(gitignorePath, GITIGNORE_TEMPLATE);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
appendMissingGitignoreLines(
|
|
88
|
+
gitignorePath,
|
|
89
|
+
readFileSync(gitignorePath, 'utf8')
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const ensureWorkspaceDir = (rootDir: string): void => {
|
|
94
|
+
const trailsDir = resolveTrailsDir({ rootDir });
|
|
95
|
+
mkdirSync(trailsDir, { recursive: true });
|
|
96
|
+
for (const subdir of WORKSPACE_SUBDIRS) {
|
|
97
|
+
mkdirSync(join(trailsDir, subdir), { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
ensureWorkspaceGitignore(trailsDir);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const initializeWritePragmas = (db: Database): void => {
|
|
103
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
104
|
+
db.run('PRAGMA synchronous = NORMAL');
|
|
105
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const ensureSchemaVersionTable = (db: Database): void => {
|
|
109
|
+
db.run(`CREATE TABLE IF NOT EXISTS ${SCHEMA_VERSION_TABLE} (
|
|
110
|
+
subsystem TEXT PRIMARY KEY,
|
|
111
|
+
version INTEGER NOT NULL,
|
|
112
|
+
updated_at TEXT NOT NULL
|
|
113
|
+
)`);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const readSubsystemVersion = (db: Database, subsystem: string): number => {
|
|
117
|
+
const row = db
|
|
118
|
+
.query<SchemaVersionRow, [string]>(
|
|
119
|
+
`SELECT version FROM ${SCHEMA_VERSION_TABLE} WHERE subsystem = ?`
|
|
120
|
+
)
|
|
121
|
+
.get(subsystem);
|
|
122
|
+
return row?.version ?? 0;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const writeSubsystemVersion = (
|
|
126
|
+
db: Database,
|
|
127
|
+
subsystem: string,
|
|
128
|
+
version: number
|
|
129
|
+
): void => {
|
|
130
|
+
db.run(
|
|
131
|
+
`INSERT INTO ${SCHEMA_VERSION_TABLE} (subsystem, version, updated_at)
|
|
132
|
+
VALUES (?, ?, ?)
|
|
133
|
+
ON CONFLICT(subsystem) DO UPDATE SET
|
|
134
|
+
version = excluded.version,
|
|
135
|
+
updated_at = excluded.updated_at`,
|
|
136
|
+
[subsystem, version, new Date().toISOString()]
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const openWriteTrailsDb = (
|
|
141
|
+
options?: TrailsDbLocationOptions
|
|
142
|
+
): Database => {
|
|
143
|
+
const rootDir = resolveRootDir(options?.rootDir);
|
|
144
|
+
const dbPath = resolveTrailsDbPath(
|
|
145
|
+
options?.path ? { path: options.path, rootDir } : { rootDir }
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (options?.path === undefined) {
|
|
149
|
+
ensureWorkspaceDir(rootDir);
|
|
150
|
+
} else {
|
|
151
|
+
ensureDbParentDir(dbPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const db = new Database(dbPath, { create: true });
|
|
155
|
+
initializeWritePragmas(db);
|
|
156
|
+
ensureSchemaVersionTable(db);
|
|
157
|
+
return db;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const openReadTrailsDb = (
|
|
161
|
+
options?: TrailsDbLocationOptions
|
|
162
|
+
): Database => {
|
|
163
|
+
const dbPath = resolveTrailsDbPath(options);
|
|
164
|
+
if (!existsSync(dbPath)) {
|
|
165
|
+
throw new NotFoundError(
|
|
166
|
+
`Trails database not found at "${dbPath}". Run a write operation first to initialize it.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const db = new Database(dbPath, { readonly: true });
|
|
170
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
171
|
+
return db;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const ensureSubsystemSchema = (
|
|
175
|
+
db: Database,
|
|
176
|
+
options: EnsureSubsystemSchemaOptions
|
|
177
|
+
): void => {
|
|
178
|
+
ensureSchemaVersionTable(db);
|
|
179
|
+
|
|
180
|
+
db.transaction(() => {
|
|
181
|
+
const currentVersion = readSubsystemVersion(db, options.subsystem);
|
|
182
|
+
if (currentVersion >= options.version) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
options.migrate(currentVersion);
|
|
187
|
+
writeSubsystemVersion(db, options.subsystem, options.version);
|
|
188
|
+
})();
|
|
189
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { SQLQueryBindings } from 'bun:sqlite';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { NotFoundError } from './errors.js';
|
|
5
|
+
import { provision } from './provision.js';
|
|
6
|
+
import { Result } from './result.js';
|
|
7
|
+
import type { TopoPinRecord, TopoSaveRecord } from './internal/topo-saves.js';
|
|
8
|
+
import { getTopoPin } from './internal/topo-saves.js';
|
|
9
|
+
import type {
|
|
10
|
+
TopoStoreExportRecord,
|
|
11
|
+
TopoStoreProvisionRecord,
|
|
12
|
+
TopoStoreRef,
|
|
13
|
+
TopoStoreTrailDetailRecord,
|
|
14
|
+
TopoStoreTrailRecord,
|
|
15
|
+
} from './internal/topo-store-read.js';
|
|
16
|
+
import {
|
|
17
|
+
getTopoStoreExport,
|
|
18
|
+
getTopoStoreProvision,
|
|
19
|
+
getTopoStoreTrail,
|
|
20
|
+
listTopoStorePins,
|
|
21
|
+
listTopoStoreProvisions,
|
|
22
|
+
listTopoStoreSaves,
|
|
23
|
+
listTopoStoreTrails,
|
|
24
|
+
queryTopoStore,
|
|
25
|
+
resolveTopoStoreSave,
|
|
26
|
+
} from './internal/topo-store-read.js';
|
|
27
|
+
import { openReadTrailsDb, resolveTrailsDbPath } from './internal/trails-db.js';
|
|
28
|
+
import type { TrailsDbLocationOptions } from './internal/trails-db.js';
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
TopoStoreExportRecord,
|
|
32
|
+
TopoStoreProvisionRecord,
|
|
33
|
+
TopoStoreRef,
|
|
34
|
+
TopoStoreTrailDetailRecord,
|
|
35
|
+
TopoStoreTrailRecord,
|
|
36
|
+
} from './internal/topo-store-read.js';
|
|
37
|
+
|
|
38
|
+
export interface ReadOnlyTopoStore {
|
|
39
|
+
readonly exports: {
|
|
40
|
+
get(ref?: TopoStoreRef): TopoStoreExportRecord | undefined;
|
|
41
|
+
};
|
|
42
|
+
readonly pins: {
|
|
43
|
+
get(name: string): TopoPinRecord | undefined;
|
|
44
|
+
list(): readonly TopoPinRecord[];
|
|
45
|
+
};
|
|
46
|
+
query<TRow extends Record<string, unknown>>(
|
|
47
|
+
sql: string,
|
|
48
|
+
bindings?: readonly SQLQueryBindings[]
|
|
49
|
+
): readonly TRow[];
|
|
50
|
+
readonly provisions: {
|
|
51
|
+
get(
|
|
52
|
+
id: string,
|
|
53
|
+
options?: { readonly save?: TopoStoreRef }
|
|
54
|
+
): TopoStoreProvisionRecord | undefined;
|
|
55
|
+
list(options?: {
|
|
56
|
+
readonly save?: TopoStoreRef;
|
|
57
|
+
}): readonly TopoStoreProvisionRecord[];
|
|
58
|
+
};
|
|
59
|
+
readonly saves: {
|
|
60
|
+
get(ref?: TopoStoreRef): TopoSaveRecord | undefined;
|
|
61
|
+
latest(): TopoSaveRecord | undefined;
|
|
62
|
+
list(): readonly TopoSaveRecord[];
|
|
63
|
+
};
|
|
64
|
+
readonly trails: {
|
|
65
|
+
get(
|
|
66
|
+
id: string,
|
|
67
|
+
options?: { readonly save?: TopoStoreRef }
|
|
68
|
+
): TopoStoreTrailDetailRecord | undefined;
|
|
69
|
+
list(options?: {
|
|
70
|
+
readonly intent?: TopoStoreTrailRecord['intent'];
|
|
71
|
+
readonly save?: TopoStoreRef;
|
|
72
|
+
}): readonly TopoStoreTrailRecord[];
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface MockTopoStoreSeed {
|
|
77
|
+
readonly exports?: readonly TopoStoreExportRecord[];
|
|
78
|
+
readonly pins?: readonly TopoPinRecord[];
|
|
79
|
+
readonly provisions?: readonly TopoStoreProvisionRecord[];
|
|
80
|
+
readonly saves?: readonly TopoSaveRecord[];
|
|
81
|
+
readonly trails?: readonly TopoStoreTrailDetailRecord[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const missingStoreMessage =
|
|
85
|
+
'No saved topo state found. Populate trails.db first or run a topo-backed surface.';
|
|
86
|
+
|
|
87
|
+
const resolveStoreRootDir = (options?: TrailsDbLocationOptions): string =>
|
|
88
|
+
options?.rootDir ?? process.cwd();
|
|
89
|
+
|
|
90
|
+
const requireReadDb = (
|
|
91
|
+
options?: TrailsDbLocationOptions
|
|
92
|
+
): ReturnType<typeof openReadTrailsDb> => {
|
|
93
|
+
const dbPath = resolveTrailsDbPath(options);
|
|
94
|
+
if (!existsSync(dbPath)) {
|
|
95
|
+
throw new NotFoundError(missingStoreMessage);
|
|
96
|
+
}
|
|
97
|
+
return openReadTrailsDb(options);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const requireSavedTopoState = (
|
|
101
|
+
db: ReturnType<typeof openReadTrailsDb>
|
|
102
|
+
): void => {
|
|
103
|
+
if (resolveTopoStoreSave(db) === undefined) {
|
|
104
|
+
throw new NotFoundError(missingStoreMessage);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const withStoredTopoState = <T>(
|
|
109
|
+
options: TrailsDbLocationOptions | undefined,
|
|
110
|
+
run: (db: ReturnType<typeof openReadTrailsDb>) => T
|
|
111
|
+
): T => {
|
|
112
|
+
const db = requireReadDb(options);
|
|
113
|
+
try {
|
|
114
|
+
requireSavedTopoState(db);
|
|
115
|
+
return run(db);
|
|
116
|
+
} finally {
|
|
117
|
+
db.close();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const createSeedResolver = (seed?: MockTopoStoreSeed) => {
|
|
122
|
+
const saves = [...(seed?.saves ?? [])];
|
|
123
|
+
const pins = [...(seed?.pins ?? [])];
|
|
124
|
+
const trails = [...(seed?.trails ?? [])];
|
|
125
|
+
const provisions = [...(seed?.provisions ?? [])];
|
|
126
|
+
const exports = [...(seed?.exports ?? [])];
|
|
127
|
+
|
|
128
|
+
const resolveSave = (ref?: TopoStoreRef): TopoSaveRecord | undefined => {
|
|
129
|
+
if (ref?.saveId !== undefined) {
|
|
130
|
+
return saves.find((save) => save.id === ref.saveId);
|
|
131
|
+
}
|
|
132
|
+
if (ref?.pin !== undefined) {
|
|
133
|
+
const pin = pins.find((candidate) => candidate.name === ref.pin);
|
|
134
|
+
return pin === undefined
|
|
135
|
+
? undefined
|
|
136
|
+
: saves.find((save) => save.id === pin.saveId);
|
|
137
|
+
}
|
|
138
|
+
return saves[0];
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
exports,
|
|
143
|
+
pins,
|
|
144
|
+
provisions,
|
|
145
|
+
resolveSave,
|
|
146
|
+
saves,
|
|
147
|
+
trails,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const createMockTopoStore = (
|
|
152
|
+
seed?: MockTopoStoreSeed
|
|
153
|
+
): ReadOnlyTopoStore => {
|
|
154
|
+
const resolved = createSeedResolver(seed);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
exports: {
|
|
158
|
+
get(ref?: TopoStoreRef) {
|
|
159
|
+
const save = resolved.resolveSave(ref);
|
|
160
|
+
return save === undefined
|
|
161
|
+
? undefined
|
|
162
|
+
: resolved.exports.find((entry) => entry.save.id === save.id);
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
pins: {
|
|
166
|
+
get(name: string) {
|
|
167
|
+
return resolved.pins.find((pin) => pin.name === name);
|
|
168
|
+
},
|
|
169
|
+
list() {
|
|
170
|
+
return resolved.pins;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
provisions: {
|
|
174
|
+
get(id, options) {
|
|
175
|
+
const save = resolved.resolveSave(options?.save);
|
|
176
|
+
return resolved.provisions.find(
|
|
177
|
+
(item) =>
|
|
178
|
+
item.id === id && (save === undefined || item.saveId === save.id)
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
list(options) {
|
|
182
|
+
const save = resolved.resolveSave(options?.save);
|
|
183
|
+
return save === undefined
|
|
184
|
+
? []
|
|
185
|
+
: resolved.provisions.filter((item) => item.saveId === save.id);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
query() {
|
|
189
|
+
throw new NotFoundError(
|
|
190
|
+
'Mock topoStore.query() is unsupported. Seed typed accessors instead.'
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
saves: {
|
|
194
|
+
get(ref?: TopoStoreRef) {
|
|
195
|
+
return resolved.resolveSave(ref);
|
|
196
|
+
},
|
|
197
|
+
latest() {
|
|
198
|
+
return resolved.saves[0];
|
|
199
|
+
},
|
|
200
|
+
list() {
|
|
201
|
+
return resolved.saves;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
trails: {
|
|
205
|
+
get(id, options) {
|
|
206
|
+
const save = resolved.resolveSave(options?.save);
|
|
207
|
+
if (save === undefined) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
return resolved.trails.find(
|
|
211
|
+
(trail) => trail.id === id && trail.saveId === save.id
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
list(options) {
|
|
215
|
+
const save = resolved.resolveSave(options?.save);
|
|
216
|
+
if (save === undefined) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
return resolved.trails.filter(
|
|
220
|
+
(trail) =>
|
|
221
|
+
trail.saveId === save.id &&
|
|
222
|
+
(options?.intent === undefined || trail.intent === options.intent)
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const createTopoStore = (
|
|
230
|
+
options?: TrailsDbLocationOptions
|
|
231
|
+
): ReadOnlyTopoStore => ({
|
|
232
|
+
exports: {
|
|
233
|
+
get(ref?: TopoStoreRef) {
|
|
234
|
+
return withStoredTopoState(options, (db) => getTopoStoreExport(db, ref));
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
pins: {
|
|
238
|
+
get(name: string) {
|
|
239
|
+
return withStoredTopoState(options, (db) => getTopoPin(db, name));
|
|
240
|
+
},
|
|
241
|
+
list() {
|
|
242
|
+
return withStoredTopoState(options, (db) => listTopoStorePins(db));
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
provisions: {
|
|
246
|
+
get(id, queryOptions) {
|
|
247
|
+
return withStoredTopoState(options, (db) =>
|
|
248
|
+
getTopoStoreProvision(db, id, queryOptions)
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
list(queryOptions) {
|
|
252
|
+
return withStoredTopoState(options, (db) =>
|
|
253
|
+
listTopoStoreProvisions(db, queryOptions)
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
query<TRow extends Record<string, unknown>>(
|
|
258
|
+
sql: string,
|
|
259
|
+
bindings?: readonly SQLQueryBindings[]
|
|
260
|
+
) {
|
|
261
|
+
return withStoredTopoState(options, (db) =>
|
|
262
|
+
queryTopoStore<TRow>(db, sql, bindings)
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
saves: {
|
|
266
|
+
get(ref?: TopoStoreRef) {
|
|
267
|
+
return withStoredTopoState(options, (db) =>
|
|
268
|
+
resolveTopoStoreSave(db, ref)
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
latest() {
|
|
272
|
+
return withStoredTopoState(options, (db) => resolveTopoStoreSave(db));
|
|
273
|
+
},
|
|
274
|
+
list() {
|
|
275
|
+
return withStoredTopoState(options, (db) => listTopoStoreSaves(db));
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
trails: {
|
|
279
|
+
get(id, queryOptions) {
|
|
280
|
+
return withStoredTopoState(options, (db) =>
|
|
281
|
+
getTopoStoreTrail(db, id, queryOptions)
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
list(queryOptions) {
|
|
285
|
+
return withStoredTopoState(options, (db) =>
|
|
286
|
+
listTopoStoreTrails(db, queryOptions)
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
export const topoStore = provision('topo.store', {
|
|
293
|
+
create: (svc) =>
|
|
294
|
+
Result.ok(
|
|
295
|
+
createTopoStore({
|
|
296
|
+
rootDir: svc.workspaceRoot ?? svc.cwd ?? resolveStoreRootDir(),
|
|
297
|
+
})
|
|
298
|
+
),
|
|
299
|
+
description: 'Read-only query access to saved topo state in trails.db',
|
|
300
|
+
mock: () => createMockTopoStore(),
|
|
301
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ValidationError } from './errors.js';
|
|
2
|
+
import { Result } from './result.js';
|
|
3
|
+
import type { Topo } from './topo.js';
|
|
4
|
+
import { validateEstablishedTopo as validateDraftFreeTopo } from './draft.js';
|
|
5
|
+
import type { TopoIssue } from './validate-topo.js';
|
|
6
|
+
import { validateTopo } from './validate-topo.js';
|
|
7
|
+
|
|
8
|
+
const PROJECTION_BLOCKING_RULES = new Set([
|
|
9
|
+
'cross-cycle',
|
|
10
|
+
'cross-exists',
|
|
11
|
+
'no-self-cross',
|
|
12
|
+
'provision-exists',
|
|
13
|
+
'signal-origin-exists',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const keepProjectionBlockingIssues = (
|
|
17
|
+
result: ReturnType<typeof validateTopo>
|
|
18
|
+
) => {
|
|
19
|
+
if (result.isOk()) {
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const issues = (
|
|
24
|
+
result.error.context as { issues?: readonly TopoIssue[] } | undefined
|
|
25
|
+
)?.issues;
|
|
26
|
+
const remainingIssues = issues?.filter((issue) =>
|
|
27
|
+
PROJECTION_BLOCKING_RULES.has(issue.rule)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (remainingIssues === undefined || remainingIssues.length === 0) {
|
|
31
|
+
return Result.ok();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return Result.err(
|
|
35
|
+
new ValidationError(
|
|
36
|
+
`Topo validation failed with ${remainingIssues.length} issue(s)`,
|
|
37
|
+
{
|
|
38
|
+
cause: result.error,
|
|
39
|
+
context: { issues: remainingIssues },
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate that a topo is ready for established outputs.
|
|
47
|
+
*
|
|
48
|
+
* Established surfaces still require the authored graph to be structurally
|
|
49
|
+
* valid, and they must also reject any remaining draft state.
|
|
50
|
+
*/
|
|
51
|
+
export const validateEstablishedTopo = (topo: Topo) => {
|
|
52
|
+
const structural = keepProjectionBlockingIssues(validateTopo(topo));
|
|
53
|
+
if (structural.isErr()) {
|
|
54
|
+
return structural;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const established = validateDraftFreeTopo(topo);
|
|
58
|
+
if (established.isErr()) {
|
|
59
|
+
return established;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Result.ok();
|
|
63
|
+
};
|
package/src/validate-topo.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ValidationError } from './errors.js';
|
|
10
|
+
import { isDraftId } from './draft.js';
|
|
10
11
|
import type { AnySignal } from './event.js';
|
|
11
12
|
import { Result } from './result.js';
|
|
12
13
|
import type { Topo } from './topo.js';
|
|
@@ -100,7 +101,7 @@ const checkCrosses = (
|
|
|
100
101
|
rule: 'no-self-cross',
|
|
101
102
|
trailId: id,
|
|
102
103
|
});
|
|
103
|
-
} else if (!topo.has(crossedId)) {
|
|
104
|
+
} else if (!topo.has(crossedId) && !isDraftId(crossedId)) {
|
|
104
105
|
issues.push({
|
|
105
106
|
message: `Crosses "${crossedId}" which is not in the topo`,
|
|
106
107
|
rule: 'cross-exists',
|
|
@@ -121,7 +122,10 @@ const checkProvisions = (
|
|
|
121
122
|
|
|
122
123
|
for (const [id, trail] of trails) {
|
|
123
124
|
for (const declaredProvision of trail.provisions) {
|
|
124
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
!topo.hasProvision(declaredProvision.id) &&
|
|
127
|
+
!isDraftId(declaredProvision.id)
|
|
128
|
+
) {
|
|
125
129
|
issues.push({
|
|
126
130
|
message: `Provision "${declaredProvision.id}" is not in the topo`,
|
|
127
131
|
rule: 'provision-exists',
|
|
@@ -187,7 +191,7 @@ const checkSignalOrigins = (
|
|
|
187
191
|
continue;
|
|
188
192
|
}
|
|
189
193
|
for (const originId of evt.from) {
|
|
190
|
-
if (!topo.has(originId)) {
|
|
194
|
+
if (!topo.has(originId) && !isDraftId(originId)) {
|
|
191
195
|
issues.push({
|
|
192
196
|
message: `Signal origin "${originId}" is not in the topo`,
|
|
193
197
|
rule: 'signal-origin-exists',
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/blob-ref.ts","./src/branded.ts","./src/collections.ts","./src/context.ts","./src/derive.ts","./src/errors.ts","./src/event.ts","./src/execute.ts","./src/fetch.ts","./src/gate.ts","./src/guards.ts","./src/index.ts","./src/path-security.ts","./src/provision-config.ts","./src/provision.ts","./src/resilience.ts","./src/result.ts","./src/run.ts","./src/serialization.ts","./src/signal.ts","./src/topo.ts","./src/trail.ts","./src/type-utils.ts","./src/types.ts","./src/validate-topo.ts","./src/validation.ts","./src/workspace.ts","./src/patterns/bulk.ts","./src/patterns/change.ts","./src/patterns/date-range.ts","./src/patterns/index.ts","./src/patterns/pagination.ts","./src/patterns/progress.ts","./src/patterns/sorting.ts","./src/patterns/status.ts","./src/patterns/timestamps.ts","./src/redaction/index.ts","./src/redaction/patterns.ts","./src/redaction/redactor.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/blob-ref.ts","./src/branded.ts","./src/collections.ts","./src/context.ts","./src/derive.ts","./src/draft.ts","./src/errors.ts","./src/event.ts","./src/execute.ts","./src/fetch.ts","./src/gate.ts","./src/guards.ts","./src/index.ts","./src/path-security.ts","./src/provision-config.ts","./src/provision.ts","./src/resilience.ts","./src/result.ts","./src/run.ts","./src/serialization.ts","./src/signal.ts","./src/topo-store.ts","./src/topo.ts","./src/trail.ts","./src/type-utils.ts","./src/types.ts","./src/validate-established-topo.ts","./src/validate-topo.ts","./src/validation.ts","./src/workspace.ts","./src/internal/topo-saves.ts","./src/internal/topo-store-read.ts","./src/internal/topo-store.ts","./src/internal/trails-db.ts","./src/patterns/bulk.ts","./src/patterns/change.ts","./src/patterns/date-range.ts","./src/patterns/index.ts","./src/patterns/pagination.ts","./src/patterns/progress.ts","./src/patterns/sorting.ts","./src/patterns/status.ts","./src/patterns/timestamps.ts","./src/redaction/index.ts","./src/redaction/patterns.ts","./src/redaction/redactor.ts"],"version":"5.9.3"}
|