omgbase 0.26.3 → 0.28.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/dist/build-info.json +2 -2
- package/dist/src/cmd/_embed.d.ts +8 -3
- package/dist/src/cmd/_embed.js +41 -25
- package/dist/src/cmd/admin.js +164 -18
- package/dist/src/cmd/diff.js +16 -7
- package/dist/src/cmd/edit.js +1 -1
- package/dist/src/cmd/hist.js +3 -2
- package/dist/src/cmd/history.js +102 -11
- package/dist/src/cmd/log.js +19 -6
- package/dist/src/cmd/mcp.js +14 -11
- package/dist/src/cmd/mutate.js +2 -2
- package/dist/src/cmd/oqx.js +3 -1
- package/dist/src/cmd/run.js +2 -2
- package/dist/src/cmd/show.js +3 -2
- package/dist/src/cmd/source.js +5 -3
- package/dist/src/cmd/status.js +13 -10
- package/dist/src/cmd/version.js +20 -3
- package/dist/src/dispatch.js +27 -2
- package/package.json +3 -3
package/dist/build-info.json
CHANGED
package/dist/src/cmd/_embed.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmbeddingWorker, EngineError, type EmbeddingProvider } from "@omgbase/core";
|
|
1
|
+
import { EmbeddingWorker, EngineError, type EmbeddingProvider, type DrainMode } from "@omgbase/core";
|
|
2
2
|
import type { Cli } from "../context.js";
|
|
3
3
|
export interface LoadedEmbedding {
|
|
4
4
|
worker: EmbeddingWorker;
|
|
@@ -19,7 +19,9 @@ export interface LoadedEmbedding {
|
|
|
19
19
|
* embedder is never mistaken for an unconfigured one. Callers MUST call close()
|
|
20
20
|
* when done (e.g. in a finally) to release a spawned process.
|
|
21
21
|
*/
|
|
22
|
-
export declare function loadEmbedding(ws: ReturnType<Cli["workspace"]>, repoId: string
|
|
22
|
+
export declare function loadEmbedding(ws: ReturnType<Cli["workspace"]>, repoId: string, opts?: {
|
|
23
|
+
switchedOff?: "null" | "throw";
|
|
24
|
+
}): Promise<LoadedEmbedding | null>;
|
|
23
25
|
/** Build the shared `embedder_failed` error: a configured embedder that can't be
|
|
24
26
|
* reached is a hard, clearly-diagnosed fault (not `semantic_unavailable`, which
|
|
25
27
|
* means "no provider configured"). */
|
|
@@ -48,7 +50,10 @@ export declare function drainEmbeddings(cli: Cli, ws: ReturnType<Cli["workspace"
|
|
|
48
50
|
}): Promise<{
|
|
49
51
|
embedded: number;
|
|
50
52
|
cached: number;
|
|
51
|
-
|
|
53
|
+
mode: DrainMode;
|
|
54
|
+
processed_seq: number | null;
|
|
55
|
+
head: number | null;
|
|
56
|
+
pruned: {
|
|
52
57
|
blocks: number;
|
|
53
58
|
docs: number;
|
|
54
59
|
};
|
package/dist/src/cmd/_embed.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmbeddingWorker, buildEmbedTasks,
|
|
1
|
+
import { EmbeddingWorker, buildEmbedTasks, embeddingSettings, createExternalProvider, resolveSettings, requireSemantic, semanticSwitchedOff, CapabilityUnavailable, EngineError } from "@omgbase/core";
|
|
2
2
|
/**
|
|
3
3
|
* Connect the configured embedding provider + build a worker for a repo, or
|
|
4
4
|
* null when embedding.provider is UNSET (callers surface semantic_unavailable).
|
|
@@ -8,10 +8,25 @@ import { EmbeddingWorker, buildEmbedTasks, buildDocEmbedTasks, embeddingSettings
|
|
|
8
8
|
* embedder is never mistaken for an unconfigured one. Callers MUST call close()
|
|
9
9
|
* when done (e.g. in a finally) to release a spawned process.
|
|
10
10
|
*/
|
|
11
|
-
export async function loadEmbedding(ws, repoId) {
|
|
11
|
+
export async function loadEmbedding(ws, repoId, opts = {}) {
|
|
12
12
|
const settings = embeddingSettings(resolveSettings(ws.store, repoId));
|
|
13
13
|
if (!settings.provider)
|
|
14
14
|
return null;
|
|
15
|
+
// The row switched off by `omg embed disable` while the provider stays
|
|
16
|
+
// configured (spec/store §5.10, Phase 4b): an opportunistic caller (`find`,
|
|
17
|
+
// `omg mcp`'s host) gets the quiet null — FTS-only, nothing is spawned; an
|
|
18
|
+
// explicit one (`embed status|drain`) is refused with the fix `omg embed enable`.
|
|
19
|
+
if (semanticSwitchedOff(ws.store.db, repoId)) {
|
|
20
|
+
if (opts.switchedOff === "throw")
|
|
21
|
+
throw CapabilityUnavailable.semanticSwitchedOff();
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
// A configured provider on a store that runs WITHOUT the semantic module
|
|
25
|
+
// (`OMGBASE_CAPABILITIES=core`) cannot embed or rank: `capability_unavailable
|
|
26
|
+
// { semantic, disabled }` (modular-core Phase 2). An unconfigured one stays
|
|
27
|
+
// the quiet null above on every composition.
|
|
28
|
+
if (!ws.store.capabilities.semantic)
|
|
29
|
+
throw CapabilityUnavailable.semanticDisabled();
|
|
15
30
|
let ext;
|
|
16
31
|
try {
|
|
17
32
|
ext = await createExternalProvider(settings);
|
|
@@ -51,8 +66,7 @@ export async function drainEmbeddings(cli, ws, repoId, opts = {}) {
|
|
|
51
66
|
if (!loaded)
|
|
52
67
|
return null;
|
|
53
68
|
try {
|
|
54
|
-
const
|
|
55
|
-
const pending = loaded.worker.staleBlocks(tasks);
|
|
69
|
+
const pending = loaded.worker.staleBlocks(buildEmbedTasks(ws.store, repoId));
|
|
56
70
|
if (opts.confirm) {
|
|
57
71
|
const ok = await opts.confirm({ pending: pending.length, remote: loaded.remote, providerName: loaded.providerName });
|
|
58
72
|
if (!ok)
|
|
@@ -66,34 +80,36 @@ export async function drainEmbeddings(cli, ws, repoId, opts = {}) {
|
|
|
66
80
|
cli.io.err(cli.style.dim(` embedding ${pending.length} block(s) via ${loaded.providerName} (local process)`));
|
|
67
81
|
}
|
|
68
82
|
const verboseHuman = Boolean(opts.verbose) && cli.flags.mode === "human";
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const docTasks = buildDocEmbedTasks(ws.store, repoId);
|
|
79
|
-
const docResult = await loaded.worker.processDocs(docTasks, {
|
|
83
|
+
// The drain is the semantic capability's (modular-core Phase 2): blocks
|
|
84
|
+
// then documents — so the pooled fallback can reuse the vectors just cached
|
|
85
|
+
// — bootstrapping from a consistent snapshot when the repository has no
|
|
86
|
+
// watermark, catching up from it otherwise. `--prune` forces the full
|
|
87
|
+
// rescan with its cleanup: the current model's vectors are written before
|
|
88
|
+
// the dead ones go, and other models' rows go with them.
|
|
89
|
+
const semantic = requireSemantic(ws.store.db);
|
|
90
|
+
const result = await semantic.drain(loaded.worker, repoId, {
|
|
91
|
+
rescan: Boolean(opts.prune),
|
|
80
92
|
...(verboseHuman
|
|
81
|
-
? { onProgress: ({ embedded, total }) => cli.io.err(cli.style.dim(` … ${embedded}/${total} doc(s) embedded`)) }
|
|
93
|
+
? { onProgress: ({ phase, embedded, total }) => cli.io.err(cli.style.dim(` … ${embedded}/${total} ${phase === "docs" ? "doc(s) " : ""}embedded`)) }
|
|
82
94
|
: {}),
|
|
83
95
|
});
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
let pruned;
|
|
96
|
+
// What the pass cleaned up: an identity change's invalidation and the S1
|
|
97
|
+
// sweep every pass does; with `--prune`, other models' leftovers as well.
|
|
98
|
+
const pruned = { ...result.pruned };
|
|
88
99
|
if (opts.prune) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
const foreign = semantic.pruneForeign(loaded.worker);
|
|
101
|
+
pruned.blocks += foreign.blocks;
|
|
102
|
+
pruned.docs += foreign.docs;
|
|
103
|
+
}
|
|
104
|
+
if (cli.flags.mode === "human" && (pruned.blocks || pruned.docs)) {
|
|
105
|
+
cli.io.err(cli.style.dim(` pruned ${pruned.blocks} block + ${pruned.docs} doc vector(s) (other models, vanished content)`));
|
|
93
106
|
}
|
|
94
107
|
// Fold doc work into the block counts so callers' "embedded/cached" report
|
|
95
108
|
// covers both grains (pooled docs count as embedded work done).
|
|
96
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
embedded: result.embedded + result.docEmbedded + result.pooled, cached: result.cached + result.docCached,
|
|
111
|
+
mode: result.mode, processed_seq: result.processed_seq, head: result.head, pruned,
|
|
112
|
+
};
|
|
97
113
|
}
|
|
98
114
|
finally {
|
|
99
115
|
await loaded.close();
|
package/dist/src/cmd/admin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { rebuildIndex, runGc,
|
|
2
|
+
import { rebuildIndex, runGc, buildEmbedTasks, buildDocEmbedTasks, indexIdentity, requireSemantic, resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, ftsIndexedRowCount, liveLeafCount, sizeReport, activeAlerts, readCapability, openCoverageFrom, captureFor, enableSemantic, disableSemantic, semanticProviderOf, SemanticProviderMissing, withWriterLock, vacuum, reclaimableBytes, humanBytes, formatShare, } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, EXIT_OK, EXIT_ERROR, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
import { columns } from "../render.js";
|
|
5
5
|
import { loadEmbedding, drainEmbeddings } from "./_embed.js";
|
|
@@ -40,7 +40,7 @@ function runGcCmd(cli, args) {
|
|
|
40
40
|
if (values.help) {
|
|
41
41
|
return renderHelp(cli, {
|
|
42
42
|
name: "gc",
|
|
43
|
-
summary: "Mark-and-sweep unreferenced blobs and tree nodes,
|
|
43
|
+
summary: "Mark-and-sweep unreferenced blobs and tree nodes, the `same` disposition rows a pre-14.2 store wrote, and (store 17.0) the id claims older than identity.claims_retain_days that name no row and the change-feed rows past feed.retain_days / feed.retain_entries (refuses unless gc.enabled is set in repo settings)",
|
|
44
44
|
usage: "gc [--dry-run] [--vacuum]",
|
|
45
45
|
options: [
|
|
46
46
|
["--dry-run", "report what would be swept, sweep nothing (allowed even when gc is disabled)"],
|
|
@@ -70,7 +70,13 @@ function runGcCmd(cli, args) {
|
|
|
70
70
|
const result = vac ? { ...swept, vacuum: vac } : swept;
|
|
71
71
|
if (cli.flags.mode !== "human")
|
|
72
72
|
return emitMachine(cli, result);
|
|
73
|
-
|
|
73
|
+
// spec/store §7.3 (17.0): the claims prune's count is shown only when it
|
|
74
|
+
// released something — the line is pinned by shape without it, and the
|
|
75
|
+
// setting is off by default.
|
|
76
|
+
const released = swept.claimsReleased > 0 ? `, ${swept.claimsReleased} id claims released` : "";
|
|
77
|
+
// spec/store §7.4 (17.0): likewise the feed rows pruned under the floors, only when non-zero.
|
|
78
|
+
const pruned = swept.feedPruned > 0 ? `, ${swept.feedPruned} feed rows pruned` : "";
|
|
79
|
+
cli.io.err(cli.style.dim(` swept ${swept.blobsSwept} blobs, ${swept.treeNodesSwept} tree nodes, ${swept.sameDispositionsSwept} same dispositions${released}${pruned}`));
|
|
74
80
|
if (vac)
|
|
75
81
|
cli.io.err(cli.style.dim(vac.applied ? ` vacuumed, reclaimed ${humanBytes(vac.reclaimable)}` : ` would vacuum ${humanBytes(vac.reclaimable)}`));
|
|
76
82
|
return EXIT_OK;
|
|
@@ -81,30 +87,58 @@ function runDoctor(cli, args) {
|
|
|
81
87
|
if (values.help) {
|
|
82
88
|
return renderHelp(cli, {
|
|
83
89
|
name: "doctor",
|
|
84
|
-
summary: "Invariant sweep:
|
|
90
|
+
summary: "Invariant sweep: FTS rows vs live leaf blocks, head present, capture complete, active alerts, SQLite integrity — exit 1 on any failure (CI-able)",
|
|
85
91
|
usage: ["doctor [--json]", "doctor --size [--json]"],
|
|
86
92
|
options: [
|
|
87
|
-
["--size", "instead of the checks: what the database is made of — bytes and rows per category (content, history, search, sync) and table, the file's pages, what VACUUM would reclaim, advice"],
|
|
88
|
-
["--json", "`{ ok, checks: [{ name, ok, detail }] }`; with --size `{ file, categories, same_dispositions, archived, advice }`"],
|
|
93
|
+
["--size", "instead of the checks: what the database is made of — bytes and rows per category (content, identity, recovery, history, search, sync — the change feed under sync) and table, the file's pages, the feed's position, what VACUUM would reclaim, advice"],
|
|
94
|
+
["--json", "`{ ok, checks: [{ name, ok, detail }] }`; with --size `{ file, categories, same_dispositions, id_claims, archived, feed, capabilities, advice }`"],
|
|
89
95
|
],
|
|
90
96
|
});
|
|
91
97
|
}
|
|
92
98
|
const ws = cli.workspace();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
// The size report is store-wide; its `feed` line is a repository's (spec/store
|
|
100
|
+
// §7.2, 17.0) — the selected one when the selection resolves, else the
|
|
101
|
+
// store's only one (the report decides), never an error.
|
|
102
|
+
if (values.size) {
|
|
103
|
+
const repoId = selectedRepoId(cli, ws);
|
|
104
|
+
return runDoctorSize(cli, sizeReport(ws.store, repoId === undefined ? {} : { repoId }));
|
|
105
|
+
}
|
|
106
|
+
const repo = cli.repo(ws); // the selected repo must resolve (`repo_not_found` otherwise); the checks are store-wide but the alerts are its
|
|
96
107
|
const db = ws.store.db;
|
|
97
108
|
const checks = [];
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
// (The `convergence` check — `repos_status.unconverged === 0` — left with the
|
|
110
|
+
// count in store 17.0, D9: `file_hash` IS the head's hash by construction
|
|
111
|
+
// (I1), so it was identically true; `disk.changed` is the signal, `status`
|
|
112
|
+
// shows it.)
|
|
100
113
|
// The index holds exactly the live LEAF blocks (spec/search §1.1): a container
|
|
101
114
|
// has no row, a tombstone none. Counted from the index's shadow table — a plain
|
|
102
115
|
// count(*) on an external-content FTS table answers from `blocks` instead.
|
|
103
116
|
const ftsCount = ftsIndexedRowCount(db);
|
|
104
117
|
const leaves = liveLeafCount(db);
|
|
105
118
|
checks.push({ name: "fts rows == live leaf blocks", ok: ftsCount === leaves, detail: `fts=${ftsCount} leaves=${leaves}` });
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
// Store 17.0 (D1): every live document has a head token, and while capture
|
|
120
|
+
// is on the recorder's `revisions` row agrees with it (spec/store §8 H1 —
|
|
121
|
+
// the check `doctor` used to call "no dangling current_rev"). Phase 4a: the
|
|
122
|
+
// check reads the repository's manifest — a head minted within the open
|
|
123
|
+
// coverage interval owes a revision; a repository whose capture is off (or a
|
|
124
|
+
// build without the module) owes none and the check says so in its detail.
|
|
125
|
+
const headless = db.prepare("SELECT count(*) c FROM docs d WHERE d.deleted_at IS NULL AND d.head IS NULL").get().c;
|
|
126
|
+
checks.push({ name: "head present", ok: headless === 0, detail: `${headless} without a head` });
|
|
127
|
+
const manifest = readCapability(db, repo.repoId, "history");
|
|
128
|
+
const since = openCoverageFrom(manifest);
|
|
129
|
+
const recorder = captureFor(db, repo.repoId);
|
|
130
|
+
if (recorder && since !== null) {
|
|
131
|
+
const uncaptured = recorder.uncapturedHeads(repo.repoId, since);
|
|
132
|
+
checks.push({ name: "capture complete", ok: uncaptured === 0, detail: `${uncaptured} heads without a revision` });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
checks.push({ name: "capture complete", ok: true, detail: manifest?.state === "enabled" ? "history unavailable in this build" : `history disabled${manifest && manifest.coverage.length > 0 ? ` (captured ${manifest.coverage.map((i) => `${i.from}..${i.to ?? "now"}`).join(", ")})` : ""}` });
|
|
136
|
+
}
|
|
137
|
+
// Store 17.0 (3e-ii): the alert channel (spec/store §3.1 `alerts`) — the
|
|
138
|
+
// conditions the engine raised for a human on the selected repository; any
|
|
139
|
+
// active one fails the check and is named in its detail.
|
|
140
|
+
const alerts = activeAlerts(db, repo.repoId);
|
|
141
|
+
checks.push({ name: "alerts", ok: alerts.length === 0, detail: alerts.length === 0 ? "none active" : alerts.map((a) => `${a.code}[${a.severity}]: ${a.message}`).join("; ") });
|
|
108
142
|
const integrity = db.pragma("integrity_check", { simple: true });
|
|
109
143
|
checks.push({ name: "sqlite integrity", ok: integrity === "ok", detail: integrity });
|
|
110
144
|
const allOk = checks.every((c) => c.ok);
|
|
@@ -119,6 +153,19 @@ function runDoctor(cli, args) {
|
|
|
119
153
|
}
|
|
120
154
|
return allOk ? EXIT_OK : EXIT_ERROR;
|
|
121
155
|
}
|
|
156
|
+
/** The selected repository's id when the selection resolves; undefined when it does not (an explicit `--repo <slug>` that fails is still the error). */
|
|
157
|
+
function selectedRepoId(cli, ws) {
|
|
158
|
+
try {
|
|
159
|
+
return cli.repo(ws).repoId;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (cli.flags.repo)
|
|
163
|
+
throw err;
|
|
164
|
+
if (err instanceof EngineErrorLike && err.code === "repo_not_found")
|
|
165
|
+
return undefined;
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
122
169
|
// `doctor --size` (spec/cli §6, cli 4.3): the store's size report (spec/store
|
|
123
170
|
// §7 "Size") as one aligned table — each category, then its three biggest
|
|
124
171
|
// tables indented — the file's page accounting, and the advice lines. Exit 0:
|
|
@@ -160,6 +207,13 @@ function runDoctorSize(cli, report) {
|
|
|
160
207
|
["reclaimable", humanBytes(f.reclaimable)],
|
|
161
208
|
// 15.0: the history that already moved to archive segments (spec/store §7 "Retention").
|
|
162
209
|
["archived", `${report.archived.revisions} revisions in ${report.archived.segments} segments, ${humanBytes(report.archived.bytes)} on disk`],
|
|
210
|
+
// 17.0: the identity claims (spec/store §2.1) — one row per id ever minted and still held; `identity.claims_retain_days` lets `gc` release old ones (§7.3).
|
|
211
|
+
["id claims", `${report.id_claims} ids claimed`],
|
|
212
|
+
// 17.0 (3e): the change feed's position (spec/store §7.2 `feed`, §7.4) — the report's repository's; none when the store has several and none was selected.
|
|
213
|
+
["feed", report.feed ? `epoch ${report.feed.epoch}, ${report.feed.entries} entries, seq ${report.feed.oldest}..${report.feed.head}` : "no repository selected"],
|
|
214
|
+
// 17.0 (3e-ii): the capability manifest of the same repository (spec/store §3.1 `capabilities`): `history enabled since <ts> | disabled`, `semantic enabled (<model>) | disabled`.
|
|
215
|
+
["history", capabilityLine(report.capabilities, "history")],
|
|
216
|
+
["semantic", capabilityLine(report.capabilities, "semantic")],
|
|
163
217
|
];
|
|
164
218
|
for (const line of columns(file))
|
|
165
219
|
io.out(` ${line}`);
|
|
@@ -167,6 +221,18 @@ function runDoctorSize(cli, report) {
|
|
|
167
221
|
io.out(` ${style.dim("advice:")} ${a}`);
|
|
168
222
|
return EXIT_OK;
|
|
169
223
|
}
|
|
224
|
+
/** The manifest line of one capability: `enabled since <coverage_from>` for history, `enabled (<model>)` for semantic, `disabled`, or `no repository selected` like `feed`. */
|
|
225
|
+
function capabilityLine(capabilities, name) {
|
|
226
|
+
if (!capabilities)
|
|
227
|
+
return "no repository selected";
|
|
228
|
+
const c = capabilities.find((x) => x.capability === name);
|
|
229
|
+
if (!c || c.state === "disabled")
|
|
230
|
+
return "disabled";
|
|
231
|
+
if (name === "history")
|
|
232
|
+
return c.coverage_from ? `enabled since ${c.coverage_from}` : "enabled";
|
|
233
|
+
const model = c.config.model;
|
|
234
|
+
return typeof model === "string" ? `enabled (${model})` : "enabled";
|
|
235
|
+
}
|
|
170
236
|
function resolveConfigScope(cli, ws) {
|
|
171
237
|
if (cli.flags.repo === "")
|
|
172
238
|
return { kind: "workspace" };
|
|
@@ -263,22 +329,29 @@ async function runEmbed(cli, args) {
|
|
|
263
329
|
if (args.includes("--help") || args.includes("-h") || sub === "help") {
|
|
264
330
|
return renderHelp(cli, {
|
|
265
331
|
name: "embed",
|
|
266
|
-
summary: "
|
|
267
|
-
usage: ["embed [status]", "embed drain [--verbose] [--prune]"],
|
|
332
|
+
summary: "Semantic indexing: switch it on or off per repository, report the provider + what is queued, or drain the queue to embed now",
|
|
333
|
+
usage: ["embed [status]", "embed enable [--provider <command|url>]", "embed disable", "embed drain [--verbose] [--prune]"],
|
|
268
334
|
options: [
|
|
269
335
|
["status", "(default) provider, model, and how many blocks/docs are embeddable but not yet embedded"],
|
|
336
|
+
["enable", "switch semantic indexing on for the repository (needs a provider: --provider sets embedding.provider at the workspace layer); idempotent"],
|
|
337
|
+
["disable", "switch it off: semantic() and the drain refuse until enable; the provider setting and the vectors stay; idempotent"],
|
|
338
|
+
["--provider <p>", "(enable) the embedder command or http(s) URL to configure first"],
|
|
270
339
|
["drain", "embed the queued blocks now — status alone makes no progress"],
|
|
271
340
|
["-v, --verbose", "(drain) per-batch progress"],
|
|
272
341
|
["--prune", "(drain) afterwards delete vectors left by other models (e.g. after switching models)"],
|
|
273
342
|
],
|
|
274
|
-
notes: [`needs embedding.provider (\`${cli.prog} config set embedding.provider omgbase-embedder --repo ""\`); without one, semantic search is simply off.`],
|
|
343
|
+
notes: [`needs embedding.provider (\`${cli.prog} config set embedding.provider omgbase-embedder --repo ""\`, or \`${cli.prog} embed enable --provider omgbase-embedder\`); without one, semantic search is simply off.`],
|
|
275
344
|
});
|
|
276
345
|
}
|
|
346
|
+
if (sub === "enable" || sub === "disable")
|
|
347
|
+
return runEmbedLifecycle(cli, sub, args.filter((a) => a !== sub));
|
|
277
348
|
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
278
349
|
const prune = args.includes("--prune");
|
|
279
350
|
const ws = cli.workspace();
|
|
280
351
|
const repo = cli.repo(ws);
|
|
281
|
-
|
|
352
|
+
// `status`/`drain` are the explicit semantic verbs: a row switched off by
|
|
353
|
+
// `embed disable` refuses them (spec/store §5.10) rather than reporting nothing.
|
|
354
|
+
const loaded = await loadEmbedding(ws, repo.repoId, { switchedOff: "throw" });
|
|
282
355
|
if (!loaded) {
|
|
283
356
|
// No provider configured (05 §6). Honest report; not an error.
|
|
284
357
|
const hint = 'set one with `omg config set embedding.provider <command|url>` (e.g. omgbase-embedder)';
|
|
@@ -305,14 +378,24 @@ async function runEmbed(cli, args) {
|
|
|
305
378
|
const docPending = loaded.worker.staleDocs(docTasks);
|
|
306
379
|
// Vectors left by a previous model (dead weight after a model switch).
|
|
307
380
|
const foreign = loaded.worker.foreignVectorCount();
|
|
381
|
+
// The indexer's watermark against the feed head (modular-core Phase 2):
|
|
382
|
+
// how far the index is behind, and whether it was built under this provider.
|
|
383
|
+
const fresh = requireSemantic(ws.store.db).freshness(repo.repoId, indexIdentity(loaded.provider));
|
|
308
384
|
// status
|
|
309
|
-
const payload = {
|
|
385
|
+
const payload = {
|
|
386
|
+
provider: loaded.providerName, model: loaded.provider.model, dim: loaded.provider.dim, embeddable: tasks.length, queued: pending.length, docs: docTasks.length, docsQueued: docPending.length, foreignBlocks: foreign.blocks, foreignDocs: foreign.docs,
|
|
387
|
+
watermark: { processed_seq: fresh.processed_seq, head: fresh.head, fresh: fresh.fresh, identity: fresh.identity },
|
|
388
|
+
};
|
|
310
389
|
if (cli.flags.mode !== "human")
|
|
311
390
|
return emitMachine(cli, payload);
|
|
312
391
|
else {
|
|
313
392
|
cli.io.out(` provider ${cli.style.accent(loaded.providerName)} ${cli.style.dim(`(${loaded.provider.model}, ${loaded.provider.dim}d)`)}`);
|
|
314
393
|
cli.io.out(` embeddable ${tasks.length} ${cli.style.dim("queued")} ${payload.queued}`);
|
|
315
394
|
cli.io.out(` docs ${docTasks.length} ${cli.style.dim("queued")} ${payload.docsQueued}`);
|
|
395
|
+
const wm = fresh.processed_seq === null ? "none (the first drain bootstraps)" : `${fresh.processed_seq}${fresh.head !== null ? ` of ${fresh.head}` : ""}${fresh.fresh === true ? " (current)" : fresh.fresh === false ? " (behind)" : ""}`;
|
|
396
|
+
cli.io.out(` watermark ${wm}`);
|
|
397
|
+
if (fresh.identity && (fresh.identity.model !== loaded.provider.model || fresh.identity.dims !== loaded.provider.dim))
|
|
398
|
+
cli.io.out(cli.style.dim(` the index was built under ${fresh.identity.model} (${fresh.identity.dims}d) — the next drain rebuilds it`));
|
|
316
399
|
// `status` reports but never embeds — point the reader at the verb that does.
|
|
317
400
|
if (payload.queued > 0 || payload.docsQueued > 0)
|
|
318
401
|
cli.io.out(cli.style.dim(` run \`omg embed drain\` to embed the ${payload.queued} queued block(s) + ${payload.docsQueued} doc(s)`));
|
|
@@ -327,6 +410,69 @@ async function runEmbed(cli, args) {
|
|
|
327
410
|
await loaded.close();
|
|
328
411
|
}
|
|
329
412
|
}
|
|
413
|
+
/** A semantic coverage interval's `from..to` as the human lines print it. */
|
|
414
|
+
function semanticInterval(i) {
|
|
415
|
+
return `${i.from}..${i.to ?? "now"}`;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* `embed enable [--provider <p>]` / `embed disable` (spec/store §5.10, Phase
|
|
419
|
+
* 4b): the semantic manifest row is the verbs'. `enable` with `--provider`
|
|
420
|
+
* first writes `embedding.provider` at the WORKSPACE layer (as `init
|
|
421
|
+
* --embedder` does — one vector space per workspace; that write re-derives
|
|
422
|
+
* every repository's row whose provider changed), then sets the selected
|
|
423
|
+
* repository's row `enabled` with an open coverage interval; without a
|
|
424
|
+
* configured provider it is a usage error. `disable` closes the interval and
|
|
425
|
+
* sets `disabled` — the provider setting and the vectors stay. Stderr `
|
|
426
|
+
* semantic enabled since <ts> (provider <p>)` / ` semantic already enabled
|
|
427
|
+
* since <ts> (provider <p>)` / ` semantic disabled (embedded <from>..<to>[,
|
|
428
|
+
* …])` / ` semantic already disabled`; `--json` `{ state, coverage,
|
|
429
|
+
* provider, changed }`. Under the writer lock (a manifest write).
|
|
430
|
+
*/
|
|
431
|
+
function runEmbedLifecycle(cli, verb, args) {
|
|
432
|
+
const { values } = parseArgs({ args, allowPositionals: false, options: { provider: { type: "string" } } });
|
|
433
|
+
if (verb === "disable" && values.provider !== undefined)
|
|
434
|
+
throw new CliUsageError("--provider goes with embed enable");
|
|
435
|
+
const ws = cli.workspace();
|
|
436
|
+
const repo = cli.repo(ws);
|
|
437
|
+
// `--provider`'s settings write may itself flip the row (a provider change
|
|
438
|
+
// re-derives it): the verb reports `changed` when the state moved either way.
|
|
439
|
+
const stateBefore = readCapability(ws.store.db, repo.repoId, "semantic")?.state ?? "disabled";
|
|
440
|
+
const result = withWriterLock(ws.omgbaseDir, () => {
|
|
441
|
+
if (verb === "enable") {
|
|
442
|
+
if (values.provider !== undefined) {
|
|
443
|
+
if (values.provider.trim() === "")
|
|
444
|
+
throw new CliUsageError("--provider needs a <command|url>");
|
|
445
|
+
const current = workspaceSettings(ws.store);
|
|
446
|
+
const embedding = current.embedding && typeof current.embedding === "object" && !Array.isArray(current.embedding) ? current.embedding : {};
|
|
447
|
+
writeWorkspaceSettings(ws.store, { ...current, embedding: { ...embedding, provider: values.provider } });
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
return enableSemantic(ws.store.db, repo.repoId);
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
if (e instanceof SemanticProviderMissing)
|
|
454
|
+
throw new CliUsageError(e.message);
|
|
455
|
+
throw e;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return disableSemantic(ws.store.db, repo.repoId);
|
|
459
|
+
});
|
|
460
|
+
const row = result.row;
|
|
461
|
+
const changed = result.changed || row.state !== stateBefore;
|
|
462
|
+
const provider = semanticProviderOf(ws.store.db, repo.repoId);
|
|
463
|
+
if (cli.flags.mode !== "human")
|
|
464
|
+
return emitMachine(cli, { state: row.state, coverage: row.coverage, provider, changed });
|
|
465
|
+
const { io, style } = cli;
|
|
466
|
+
if (verb === "enable") {
|
|
467
|
+
const open = row.coverage.length > 0 ? row.coverage[row.coverage.length - 1] : null;
|
|
468
|
+
const since = open && open.to === null ? open.from : null;
|
|
469
|
+
io.err(style.dim(` semantic ${changed ? "enabled" : "already enabled"}${since ? ` since ${since}` : ""}${provider ? ` (provider ${provider})` : ""}`));
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
io.err(style.dim(changed || row.coverage.length > 0 ? ` semantic ${changed ? "disabled" : "already disabled"} (embedded ${row.coverage.map(semanticInterval).join(", ")})` : " semantic already disabled"));
|
|
473
|
+
}
|
|
474
|
+
return EXIT_OK;
|
|
475
|
+
}
|
|
330
476
|
// helpers ---------------------------------------------------------------------
|
|
331
477
|
function getPath(obj, path) {
|
|
332
478
|
return path.split(".").reduce((o, k) => (o && typeof o === "object" ? o[k] : undefined), obj);
|
package/dist/src/cmd/diff.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { findDoc,
|
|
2
|
+
import { findDoc, requireHistoryReader, historyReadRefusal, RevisionNotFound } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
import { remoteCall, docLocator } from "./_remote.js";
|
|
5
5
|
// `omg diff <doc>` (11 §5.5) — `docs_diff_unified`: the unified diff, `--json` the
|
|
@@ -41,11 +41,20 @@ async function runDiff(cli, args) {
|
|
|
41
41
|
const info = ref.startsWith("d_") ? findDoc(ws.store, { docId: ref }) : findDoc(ws.store, { repoId: repo.repoId, path: ref });
|
|
42
42
|
if (!info)
|
|
43
43
|
throw new EngineErrorLike("doc_missing", `no document ${ref}`);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Through the recorder seam (Phase 4a): `capability_unavailable { history, unavailable | disabled }` when the build has no module or nothing was ever captured;
|
|
45
|
+
// a revision the recorder does not hold answers by the spec/surface §2.1 decision table (Phase 4b).
|
|
46
|
+
try {
|
|
47
|
+
result = requireHistoryReader(ws.store.db).docDiffUnified(info.docId, {
|
|
48
|
+
...(values.from ? { fromRev: values.from } : {}),
|
|
49
|
+
...(values.to ? { toRev: values.to } : {}),
|
|
50
|
+
label: ref,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (e instanceof RevisionNotFound)
|
|
55
|
+
historyReadRefusal(ws.store.db, repo.repoId, e.rev, { docId: info.docId });
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
59
|
const { style, io } = cli;
|
|
51
60
|
if (values.blocks) {
|
|
@@ -54,7 +63,7 @@ async function runDiff(cli, args) {
|
|
|
54
63
|
if (cli.flags.server)
|
|
55
64
|
entries = await remoteCall(cli, "docs_diff", { ...docLocator(ref), from_rev: result.from, to_rev: result.to });
|
|
56
65
|
else
|
|
57
|
-
entries =
|
|
66
|
+
entries = requireHistoryReader(cli.workspace().store.db).diffBlocks(result._id, result.from, result.to);
|
|
58
67
|
if (cli.flags.mode !== "human")
|
|
59
68
|
return emitMachine(cli, entries, { items: entries, ids: entries.map((e) => e._id) });
|
|
60
69
|
if (entries.length === 0) {
|
package/dist/src/cmd/edit.js
CHANGED
|
@@ -13,7 +13,7 @@ import { runSugar } from "./_mutate.js";
|
|
|
13
13
|
// The human structural-edit loop.
|
|
14
14
|
function rawHashOf(ws, blockId) {
|
|
15
15
|
const row = ws.store.db
|
|
16
|
-
.prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND
|
|
16
|
+
.prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_at IS NULL")
|
|
17
17
|
.get(blockId);
|
|
18
18
|
return row?.h ?? null;
|
|
19
19
|
}
|
package/dist/src/cmd/hist.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { resolveRef,
|
|
2
|
+
import { resolveRef, requireHistoryReader } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
import { remoteCall } from "./_remote.js";
|
|
5
5
|
// `omg hist <node>` (11 §5.5) — blocks_history: a block's biography.
|
|
@@ -32,7 +32,8 @@ async function runHist(cli, args) {
|
|
|
32
32
|
const resolved = resolveRef(ws.store, repo.repoId, ref);
|
|
33
33
|
if (!resolved || resolved.kind !== "block")
|
|
34
34
|
throw new EngineErrorLike("block_missing", `hist needs a block id; got ${ref}`);
|
|
35
|
-
|
|
35
|
+
// Through the recorder seam (Phase 4a): `capability_unavailable { history, unavailable | disabled }` when the build has no module or nothing was ever captured.
|
|
36
|
+
changes = requireHistoryReader(ws.store.db).blocksHistory(resolved.blockId, values.n ? { limit: Number(values.n) } : {});
|
|
36
37
|
}
|
|
37
38
|
// A change carries no id of its own, so `--ids` is the `--json` document.
|
|
38
39
|
if (cli.flags.mode !== "human")
|
package/dist/src/cmd/history.js
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { evaluateRetention, retentionPolicy, exportHistory, attachArchive, detachArchive, listArchives, humanBytes, withWriterLock, RetentionPolicyError, NotASegment, StaleSegment, ARCHIVE_DIR, } from "@omgbase/core";
|
|
4
|
+
import { evaluateRetention, retentionPolicy, exportHistory, attachArchive, detachArchive, listArchives, humanBytes, withWriterLock, enableHistory, disableHistory, purgeHistory, openCoverageFrom, RetentionPolicyError, NotASegment, StaleSegment, ARCHIVE_DIR, } from "@omgbase/core";
|
|
5
5
|
import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
6
6
|
import { columns } from "../render.js";
|
|
7
|
-
// `omg history <plan|export|attach|detach|list>` (spec/cli
|
|
8
|
-
// 4.6
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
7
|
+
// `omg history <enable|disable|purge|plan|export|attach|detach|list>` (spec/cli
|
|
8
|
+
// §6 `history`, cli 4.6; `enable|disable` cli 5.1 — store 17.1 Phase 4a;
|
|
9
|
+
// `purge` cli 5.2 — store 17.2 Phase 4b): the operator surface of history
|
|
10
|
+
// CAPTURE (spec/store §5.9: `enable` turns the repository's capture on with a
|
|
11
|
+
// baseline commit and opens a coverage interval, `disable` closes it — the rows
|
|
12
|
+
// stay, reads of what was captured keep working; §7.6: `purge` removes the rows)
|
|
13
|
+
// and of history RETENTION (spec/store §7 "Retention", 15.0: `plan` evaluates
|
|
14
|
+
// the policy and prints what would move; `export` writes one archive segment
|
|
15
|
+
// and with `--prune` squashes the store; `attach` / `detach` / `list` manage
|
|
16
|
+
// the segment registry). No MCP tool: the catalog's traces are
|
|
17
|
+
// `version.capabilities.history`, the `revisions.archived` attribute,
|
|
18
|
+
// `capability_unavailable` and `revision_archived`.
|
|
19
|
+
const SUBCOMMANDS = ["enable", "disable", "purge", "plan", "export", "attach", "detach", "list"];
|
|
14
20
|
function renderCard(cli) {
|
|
15
21
|
return renderHelp(cli, {
|
|
16
22
|
name: "history",
|
|
17
|
-
summary: "History retention: plan what the policy would archive, export it to a segment, attach/detach/list segments",
|
|
23
|
+
summary: "History capture (enable/disable/purge per repository) and retention: plan what the policy would archive, export it to a segment, attach/detach/list segments",
|
|
18
24
|
usage: [
|
|
25
|
+
"history enable [--best-effort]",
|
|
26
|
+
"history disable",
|
|
27
|
+
"history purge [--before <ts>]",
|
|
19
28
|
"history plan [--before <ts>] [--policy <json|@file>]",
|
|
20
29
|
"history export [--prune] [--dest <dir>] [--before <ts>] [--policy <json|@file>] [--dry-run]",
|
|
21
30
|
"history attach <file|dir>",
|
|
@@ -23,12 +32,16 @@ function renderCard(cli) {
|
|
|
23
32
|
"history list",
|
|
24
33
|
],
|
|
25
34
|
options: [
|
|
35
|
+
["enable", "turn capture on for the repository: one baseline commit (origin import) records every live document's current head, a coverage interval opens, every later write is captured; idempotent"],
|
|
36
|
+
["disable", "turn capture off: the coverage interval closes, later writes record no history (the feed still does); what was captured stays readable; idempotent"],
|
|
37
|
+
["--best-effort", "(enable) do not require capture: a writer without the history module may still write (default: such a writer is refused — writer_requires)"],
|
|
38
|
+
["purge", "delete the repository's captured history — every commit, or with --before those before an instant (a live head's revision stays while capture is on); the recorder's id holds are released; the history tables are dropped when no repository is enabled and nothing is left"],
|
|
26
39
|
["plan", "per document: revisions kept / to archive and the bytes only they reach; the policy in effect"],
|
|
27
40
|
["export", "write ONE new segment file (default dest .omgbase/archives/) holding the archived revisions' bodies; --prune then squashes the store and records the segment"],
|
|
28
41
|
["attach <path>", "record a segment file (or every segment of a directory that belongs to this repo) so reads at its revisions work again"],
|
|
29
42
|
["detach <ref>", "forget a segment by id or path (its revisions stay listed; reading one is revision_archived)"],
|
|
30
43
|
["list", "the repo's segments: commit range, revisions, bytes, state (attached, missing, pending, detached, stale)"],
|
|
31
|
-
["--before <ts>", "only revisions committed before this RFC 3339 instant are candidates"],
|
|
44
|
+
["--before <ts>", "(plan/export/purge) only revisions committed before this RFC 3339 instant are candidates"],
|
|
32
45
|
["--policy <p>", "a JSON object (or @file) merged over the effective history.retention settings for this run"],
|
|
33
46
|
["--prune", "(export) after the segment is durable: NULL the archived bodies, drop their change rows, sweep, record"],
|
|
34
47
|
["--dest <dir>", "(export) where the segment file goes"],
|
|
@@ -213,6 +226,81 @@ function runList(cli, args) {
|
|
|
213
226
|
io.out(` ${line}`);
|
|
214
227
|
return EXIT_OK;
|
|
215
228
|
}
|
|
229
|
+
/** A coverage interval's `from..to` as the human lines print it. */
|
|
230
|
+
function interval(i) {
|
|
231
|
+
return `${i.from}..${i.to ?? "now"}`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The `enable`/`disable` rendering: `--json` the manifest row's capture state
|
|
235
|
+
* `{ state, coverage, writer_requires, changed, baseline? }` (`baseline` the
|
|
236
|
+
* commit and the revisions it wrote, on an `enable` that changed something);
|
|
237
|
+
* human one stderr line — ` history enabled since <ts>` / ` history already
|
|
238
|
+
* enabled since <ts>` / ` history disabled (captured <from>..<to>[, …])` /
|
|
239
|
+
* ` history already disabled`.
|
|
240
|
+
*/
|
|
241
|
+
function renderLifecycle(cli, verb, r) {
|
|
242
|
+
const row = r.row;
|
|
243
|
+
if (cli.flags.mode !== "human") {
|
|
244
|
+
return emitMachine(cli, { state: row.state, coverage: row.coverage, writer_requires: row.writer_requires, changed: r.changed, ...(r.baseline ? { baseline: r.baseline } : {}) });
|
|
245
|
+
}
|
|
246
|
+
const { io, style } = cli;
|
|
247
|
+
if (verb === "enable") {
|
|
248
|
+
const since = openCoverageFrom(row);
|
|
249
|
+
io.err(style.dim(` history ${r.changed ? "enabled" : "already enabled"}${since ? ` since ${since}` : ""}${r.baseline ? ` (baseline ${r.baseline.commit}: ${plural(r.baseline.revisions, "revision")})` : ""}`));
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
io.err(style.dim(r.changed || row.coverage.length > 0 ? ` history ${r.changed ? "disabled" : "already disabled"} (captured ${row.coverage.map(interval).join(", ")})` : " history already disabled"));
|
|
253
|
+
}
|
|
254
|
+
return EXIT_OK;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* `history purge [--before <ts>]` (spec/store §7.6, Phase 4b): the receipt
|
|
258
|
+
* `{ purged: { <table>: n }, claims_released, coverage, tables_dropped }` under
|
|
259
|
+
* `--json`; human one stderr line — ` purged <c> commit(s), <r> revision(s),
|
|
260
|
+
* <d> disposition(s), <b> block change(s), <e> edge transition(s), <s>
|
|
261
|
+
* segment(s); swept <x> blob(s), <y> tree node(s); released <n> id claim(s)` —
|
|
262
|
+
* then ` history tables dropped` when the bundle left, or ` nothing to
|
|
263
|
+
* purge` when every count is zero. Coverage is untouched (it records when
|
|
264
|
+
* capture was on; a purged token reads `pruned`).
|
|
265
|
+
*/
|
|
266
|
+
function renderPurge(cli, r) {
|
|
267
|
+
if (cli.flags.mode !== "human")
|
|
268
|
+
return emitMachine(cli, { purged: r.purged, claims_released: r.claims_released, coverage: r.coverage, tables_dropped: r.tables_dropped });
|
|
269
|
+
const { io, style } = cli;
|
|
270
|
+
const p = r.purged;
|
|
271
|
+
const total = Object.values(p).reduce((n, v) => n + v, 0) + r.claims_released;
|
|
272
|
+
if (total === 0 && !r.tables_dropped) {
|
|
273
|
+
io.err(style.dim(" nothing to purge"));
|
|
274
|
+
return EXIT_OK;
|
|
275
|
+
}
|
|
276
|
+
io.err(style.dim(` purged ${plural(p.commits, "commit")}, ${plural(p.revisions, "revision")}, ${plural(p.dispositions, "disposition")}, ${plural(p.block_changes, "block change")}, ${plural(p.edge_transitions, "edge transition")}, ${plural(p.archives, "segment")}; swept ${plural(p.blobs, "blob")}, ${plural(p.tree_nodes, "tree node")}; released ${plural(r.claims_released, "id claim")}`));
|
|
277
|
+
if (r.tables_dropped)
|
|
278
|
+
io.err(style.dim(" history tables dropped"));
|
|
279
|
+
return EXIT_OK;
|
|
280
|
+
}
|
|
281
|
+
function runPurge(cli, args) {
|
|
282
|
+
const { values } = parseArgs({ args, allowPositionals: false, options: { before: { type: "string" } } });
|
|
283
|
+
const ws = cli.workspace();
|
|
284
|
+
const repo = cli.repo(ws);
|
|
285
|
+
const before = parseBefore(values.before);
|
|
286
|
+
const result = withWriterLock(ws.omgbaseDir, () => purgeHistory(ws.store.db, repo.repoId, { ...(before !== undefined ? { before } : {}) }));
|
|
287
|
+
return renderPurge(cli, result);
|
|
288
|
+
}
|
|
289
|
+
function runEnable(cli, args) {
|
|
290
|
+
const { values } = parseArgs({ args, allowPositionals: false, options: { "best-effort": { type: "boolean" } } });
|
|
291
|
+
const ws = cli.workspace();
|
|
292
|
+
const repo = cli.repo(ws);
|
|
293
|
+
// A write (the baseline commit): under the workspace writer lock like every other.
|
|
294
|
+
const result = withWriterLock(ws.omgbaseDir, () => enableHistory(ws.store.db, repo.repoId, { writerRequires: !values["best-effort"] }));
|
|
295
|
+
return renderLifecycle(cli, "enable", result);
|
|
296
|
+
}
|
|
297
|
+
function runDisable(cli, args) {
|
|
298
|
+
parseArgs({ args, allowPositionals: false, options: {} });
|
|
299
|
+
const ws = cli.workspace();
|
|
300
|
+
const repo = cli.repo(ws);
|
|
301
|
+
const result = withWriterLock(ws.omgbaseDir, () => disableHistory(ws.store.db, repo.repoId));
|
|
302
|
+
return renderLifecycle(cli, "disable", result);
|
|
303
|
+
}
|
|
216
304
|
function runHistory(cli, args) {
|
|
217
305
|
const [sub, ...rest] = args;
|
|
218
306
|
if (sub === undefined || sub === "--help" || sub === "-h" || sub === "help" || rest.includes("--help") || rest.includes("-h"))
|
|
@@ -221,6 +309,9 @@ function runHistory(cli, args) {
|
|
|
221
309
|
throw new CliUsageError(`unknown history subcommand '${sub}' (${SUBCOMMANDS.join("|")})`, `run '${cli.prog} history --help'`);
|
|
222
310
|
}
|
|
223
311
|
switch (sub) {
|
|
312
|
+
case "enable": return runEnable(cli, rest);
|
|
313
|
+
case "disable": return runDisable(cli, rest);
|
|
314
|
+
case "purge": return runPurge(cli, rest);
|
|
224
315
|
case "plan": return runPlan(cli, rest);
|
|
225
316
|
case "export": return runExport(cli, rest);
|
|
226
317
|
case "attach": return runAttach(cli, rest);
|
|
@@ -228,5 +319,5 @@ function runHistory(cli, args) {
|
|
|
228
319
|
case "list": return runList(cli, rest);
|
|
229
320
|
}
|
|
230
321
|
}
|
|
231
|
-
export const cmdHistory = { name: "history", summary: "
|
|
322
|
+
export const cmdHistory = { name: "history", summary: "Capture: enable/disable/purge per repository; retention: plan/export archive segments, attach/detach/list", run: (cli, a) => runHistory(cli, a) };
|
|
232
323
|
//# sourceMappingURL=history.js.map
|
package/dist/src/cmd/log.js
CHANGED
|
@@ -4,16 +4,24 @@ import { CliUsageError, truncationFooter, EXIT_OK, emitMachine, renderHelp } fro
|
|
|
4
4
|
import { remoteCall } from "./_remote.js";
|
|
5
5
|
// `omg log` (11 §5.5) — changes_since, one digest summary per line. Relative
|
|
6
6
|
// --since (24h/7d) is resolved to a literal ISO timestamp client-side, then to
|
|
7
|
-
// the
|
|
7
|
+
// the feed seq at/just-before that time (the query language stays clock-free).
|
|
8
|
+
// A cursor is `(epoch, seq)` since store 17.0 (D7): `--cursor <n>` continues
|
|
9
|
+
// from a seq of the current epoch unless `--epoch <ep>` names the one it came
|
|
10
|
+
// from; a cursor the feed no longer serves is `expired_cursor` (exit 1).
|
|
8
11
|
function resolveSinceSeq(cli, repoId, since) {
|
|
9
12
|
if (!since)
|
|
10
13
|
return 0;
|
|
11
14
|
const iso = relativeToIso(since);
|
|
12
15
|
const ws = cli.workspace();
|
|
13
16
|
const row = ws.store.db
|
|
14
|
-
.prepare("SELECT MAX(seq) AS seq FROM
|
|
15
|
-
.get(repoId, iso);
|
|
16
|
-
|
|
17
|
+
.prepare("SELECT MAX(seq) AS seq FROM feed WHERE repo_id = ? AND epoch = (SELECT epoch FROM repos WHERE repo_id = ?) AND ts < ?")
|
|
18
|
+
.get(repoId, repoId, iso);
|
|
19
|
+
// A `--since` before the oldest retained row means "everything the feed
|
|
20
|
+
// holds" — cursor `oldest − 1` (cursor 0 on a feed pruned to `oldest ≥ 2`
|
|
21
|
+
// is `expired_cursor` since store 17.0, spec/surface §3).
|
|
22
|
+
const oldest = ws.store.db.prepare("SELECT COALESCE(MIN(seq), 0) AS seq FROM feed WHERE repo_id = ? AND epoch = (SELECT epoch FROM repos WHERE repo_id = ?)").get(repoId, repoId).seq;
|
|
23
|
+
const seq = row.seq ?? 0;
|
|
24
|
+
return seq < oldest - 1 ? Math.max(oldest - 1, 0) : seq;
|
|
17
25
|
}
|
|
18
26
|
function relativeToIso(since) {
|
|
19
27
|
const m = /^(\d+)([hdwm])$/.exec(since.trim());
|
|
@@ -37,6 +45,7 @@ async function runLog(cli, args) {
|
|
|
37
45
|
options: {
|
|
38
46
|
since: { type: "string" },
|
|
39
47
|
cursor: { type: "string" },
|
|
48
|
+
epoch: { type: "string" },
|
|
40
49
|
origin: { type: "string" },
|
|
41
50
|
n: { type: "string", short: "n" },
|
|
42
51
|
help: { type: "boolean" },
|
|
@@ -46,10 +55,11 @@ async function runLog(cli, args) {
|
|
|
46
55
|
return renderHelp(cli, {
|
|
47
56
|
name: "log",
|
|
48
57
|
summary: "What changed: one digest per commit, newest first",
|
|
49
|
-
usage: "log [--since <24h|7d|ISO>] [--cursor <n>] [--origin api|observed] [-n <N>]",
|
|
58
|
+
usage: "log [--since <24h|7d|ISO>] [--cursor <n> [--epoch <ep>]] [--origin api|observed] [-n <N>]",
|
|
50
59
|
options: [
|
|
51
60
|
["--since <t>", "only commits after a relative age (24h, 7d) or an ISO timestamp"],
|
|
52
|
-
["--cursor <n>", "continue from a
|
|
61
|
+
["--cursor <n>", "continue from a feed sequence number (the `cursor` of a previous page)"],
|
|
62
|
+
["--epoch <ep>", "the epoch the cursor belongs to (a page's `epoch`; default: the repo's current one)"],
|
|
53
63
|
["--origin <o>", "api (agent/CLI writes) | observed (file edits picked up by sync)"],
|
|
54
64
|
["-n <N>", "max commits"],
|
|
55
65
|
],
|
|
@@ -64,6 +74,7 @@ async function runLog(cli, args) {
|
|
|
64
74
|
throw new CliUsageError("--since is not supported with --server; use --cursor <seq>");
|
|
65
75
|
result = await remoteCall(cli, "changes_since", {
|
|
66
76
|
...(values.cursor ? { cursor: Number(values.cursor) } : {}),
|
|
77
|
+
...(values.epoch ? { epoch: values.epoch } : {}),
|
|
67
78
|
...(values.n ? { limit: Number(values.n) } : {}),
|
|
68
79
|
...(values.origin ? { origin: values.origin } : {}),
|
|
69
80
|
});
|
|
@@ -73,6 +84,8 @@ async function runLog(cli, args) {
|
|
|
73
84
|
const repo = cli.repo(ws);
|
|
74
85
|
const cursor = values.cursor ? Number(values.cursor) : resolveSinceSeq(cli, repo.repoId, values.since);
|
|
75
86
|
const opts = { cursor };
|
|
87
|
+
if (values.epoch)
|
|
88
|
+
opts.epoch = values.epoch;
|
|
76
89
|
if (values.n)
|
|
77
90
|
opts.limit = Number(values.n);
|
|
78
91
|
if (values.origin)
|
package/dist/src/cmd/mcp.js
CHANGED
|
@@ -113,7 +113,7 @@ async function runMcp(cli, args) {
|
|
|
113
113
|
cli.io.err(cli.style.err("[mcp] ✖ EMBEDDER NONFUNCTIONAL — semantic search + auto-embed disabled"));
|
|
114
114
|
cli.io.err(cli.style.err(` provider: ${d.provider ?? "?"}`));
|
|
115
115
|
cli.io.err(cli.style.err(` reason: ${d.reason ?? embedderError.message}`));
|
|
116
|
-
cli.io.err(cli.style.err(" semantic queries will fail with
|
|
116
|
+
cli.io.err(cli.style.err(" semantic queries will fail with capability_unavailable (state unavailable) until this is fixed."));
|
|
117
117
|
}
|
|
118
118
|
// Keep embeddings timely: a background drainer embeds blocks touched by a
|
|
119
119
|
// mutation (or a watcher checkpoint) without blocking the tool's response.
|
|
@@ -147,7 +147,7 @@ async function runMcp(cli, args) {
|
|
|
147
147
|
omgbaseDir: ws.omgbaseDir,
|
|
148
148
|
onCheckpoint: (r) => {
|
|
149
149
|
if (r.ingested.length > 0 || r.deleted.length > 0) {
|
|
150
|
-
cli.io.err(cli.style.dim(`[watch]
|
|
150
|
+
cli.io.err(cli.style.dim(`[watch] batch: +${r.ingested.length} -${r.deleted.length}`));
|
|
151
151
|
// A file change the watcher ingested may also need (re-)embedding.
|
|
152
152
|
drainer?.schedule();
|
|
153
153
|
}
|
|
@@ -180,19 +180,21 @@ async function runMcp(cli, args) {
|
|
|
180
180
|
cli.io.err(cli.style.dim(`[mcp] auto-embed on mutation enabled`));
|
|
181
181
|
if (tools !== "full")
|
|
182
182
|
cli.io.err(cli.style.dim(`[mcp] tools profile: ${tools}`));
|
|
183
|
-
// The query surface's `embedQuery`: a live vectorizer when the embedder loaded
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// `
|
|
187
|
-
//
|
|
188
|
-
//
|
|
183
|
+
// The query surface's `embedQuery`: a live vectorizer when the embedder loaded.
|
|
184
|
+
// When it is configured-but-broken the server gets the host's REPORT instead
|
|
185
|
+
// (`semanticUnavailable`): every semantic path then fails loudly with
|
|
186
|
+
// `capability_unavailable { semantic, unavailable }` (retriable) naming the
|
|
187
|
+
// provider and the reason, and `version` reports the state — not the
|
|
188
|
+
// misleading `provider_unavailable`, which the server emits only when no
|
|
189
|
+
// provider is configured at all. `const` captures satisfy closure narrowing
|
|
190
|
+
// over the `let`-bound state above.
|
|
189
191
|
const loaded = embedding;
|
|
190
192
|
const failed = embedderError;
|
|
191
193
|
const embedQuery = loaded
|
|
192
194
|
? async (text) => ({ model: loaded.provider.model, vec: await loaded.worker.embedQuery(text) })
|
|
193
|
-
:
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
: undefined;
|
|
196
|
+
const failedReport = failed ? (failed.data ?? {}) : null;
|
|
197
|
+
const semanticUnavailable = failedReport ? { provider: failedReport.provider ?? "?", reason: failedReport.reason ?? failed.message } : undefined;
|
|
196
198
|
// The workspace dir makes every write tool take the writer lock for its whole
|
|
197
199
|
// call (spec/sync §7) — `omg` verbs, the Rust `omgbase mcp` and the watcher
|
|
198
200
|
// all lock the same file, so the server must too.
|
|
@@ -204,6 +206,7 @@ async function runMcp(cli, args) {
|
|
|
204
206
|
...(drainer ? { onMutation: () => drainer.schedule() } : {}),
|
|
205
207
|
...(embedQuery ? { embedQuery } : {}),
|
|
206
208
|
...(loaded ? { embedProvider: loaded.providerName } : {}),
|
|
209
|
+
...(semanticUnavailable ? { semanticUnavailable } : {}),
|
|
207
210
|
// The `version` tool's view of this binary (spec/surface §4).
|
|
208
211
|
host: hostInfo(),
|
|
209
212
|
tools,
|
package/dist/src/cmd/mutate.js
CHANGED
|
@@ -38,7 +38,7 @@ function parent(ws, repoId, ref) {
|
|
|
38
38
|
// `edited` commit (its `_attrs.checked` only rewrites a task's marker).
|
|
39
39
|
function taskBlock(ws, ref, blockId) {
|
|
40
40
|
const row = ws.store.db
|
|
41
|
-
.prepare("SELECT type FROM blocks WHERE block_id = ? AND
|
|
41
|
+
.prepare("SELECT type FROM blocks WHERE block_id = ? AND deleted_at IS NULL")
|
|
42
42
|
.get(blockId);
|
|
43
43
|
if (!row)
|
|
44
44
|
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { _id: ref } });
|
|
@@ -364,7 +364,7 @@ async function runAppend(cli, args) {
|
|
|
364
364
|
* `block_not_container` (append means into, never a sibling insert). */
|
|
365
365
|
function appendSugar(store, blockId, markdown) {
|
|
366
366
|
const row = store.db
|
|
367
|
-
.prepare("SELECT b.type AS type, d.format AS format, (SELECT count(*) FROM blocks c WHERE c.parent_block = b.block_id AND c.
|
|
367
|
+
.prepare("SELECT b.type AS type, d.format AS format, (SELECT count(*) FROM blocks c WHERE c.parent_block = b.block_id AND c.deleted_at IS NULL) AS children FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ? AND b.deleted_at IS NULL")
|
|
368
368
|
.get(blockId);
|
|
369
369
|
if (!row)
|
|
370
370
|
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(blockId)}`, { data: { _id: blockId } });
|
package/dist/src/cmd/oqx.js
CHANGED
|
@@ -129,7 +129,9 @@ async function runOqx(cli, args) {
|
|
|
129
129
|
// provider loaded. loadEmbedding is only touched when a phrase is present.
|
|
130
130
|
const phrases = collectSemanticPhrases(source);
|
|
131
131
|
if (phrases.length > 0) {
|
|
132
|
-
|
|
132
|
+
// An explicit `semantic()` on a repository switched off by `omg embed
|
|
133
|
+
// disable` is the refusal with the fix, as the MCP `query` answers (Phase 4b).
|
|
134
|
+
const loaded = await loadEmbedding(ws, repo.repoId, { switchedOff: "throw" });
|
|
133
135
|
if (!loaded) {
|
|
134
136
|
throw new EngineErrorLike("provider_unavailable", "semantic(...) needs an embedding provider", {
|
|
135
137
|
hint: "omg config set embedding.provider <command|url>",
|
package/dist/src/cmd/run.js
CHANGED
|
@@ -64,7 +64,7 @@ async function runRun(cli, args) {
|
|
|
64
64
|
const node = find(roots);
|
|
65
65
|
if (!node || node.type !== "code_fence")
|
|
66
66
|
throw new EngineErrorLike("opaque_block", `block ${ref} is not an omg fence`);
|
|
67
|
-
fenceRaw = blockRaw(ws.store, node.
|
|
67
|
+
fenceRaw = blockRaw(ws.store, node.blockId);
|
|
68
68
|
}
|
|
69
69
|
else {
|
|
70
70
|
const info = findDoc(ws.store, { docId: resolved.docId });
|
|
@@ -72,7 +72,7 @@ async function runRun(cli, args) {
|
|
|
72
72
|
const fence = firstOmgFence(loadDocBlocks(ws.store, resolved.docId));
|
|
73
73
|
if (!fence)
|
|
74
74
|
throw new EngineErrorLike("target_missing", `no \`\`\`omg fence in ${ref}`);
|
|
75
|
-
fenceRaw = blockRaw(ws.store, fence.
|
|
75
|
+
fenceRaw = blockRaw(ws.store, fence.blockId);
|
|
76
76
|
}
|
|
77
77
|
const source = fenceBody(fenceRaw).trim();
|
|
78
78
|
if (!source)
|
package/dist/src/cmd/show.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { resolveRef, nodesGet, docsRead, docLinks,
|
|
2
|
+
import { resolveRef, nodesGet, docsRead, docLinks, historyReader, docPropertiesMerged } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
import { expandBlockArgs } from "./_mutate.js";
|
|
5
5
|
function showOne(cli, ref, include) {
|
|
@@ -27,7 +27,8 @@ function showOne(cli, ref, include) {
|
|
|
27
27
|
const node = nodesGet(ws.store, resolved.docId, resolved.blockId, { resolution: "full" });
|
|
28
28
|
if (!node)
|
|
29
29
|
throw new EngineErrorLike("block_missing", `no block ${ref}`);
|
|
30
|
-
|
|
30
|
+
// `--include history` on a store with nothing captured (Phase 4a): the card simply carries no history.
|
|
31
|
+
const history = include.includes("history") ? historyReader(ws.store.db)?.blocksHistory(resolved.blockId, { limit: 5 }) ?? [] : undefined;
|
|
31
32
|
return { payload: { ...node, ...(history ? { history } : {}) }, render: () => renderBlockCard(cli, node, history) };
|
|
32
33
|
}
|
|
33
34
|
function runShow(cli, args) {
|
package/dist/src/cmd/source.js
CHANGED
|
@@ -19,7 +19,7 @@ function help(cli) {
|
|
|
19
19
|
summary: "Where a repo's bytes come from (a filesystem directory today; git/S3/… via adapters later)",
|
|
20
20
|
usage: "source <add|list|attach|detach|rm> …",
|
|
21
21
|
options: [
|
|
22
|
-
["add <dir> [--repo <slug>] [--name <n>] [-y]", "point a repo at a directory: creates the repo (named after the dir unless --repo) + runs the initial sync (-y skips the consent prompt)"],
|
|
22
|
+
["add <dir> [--repo <slug>] [--name <n>] [-y] [--no-history]", "point a repo at a directory: creates the repo (named after the dir unless --repo) + runs the initial sync (-y skips the consent prompt; --no-history creates it with history capture off — omg history enable turns it on later)"],
|
|
23
23
|
["list", "list sources and which repos they feed"],
|
|
24
24
|
["attach <name> [--repo <slug>]", "attach an existing source to a repo"],
|
|
25
25
|
["detach <name> [--repo <slug>]", "detach a source from a repo"],
|
|
@@ -82,7 +82,7 @@ async function runAdd(cli, args) {
|
|
|
82
82
|
const { values, positionals } = parseArgs({
|
|
83
83
|
args,
|
|
84
84
|
allowPositionals: true,
|
|
85
|
-
options: { name: { type: "string" }, yes: { type: "boolean", short: "y" }, help: { type: "boolean" } },
|
|
85
|
+
options: { name: { type: "string" }, yes: { type: "boolean", short: "y" }, "no-history": { type: "boolean" }, help: { type: "boolean" } },
|
|
86
86
|
});
|
|
87
87
|
if (values.help)
|
|
88
88
|
return help(cli);
|
|
@@ -117,7 +117,9 @@ async function runAdd(cli, args) {
|
|
|
117
117
|
// A repo owns identity; register its fs source, then run the initial sync
|
|
118
118
|
// through the ordinary freshness path (the same reconcile every later sync
|
|
119
119
|
// uses). ensureRepo(…, null) mints identity only — the source is explicit.
|
|
120
|
-
|
|
120
|
+
// `--no-history` (store 17.0 Phase 4a, spec/store §3.1): the repository
|
|
121
|
+
// starts with capture off, so the import itself writes no history row.
|
|
122
|
+
const repoId = ensureRepo(ws.store, slug, null, values["no-history"] ? { history: false } : {});
|
|
121
123
|
ensureFsAdapter(ws.store);
|
|
122
124
|
const sourceId = createSource(ws.store, { name: sourceName, adapter: FS_ADAPTER, config: { root: abs } });
|
|
123
125
|
attachSourceToRepo(ws.store, repoId, sourceId);
|
package/dist/src/cmd/status.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { reposStatus, syncStatus, watchLeaseLive,
|
|
2
|
+
import { reposStatus, syncStatus, watchLeaseLive, resolveSettings, embeddingSettings } from "@omgbase/core";
|
|
3
3
|
import { EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
// `omg status` (11 §5.2): repos_status + sync_status + watch-lease probe +
|
|
5
5
|
// embedding queue depth. The "where am I" command — the visual showcase. The
|
|
6
6
|
// machine shape is the two tools' results, nested as they are on the wire
|
|
7
|
-
// (spec/cli §6): `{ ...repos_status, sync: sync_status, watcher, embedQueue }
|
|
7
|
+
// (spec/cli §6): `{ ...repos_status, sync: sync_status, watcher, embedQueue }`;
|
|
8
|
+
// the human grid's `commits` figure and `commit#` are the feed's head (store
|
|
9
|
+
// 17.0: `repos_status.feed`, `sync_status.head`).
|
|
8
10
|
function runStatus(cli, args) {
|
|
9
11
|
const { values } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean" } } });
|
|
10
12
|
if (values.help) {
|
|
@@ -23,7 +25,9 @@ function runStatus(cli, args) {
|
|
|
23
25
|
// Embedding queue depth (spec/search §2.3): the embeddable blocks whose
|
|
24
26
|
// current context has no cached vector — for the configured model when one is
|
|
25
27
|
// named, else for any. Counted from the cache alone: no provider is spawned.
|
|
26
|
-
|
|
28
|
+
// The count is the semantic capability's; a store without the module has
|
|
29
|
+
// nothing queued (modular-core Phase 2).
|
|
30
|
+
const queued = ws.store.capabilities.semantic?.staleCount(repo.repoId, embeddingSettings(resolveSettings(ws.store, repo.repoId)).model) ?? 0;
|
|
27
31
|
const payload = { ...rs, sync: ss, watcher: watcher ? "live" : "none", embedQueue: queued };
|
|
28
32
|
if (cli.flags.mode !== "human")
|
|
29
33
|
return emitMachine(cli, payload);
|
|
@@ -36,14 +40,14 @@ function runStatus(cli, args) {
|
|
|
36
40
|
const left = [
|
|
37
41
|
[`${style.path(g.doc)} docs`, String(rs.docs)],
|
|
38
42
|
[`${style.dim(g.block)} blocks`, String(rs.blocks)],
|
|
39
|
-
[`${g.diamond} commits`, String(rs.
|
|
43
|
+
[`${g.diamond} commits`, String(rs.feed.head)],
|
|
40
44
|
[`${g.arrow} edges`, String(rs.openEdges)],
|
|
41
45
|
];
|
|
42
46
|
const right = [
|
|
43
47
|
["watcher", watcher ? render.statusDot("live", "live") : render.statusDot("none", "none")],
|
|
44
|
-
["synced", ss.convergent ? render.statusDot("ok", "converged") : render.statusDot("warn", syncedLabel(ss
|
|
48
|
+
["synced", ss.convergent ? render.statusDot("ok", "converged") : render.statusDot("warn", syncedLabel(ss))],
|
|
45
49
|
["queue", queued === 0 ? style.dim("empty") : style.warn(`${queued} queued`)],
|
|
46
|
-
["commit#", style.dim(String(ss.
|
|
50
|
+
["commit#", style.dim(String(ss.head))],
|
|
47
51
|
];
|
|
48
52
|
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
49
53
|
const l = left[i];
|
|
@@ -54,11 +58,10 @@ function runStatus(cli, args) {
|
|
|
54
58
|
}
|
|
55
59
|
return EXIT_OK;
|
|
56
60
|
}
|
|
57
|
-
// Summarize why the repo isn't convergent:
|
|
58
|
-
|
|
61
|
+
// Summarize why the repo isn't convergent: on-disk drift (store 17.0, D9: the
|
|
62
|
+
// DB-internal `unconverged` count was identically zero and is gone).
|
|
63
|
+
function syncedLabel(ss) {
|
|
59
64
|
const parts = [];
|
|
60
|
-
if (unconverged > 0)
|
|
61
|
-
parts.push(`${unconverged} behind`);
|
|
62
65
|
if (ss.disk.deleted > 0)
|
|
63
66
|
parts.push(`${ss.disk.deleted} deleted`);
|
|
64
67
|
if (ss.disk.changed > 0)
|
package/dist/src/cmd/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { versionInfo } from "@omgbase/core";
|
|
2
|
+
import { versionInfo, repoEpoch, capabilityStates } from "@omgbase/core";
|
|
3
3
|
import { EXIT_OK, emitMachine, renderHelp } from "../output.js";
|
|
4
4
|
import { hostInfo } from "../build.js";
|
|
5
5
|
import { remoteCall } from "./_remote.js";
|
|
@@ -28,13 +28,27 @@ async function runVersion(cli, args) {
|
|
|
28
28
|
// `schema` is the open database's user_version; without a workspace the
|
|
29
29
|
// command still answers (spec/cli §3.7: version ∈ NO_WORKSPACE_OK).
|
|
30
30
|
let store = null;
|
|
31
|
+
let epoch = null;
|
|
32
|
+
let capabilities;
|
|
31
33
|
try {
|
|
32
|
-
|
|
34
|
+
const ws = cli.workspace();
|
|
35
|
+
store = ws.store;
|
|
36
|
+
// The default repository's feed epoch (spec/surface §4 `version.epoch`, 6.0) and
|
|
37
|
+
// its capability manifest (6.1, Phase 4a — `coverage_from`/`writer_requires`/`model`
|
|
38
|
+
// are the selected repository's row); the workspace layer's answer when none selects.
|
|
39
|
+
try {
|
|
40
|
+
const repoId = cli.repo(ws).repoId;
|
|
41
|
+
epoch = repoEpoch(store.db, repoId);
|
|
42
|
+
capabilities = capabilityStates(store, repoId);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
epoch = null;
|
|
46
|
+
}
|
|
33
47
|
}
|
|
34
48
|
catch {
|
|
35
49
|
store = null;
|
|
36
50
|
}
|
|
37
|
-
info = versionInfo(store, hostInfo());
|
|
51
|
+
info = versionInfo(store, hostInfo(), "full", capabilities, epoch);
|
|
38
52
|
}
|
|
39
53
|
cli.capture?.(info);
|
|
40
54
|
if (cli.flags.mode !== "human")
|
|
@@ -61,6 +75,9 @@ async function runVersion(cli, args) {
|
|
|
61
75
|
// The catalog profile served (spec/surface §4 "Profiles", 4.3): `full` for
|
|
62
76
|
// the library itself; over --server, whatever the remote serves.
|
|
63
77
|
line("profile", info.profile);
|
|
78
|
+
// The capability manifest (modular-core Phase 2) rides in `--json` only:
|
|
79
|
+
// the human rendering is pinned line for line by spec/cli (`read::version`,
|
|
80
|
+
// `invoke::version-verb-no-workspace`) and both binaries render it alike.
|
|
64
81
|
return EXIT_OK;
|
|
65
82
|
}
|
|
66
83
|
export const cmdVersion = {
|
package/dist/src/dispatch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { freshnessSweep, watchLeaseLive, withWriterLock } from "@omgbase/core";
|
|
1
|
+
import { activeAlerts, freshnessSweep, watchLeaseLive, withWriterLock } from "@omgbase/core";
|
|
2
2
|
import { NO_WORKSPACE_OK, SKIP_FRESHNESS, REMOTE_OK, asksForHelp } from "./context.js";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, renderError, usageFromParseArgs } from "./output.js";
|
|
4
4
|
import { resolveCommand, unknownCommandError } from "./commands.js";
|
|
@@ -159,7 +159,9 @@ export async function runCommand(cli, command, rest) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
const code = await resolved.run(cli, args);
|
|
163
|
+
printAlerts(cli, resolved.name, help);
|
|
164
|
+
return code;
|
|
163
165
|
}
|
|
164
166
|
catch (err) {
|
|
165
167
|
// A command's own `parseArgs` rejecting an option is a usage error (exit 2),
|
|
@@ -167,4 +169,27 @@ export async function runCommand(cli, command, rest) {
|
|
|
167
169
|
return renderError(usageFromParseArgs(err, cli.prog, resolved.name), cli.io, cli.style, cli.flags.mode !== "human");
|
|
168
170
|
}
|
|
169
171
|
}
|
|
172
|
+
// The alert channel on the CLI (spec/cli §3.1, store 17.0 — spec/store §3.1
|
|
173
|
+
// `alerts`): after a verb that ran against a repository, every ACTIVE alert of
|
|
174
|
+
// that repository is one stderr line, `alert[<severity>] <code>: <message>`,
|
|
175
|
+
// so a stalled materialization or a refused writer is told where the user
|
|
176
|
+
// works. Help, the workspace-less verbs, the servers (`mcp`, `shell` — whose
|
|
177
|
+
// lines each run through here) and `--server` (no local store) print none; a
|
|
178
|
+
// repository that does not resolve prints none either (the verb said so).
|
|
179
|
+
function printAlerts(cli, name, help) {
|
|
180
|
+
if (help || cli.flags.server !== undefined || NO_WORKSPACE_OK.has(name) || NO_ALERTS.has(name))
|
|
181
|
+
return;
|
|
182
|
+
let alerts;
|
|
183
|
+
try {
|
|
184
|
+
const ws = cli.workspace();
|
|
185
|
+
alerts = activeAlerts(ws.store.db, cli.repo(ws).repoId);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const a of alerts)
|
|
191
|
+
cli.io.err(`alert[${a.severity}] ${a.code}: ${a.message}`);
|
|
192
|
+
}
|
|
193
|
+
/** The verbs whose own run is a server or a session, not a command over the repository. */
|
|
194
|
+
const NO_ALERTS = new Set(["mcp", "shell"]);
|
|
170
195
|
//# sourceMappingURL=dispatch.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omgbase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Open Markdown Graph Base — CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"omg": "./dist/src/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@omgbase/core": "^0.
|
|
48
|
+
"@omgbase/core": "^0.31.0",
|
|
49
49
|
"@omgbase/fs-adapter": "^0.2.0",
|
|
50
|
-
"@omgbase/sync": "^0.
|
|
50
|
+
"@omgbase/sync": "^0.7.1",
|
|
51
51
|
"yaml": "^2.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|