omgbase 0.11.1 → 0.16.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.
@@ -1,12 +1,8 @@
1
1
  import { parseArgs } from "node:util";
2
- import { linksRetarget, surfaceRetargetHits } from "@omgbase/core";
2
+ import { applySugar, planEffects, retargetEffects, surfacePlan, surfaceReceipt } from "@omgbase/core";
3
3
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
4
- import { runOps, unifiedDiffLines } from "./_mutate.js";
5
- import { remoteCall } from "./_remote.js";
6
- // `omg retarget <from> <to> [--scope glob] [--apply]` (11 §5.6) — plan-by-default.
7
- // Without --apply it runs the dry-run and prints, per hit block, the unified
8
- // diff of its raw (the same diff every other preview prints — the 06 §4 "always
9
- // dry-run first" contract, encoded as the default). --apply commits.
4
+ import { defaultActor, renderPlan as renderPlanDiffs, renderReceipt, unifiedDiffLines } from "./_mutate.js";
5
+ import { remoteCall, remoteSugar } from "./_remote.js";
10
6
  async function runRetarget(cli, args) {
11
7
  const { values, positionals } = parseArgs({
12
8
  args,
@@ -29,32 +25,40 @@ async function runRetarget(cli, args) {
29
25
  const to = positionals[1];
30
26
  if (!from || !to)
31
27
  throw new CliUsageError("retarget requires <from> and <to> targets");
32
- // Remote: links_retarget resolves + (optionally) applies server-side.
28
+ const sugar = retargetEffects([{ from, to }], values.scope, "links_retarget");
29
+ // Remote: the `plan` tool previews, `links_retarget` commits server-side.
33
30
  if (cli.flags.server) {
34
- const r = await remoteCall(cli, "links_retarget", {
35
- from_target: from, to_target: to, dry_run: !values.apply, ...(values.scope ? { path_glob: values.scope } : {}),
36
- });
37
- if (values.apply) {
38
- if (cli.flags.mode !== "human")
39
- cli.io.out(JSON.stringify(r));
40
- else
41
- cli.io.err(cli.style.dim(` ${cli.style.ok(cli.render.g.ok)} retargeted ${r.hits.length} block(s)`));
42
- return EXIT_OK;
31
+ if (!values.apply || cli.flags.dryRun) {
32
+ const plan = await remoteCall(cli, "plan", { effects: sugar.effects });
33
+ return values.apply ? renderPlanDiffs(cli, plan) : renderHits(cli, from, to, retargetOf(plan).hits);
43
34
  }
44
- return renderPlan(cli, from, to, r.hits);
35
+ // `links_retarget` where listed (its result adds `hits`/`pairs`), else `apply { retarget }` (spec/cli §2.3, 4.2).
36
+ const r = await remoteSugar(cli, "links_retarget", { from_target: from, to_target: to, ...(values.scope ? { path_glob: values.scope } : {}) }, () => sugar.effects);
37
+ return renderReceipt(cli, r);
45
38
  }
46
39
  const ws = cli.workspace();
47
40
  const repo = cli.repo(ws);
48
- const { ops, hits: rawHits } = linksRetarget(ws.store, repo.repoId, from, to, values.scope ? { pathGlob: values.scope } : {});
49
- const hits = surfaceRetargetHits(rawHits); // the hits' document paths in the reference form
50
- // --apply → commit via the shared helper (honors --dry-run too, if combined).
51
- if (values.apply) {
52
- return runOps(cli, ws, repo, ops, values.actor ? { actor: values.actor } : {});
41
+ const request = { repoId: repo.repoId, ...(repo.rootPath ? { rootPath: repo.rootPath } : {}), omgbaseDir: ws.omgbaseDir };
42
+ if (!values.apply) {
43
+ // The plan (default): what WOULD change, commit nothing, mint nothing.
44
+ const plan = surfacePlan(planEffects(ws.store, { ...request, effects: sugar.effects }));
45
+ return renderHits(cli, from, to, retargetOf(plan).hits);
53
46
  }
54
- return renderPlan(cli, from, to, hits);
47
+ // --apply → commit (the global --dry-run plans and renders the file diffs instead, §3.6).
48
+ const { plan, receipt } = applySugar(ws.store, { ...request, actor: values.actor ?? defaultActor(), ...(cli.flags.dryRun ? { dryRun: true } : {}) }, sugar);
49
+ const rooted = surfacePlan(plan);
50
+ if (!receipt)
51
+ return renderPlanDiffs(cli, rooted);
52
+ const { hits, pairs } = retargetOf(rooted);
53
+ return renderReceipt(cli, { hits, pairs, ...surfaceReceipt(receipt) }, plan);
54
+ }
55
+ /** The one retarget effect's preview; a plan over no hit still carries it (empty hits, a zero count). */
56
+ function retargetOf(plan) {
57
+ const r = plan.preview?.retarget?.[0];
58
+ return { hits: r?.hits ?? [], pairs: r?.pairs ?? [] };
55
59
  }
56
60
  // Plan (default): show what WOULD change, commit nothing.
57
- function renderPlan(cli, from, to, hits) {
61
+ function renderHits(cli, from, to, hits) {
58
62
  if (cli.flags.mode === "json") {
59
63
  cli.io.out(JSON.stringify({ from, to, hits }));
60
64
  return EXIT_OK;
@@ -2,7 +2,7 @@ import { resolve, basename } from "node:path";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import { parseArgs } from "node:util";
4
4
  import { createInterface } from "node:readline";
5
- import { createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, ensureRepo, freshnessSweep, walkMarkdownAsync, rebuildFileStats, } from "@omgbase/core";
5
+ import { createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, ensureRepo, freshnessSweep, walkMarkdownAsync, rebuildFileStats, withWriterLock, } from "@omgbase/core";
6
6
  import { columns } from "../render.js";
7
7
  import { drainEmbeddings } from "./_embed.js";
8
8
  import { ensureFsAdapter, FS_ADAPTER } from "./_source.js";
@@ -121,7 +121,9 @@ async function runAdd(cli, args) {
121
121
  ensureFsAdapter(ws.store);
122
122
  const sourceId = createSource(ws.store, { name: sourceName, adapter: FS_ADAPTER, config: { root: abs } });
123
123
  attachSourceToRepo(ws.store, repoId, sourceId);
124
- const swept = freshnessSweep(ws.store, repoId, abs);
124
+ // The initial sweep is a checkpoint ingest: under the workspace writer lock
125
+ // (spec/sync §7), like every other sweep.
126
+ const swept = withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repoId, abs));
125
127
  rebuildFileStats(ws.store, repoId, abs);
126
128
  // Offer to embed freshly-ingested blocks now (consent-gated like the ingest).
127
129
  const drained = await drainEmbeddings(cli, ws, repoId, {
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { parseArgs } from "node:util";
3
- import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EngineError, surfaceSweepResult } from "@omgbase/core";
3
+ import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, withWriterLock, awaitReady, WATCH_READY_PATIENCE_MS, EngineError, surfaceSweepResult } from "@omgbase/core";
4
4
  import { runFsMirror } from "@omgbase/sync";
5
5
  import { EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
6
6
  import { loadEmbedding } from "./_embed.js";
@@ -64,12 +64,14 @@ function runOneShot(cli, ws, repo) {
64
64
  if (!repo.rootPath) {
65
65
  // Nothing to sweep: an empty-result note (stderr, §3.1), or the empty sweep shape.
66
66
  if (cli.flags.mode !== "human")
67
- return emitMachine(cli, { scanned: 0, ingested: [], deleted: [], conflicted: [], changed: false });
67
+ return emitMachine(cli, { scanned: 0, ingested: [], deleted: [], conflicted: [], changed: false, materialized: [] });
68
68
  cli.io.err(cli.style.dim(` ${repo.slug} has no filesystem source — nothing to sync`));
69
69
  return EXIT_OK;
70
70
  }
71
- // The paths the sweep names are printed in the reference form (spec/surface §1 "Paths").
72
- const result = surfaceSweepResult(freshnessSweep(ws.store, repo.repoId, repo.rootPath));
71
+ // The sweep is a checkpoint ingest: under the workspace writer lock (spec/sync
72
+ // §7). The paths it names are printed in the reference form (spec/surface §1 "Paths").
73
+ const root = repo.rootPath;
74
+ const result = surfaceSweepResult(withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root)));
73
75
  // A `SweepResult` is a record (its lists are per-kind): every machine mode is the document.
74
76
  if (cli.flags.mode !== "human")
75
77
  return emitMachine(cli, result);
@@ -137,7 +139,10 @@ async function runLocalWatch(cli, ws, repo) {
137
139
  await embedding.close();
138
140
  return EXIT_OK;
139
141
  }
142
+ // Checkpoints and the priming sweep run under the workspace writer lock
143
+ // (spec/sync §7), as the Rust watcher's do.
140
144
  const watcher = new Watcher(ws.store, repo.repoId, source, {
145
+ omgbaseDir: ws.omgbaseDir,
141
146
  onCheckpoint: (r) => {
142
147
  if (r.ingested.length || r.deleted.length || r.conflicted.length) {
143
148
  cli.io.err(`${cli.style.ok(cli.render.g.sync)} +${r.ingested.length} -${r.deleted.length}${r.conflicted.length ? ` !${r.conflicted.length}` : ""}`);
@@ -150,8 +155,9 @@ async function runLocalWatch(cli, ws, repo) {
150
155
  if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
151
156
  cli.io.err(cli.style.dim(` the fs adapter did not report ready within ${WATCH_READY_PATIENCE_MS / 1000}s — proceeding as if ready (an edit made before now may be missed until the next sweep)`));
152
157
  }
153
- if (repo.rootPath)
154
- freshnessSweep(ws.store, repo.repoId, repo.rootPath); // prime: anything that landed before the feed was ready
158
+ const root = repo.rootPath;
159
+ if (root)
160
+ withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root)); // prime: anything that landed before the feed was ready
155
161
  drainer?.schedule(); // embed anything already stale at startup, in the background
156
162
  cli.io.err(cli.style.dim(` watching ${repo.slug} — Ctrl-C to stop${drainer ? " · auto-embed on" : ""}`));
157
163
  await new Promise((resolveWait) => {
@@ -1,17 +1,19 @@
1
1
  import { parseArgs } from "node:util";
2
- import { docsUpdate, renderOpsetPlan, surfaceDocsUpdate } from "@omgbase/core";
2
+ import { applyEffectsDetailed, planEffects, referencePath, renderIdentityPlan, surfacePlan, surfaceReceipt } from "@omgbase/core";
3
3
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
4
- import { readContent, extractContentOpts } from "./_mutate.js";
4
+ import { readContent, extractContentOpts, receiptIds, defaultActor, renderSummary } from "./_mutate.js";
5
5
  import { cmdUpdate as cmdUpdateBlock } from "./mutate.js";
6
- import { remoteCall, docLocator, requireDoc } from "./_remote.js";
6
+ import { remoteCall, remoteSugar, docLocator, requireDoc } from "./_remote.js";
7
7
  // `omg update <target> (-f file | -)`: a polymorphic-target command (the omgbase
8
8
  // north-star — every command accepts any entity ref). A block id (b_…) replaces
9
9
  // that block's markdown (CAS-pinned, the low-level op). A doc id or path is the
10
- // note's docs.update: submit a complete proposed document; the engine reconciles
11
- // it against the current tree, preserves stable block ids for recognizably-same
12
- // structure, and commits the derived opset through the kernel write path. Global
13
- // --dry-run (or --plan) prints the plan (identity effects + summary), commits
14
- // nothing.
10
+ // whole-document update: the complete proposed document is ONE `set content`
11
+ // effect (spec/mutate §11.3; the `docs_update` tool's sugar, spec/surface 4.0) —
12
+ // the engine reconciles it against the current tree, preserves stable block ids
13
+ // for recognizably-same structure, and commits the derived ops as one
14
+ // transaction. The global --dry-run is the plan: it prints the identity
15
+ // consequences from the Plan's `preview.identity` (spec/cli §3.6, 4.0: every dry
16
+ // run is a plan and burns no id) and commits nothing.
15
17
  function runUpdate(cli, args) {
16
18
  // Peek at the target to dispatch: a `b_` token is a block replace — a token
17
19
  // that names no live block fails there as block_missing (spec/cli §6), never
@@ -28,18 +30,18 @@ async function runDocUpdate(cli, args) {
28
30
  const { values, positionals } = parseArgs({
29
31
  args: content.rest,
30
32
  allowPositionals: true,
31
- options: { actor: { type: "string" }, reason: { type: "string" }, plan: { type: "boolean" }, help: { type: "boolean" } },
33
+ options: { actor: { type: "string" }, reason: { type: "string" }, help: { type: "boolean" } },
32
34
  });
33
35
  if (values.help) {
34
36
  return renderHelp(cli, {
35
37
  name: "update",
36
38
  summary: "Replace a block's markdown (b_… target), or reconcile a whole document from complete new bytes (doc id/path)",
37
- usage: "update <block|doc> (-m <markdown> | -f <file> | -) [--plan] [--expect <hash>] [--reason <s>] [--actor <s>] [--dry-run]",
39
+ usage: "update <block|doc> (-m <markdown> | -f <file> | -) [--expect <hash>] [--reason <s>] [--actor <s>] [--dry-run]",
38
40
  options: [
39
41
  ["-m <markdown>", "content inline"],
40
42
  ["-f <file>", "content from a file"],
41
43
  ["-", "content from stdin"],
42
- ["--plan", "(doc) print the reconciliation opset — identity effects + summary — and commit nothing"],
44
+ ["--dry-run", "(doc) print the reconciliation plan — identity effects + summary — and commit nothing"],
43
45
  ["--expect <hash>", "(block) fail with stale_expectation unless the block's hash still matches"],
44
46
  ["--reason <s>", "commit reason recorded in history"],
45
47
  ["--actor <s>", "commit actor (default human:$USER)"],
@@ -50,54 +52,66 @@ async function runDocUpdate(cli, args) {
50
52
  if (!doc)
51
53
  throw new CliUsageError("update requires a <doc> (id or path)");
52
54
  const bytes = readContent(content);
53
- const dryRun = Boolean(values.plan) || cli.flags.dryRun;
54
- let opset;
55
- let result;
55
+ const dryRun = cli.flags.dryRun;
56
+ let plan;
57
+ let receipt;
56
58
  if (cli.flags.server) {
57
- // Remote: docs_update reconciles + commits server-side, returning the same
58
- // { opset, result } — render unchanged.
59
- const r = await remoteCall(cli, "docs_update", {
60
- ...docLocator(doc),
61
- content: bytes,
62
- ...(values.reason ? { reason: values.reason } : {}),
63
- ...(dryRun ? { dry_run: true } : {}),
64
- });
65
- opset = r.opset;
66
- result = r.result;
59
+ // Remote: the `docs_update` tool commits the `set content` server-side and
60
+ // returns the Receipt (the confirmation then lists the ids it minted, as a
61
+ // remote `apply` does); the dry run is the `plan` tool over the same effect.
62
+ // A server without `docs_update` (a minimal profile) gets `apply` over the same effect (spec/cli §2.3, 4.2).
63
+ const setContent = { op: "set", target: { kind: "document", ...docLocator(doc) }, field: "content", value: bytes };
64
+ if (dryRun)
65
+ plan = await remoteCall(cli, "plan", { effects: [setContent] });
66
+ else
67
+ receipt = await remoteSugar(cli, "docs_update", { ...docLocator(doc), content: bytes, ...(values.reason ? { reason: values.reason } : {}) }, () => [setContent]);
67
68
  }
68
69
  else {
69
70
  const ws = cli.workspace();
70
71
  const repo = cli.repo(ws);
71
72
  if (!repo.rootPath)
72
73
  throw new CliUsageError(`repo '${repo.slug}' has no filesystem source; 'update' needs a working tree`);
73
- const ctx = {
74
+ const effects = [{ op: "set", target: { kind: "document", id: requireDoc(ws.store, repo.repoId, doc) }, field: "content", value: bytes }];
75
+ const req = {
74
76
  repoId: repo.repoId,
75
77
  rootPath: repo.rootPath,
78
+ effects,
79
+ origin: { actor: values.actor ?? defaultActor(), ...(values.reason ? { reason: values.reason } : {}) },
76
80
  ...(ws.omgbaseDir ? { omgbaseDir: ws.omgbaseDir } : {}),
77
- ...(values.actor ? { actor: values.actor } : {}),
78
81
  };
79
- ({ opset, result } = surfaceDocsUpdate(docsUpdate(ws.store, ctx, requireDoc(ws.store, repo.repoId, doc), bytes, {
80
- dryRun,
81
- ...(values.reason ? { reason: values.reason } : {}),
82
- })));
82
+ if (dryRun) {
83
+ plan = surfacePlan(planEffects(ws.store, req));
84
+ }
85
+ else {
86
+ const res = applyEffectsDetailed(ws.store, req);
87
+ plan = surfacePlan(res.plan);
88
+ receipt = res.receipt === undefined ? undefined : surfaceReceipt(res.receipt);
89
+ }
83
90
  }
84
91
  if (cli.flags.mode !== "human") {
85
- cli.io.out(JSON.stringify({ opset, result }));
92
+ cli.io.out(JSON.stringify(receipt ?? plan));
86
93
  return EXIT_OK;
87
94
  }
88
- if (dryRun || result === null) {
89
- const s = opset.summary;
90
- cli.io.err(cli.style.dim(` plan for ${cli.style.accent(opset.target.path)}${opset.converges ? "" : " — DOES NOT CONVERGE (will not apply)"}`));
91
- cli.io.out(renderOpsetPlan(opset));
92
- void s;
95
+ if (receipt === undefined) {
96
+ // The plan: its one effect names the document; `preview.identity[<id>]` is
97
+ // the opset plan (one line per op, an empty line, the summary).
98
+ const target = plan.effects[0] && "target" in plan.effects[0] ? plan.effects[0].target : undefined;
99
+ const docId = target && target.kind === "document" ? target.id : undefined;
100
+ const path = target && target.kind === "document" ? referencePath(target.path) : doc;
101
+ const identity = docId !== undefined ? plan.preview?.identity?.[docId] : undefined;
102
+ cli.io.err(cli.style.dim(` plan for ${cli.style.accent(path)}`));
103
+ if (identity)
104
+ cli.io.out(renderIdentityPlan(identity));
93
105
  return EXIT_OK;
94
106
  }
107
+ // The Receipt confirmation (§4): one revision per touched document; every id
108
+ // the plan's results name, once each (a block appears in several ops' results
109
+ // — an update then a retile of the same block — and is listed a single time).
95
110
  const g = cli.render.g;
96
- cli.io.err(cli.style.dim(` ${cli.style.ok(g.ok)} updated ${cli.style.accent(opset.target.path)} · ${opset.summary.preserved} preserved, ${opset.summary.updated} updated, ${opset.summary.moved} moved, ${opset.summary.created} created, ${opset.summary.removed} removed`));
97
- // Every id the opset touched, once each: a block appears in several ops'
98
- // results (an update then a retile of the same block), the confirmation
99
- // lists it a single time.
100
- for (const id of new Set(result.results.flatMap((r) => r.ids)))
111
+ const n = receipt.revisions.length;
112
+ cli.io.err(cli.style.dim(` ${cli.style.ok(g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
113
+ renderSummary(cli, receipt.summary);
114
+ for (const id of new Set(receiptIds(receipt, plan)))
101
115
  cli.io.out(id);
102
116
  return EXIT_OK;
103
117
  }
@@ -41,7 +41,7 @@ async function runVersion(cli, args) {
41
41
  return emitMachine(cli, info);
42
42
  const { io, style } = cli;
43
43
  const dash = "—";
44
- const keys = ["engine", "version", "specs", "schema", "mcp", "runtime", "commit", "built"];
44
+ const keys = ["engine", "version", "specs", "schema", "mcp", "runtime", "commit", "built", "profile"];
45
45
  const width = Math.max(...keys.map((k) => k.length));
46
46
  const line = (key, value) => {
47
47
  io.out(`${style.dim(key.padEnd(width))} ${value === null ? style.dim(dash) : String(value)}`);
@@ -58,6 +58,9 @@ async function runVersion(cli, args) {
58
58
  line("runtime", info.runtime);
59
59
  line("commit", info.commit);
60
60
  line("built", info.built);
61
+ // The catalog profile served (spec/surface §4 "Profiles", 4.3): `full` for
62
+ // the library itself; over --server, whatever the remote serves.
63
+ line("profile", info.profile);
61
64
  return EXIT_OK;
62
65
  }
63
66
  export const cmdVersion = {
@@ -13,7 +13,7 @@ import { cmdDiff } from "./cmd/diff.js";
13
13
  import { cmdLinks } from "./cmd/links.js";
14
14
  import { cmdSync } from "./cmd/sync.js";
15
15
  import { cmdMcp } from "./cmd/mcp.js";
16
- import { cmdApply, cmdInsert, cmdMove, cmdRm, cmdDone, cmdAppend, cmdSplit, cmdMerge } from "./cmd/mutate.js";
16
+ import { cmdApply, cmdExecute, cmdInsert, cmdMove, cmdRm, cmdDone, cmdAppend, cmdSplit, cmdMerge } from "./cmd/mutate.js";
17
17
  import { cmdEdit } from "./cmd/edit.js";
18
18
  import { cmdRetarget } from "./cmd/retarget.js";
19
19
  import { cmdNode } from "./cmd/node.js";
@@ -40,6 +40,7 @@ export const COMMANDS = [
40
40
  cmdDiff,
41
41
  cmdLinks,
42
42
  cmdApply,
43
+ cmdExecute,
43
44
  cmdInsert,
44
45
  cmdUpdateDoc,
45
46
  cmdEdit,
@@ -85,8 +85,9 @@ export const REMOTE_OK = new Set([
85
85
  // doc-level mutators
86
86
  "new", "mv", "meta", "rm", "update", "retarget",
87
87
  // block-level sugar (server-side ref resolution + CAS via the blocks_* /
88
- // blocks_check / blocks_append / nodes_set / apply tools)
89
- "apply", "insert", "move", "split", "merge", "done", "append", "node",
88
+ // blocks_check / blocks_append / nodes_set / apply tools); `execute` is the
89
+ // `execute` tool's plan form
90
+ "apply", "execute", "insert", "move", "split", "merge", "done", "append", "node",
90
91
  // shell accepts --server itself, then threads it into every line it runs.
91
92
  "shell",
92
93
  // version asks the REMOTE engine which engine/versions it is (spec/cli §6).
@@ -1,4 +1,4 @@
1
- import { freshnessSweep, watchLeaseLive } from "@omgbase/core";
1
+ import { 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";
@@ -135,7 +135,11 @@ export async function runCommand(cli, command, rest) {
135
135
  try {
136
136
  // Freshness sweep (11 §3.3): current-by-default, unless --stale, a live
137
137
  // watcher holds the lease, remote (`--server`) mode (no local tree to sweep),
138
- // or the command manages sync itself.
138
+ // or the command manages sync itself. The sweep is a checkpoint ingest, so it
139
+ // runs under the workspace writer lock (spec/sync §7) like every other
140
+ // host's; a writer that follows re-enters the lock. A lock held past the
141
+ // wait fails the command as the catch-all engine error (exit 1), like any
142
+ // write would — `--stale` skips the sweep and so never contends.
139
143
  if (!help && !cli.flags.stale && cli.flags.server === undefined && !SKIP_FRESHNESS.has(resolved.name) && !NO_WORKSPACE_OK.has(resolved.name)) {
140
144
  const ws = cli.workspace();
141
145
  if (!watchLeaseLive(ws.omgbaseDir)) {
@@ -145,8 +149,9 @@ export async function runCommand(cli, command, rest) {
145
149
  // repo_not_found if it truly needs a repo.
146
150
  try {
147
151
  const repo = cli.repo(ws);
148
- if (repo.rootPath)
149
- freshnessSweep(ws.store, repo.repoId, repo.rootPath);
152
+ const root = repo.rootPath;
153
+ if (root)
154
+ withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root));
150
155
  }
151
156
  catch (err) {
152
157
  if (!(err instanceof EngineErrorLike && err.code === "repo_not_found"))
@@ -103,6 +103,15 @@ export declare function renderHits(cli: {
103
103
  io: IO;
104
104
  style: Style;
105
105
  }, hits: HitLike[]): void;
106
+ /** The bare hit table (`query --bare`, spec/cli §4, 4.1): a hit is its
107
+ * projection alone (`spec/surface` §1.4, 4.2), so the columns are every key in
108
+ * first-seen order across the hits — a projected `id`/`path` is an ordinary
109
+ * column, rendered as any other cell — with no envelope columns before them.
110
+ * Header row, then one row per hit; cells as the enveloped table's. */
111
+ export declare function renderBareHits(cli: {
112
+ io: IO;
113
+ style: Style;
114
+ }, hits: Record<string, unknown>[]): void;
106
115
  /** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
107
116
  export declare function truncationFooter(io: IO, style: Style, cursor: number | string): void;
108
117
  export {};
@@ -231,6 +231,29 @@ export function renderHits(cli, hits) {
231
231
  for (const line of columns(rows))
232
232
  io.out(line);
233
233
  }
234
+ /** The bare hit table (`query --bare`, spec/cli §4, 4.1): a hit is its
235
+ * projection alone (`spec/surface` §1.4, 4.2), so the columns are every key in
236
+ * first-seen order across the hits — a projected `id`/`path` is an ordinary
237
+ * column, rendered as any other cell — with no envelope columns before them.
238
+ * Header row, then one row per hit; cells as the enveloped table's. */
239
+ export function renderBareHits(cli, hits) {
240
+ const { io, style } = cli;
241
+ const cols = [];
242
+ const seen = new Set();
243
+ for (const h of hits) {
244
+ for (const k of Object.keys(h)) {
245
+ if (seen.has(k))
246
+ continue;
247
+ seen.add(k);
248
+ cols.push(k);
249
+ }
250
+ }
251
+ const rows = [cols.map((c) => style.dim(c))];
252
+ for (const h of hits)
253
+ rows.push(cols.map((c) => hitCell(h[c])));
254
+ for (const line of columns(rows))
255
+ io.out(line);
256
+ }
234
257
  /** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
235
258
  export function truncationFooter(io, style, cursor) {
236
259
  io.err(style.dim(`… truncated; continue with --cursor ${cursor}`));
@@ -1,8 +1,11 @@
1
+ export declare const WRITER_LOCK_TIMEOUT_ENV = "OMGBASE_WRITER_LOCK_TIMEOUT_MS";
1
2
  export interface SpecSeams {
2
3
  /** the sequential minter is installed */
3
4
  minter: boolean;
4
5
  /** the pinned instant (canonical ISO form), when the clock is pinned */
5
6
  clock: string | null;
7
+ /** the writer lock's wait in ms, when overridden */
8
+ writerLockTimeoutMs: number | null;
6
9
  }
7
10
  /**
8
11
  * Read + install the seams (idempotent: a second call returns what the first
package/dist/src/seams.js CHANGED
@@ -1,5 +1,32 @@
1
- import { pinClock, sequentialMinter, setIdMinter } from "@omgbase/core";
1
+ import { pinClock, sequentialMinter, setIdMinter, setWriterLockDefaults } from "@omgbase/core";
2
2
  import { CliUsageError } from "./output.js";
3
+ // The two conformance seams of spec/surface §7.1 (and spec/cli §2.6), read
4
+ // from the environment at process start — BEFORE any workspace opens, so no id
5
+ // is minted and no timestamp read without them. Every verb honors them (the
6
+ // CLI fixtures of spec/cli run each verb under them); `omg mcp` additionally
7
+ // announces them on stderr. For conformance runs only — never set in production:
8
+ //
9
+ // OMGBASE_SPEC_MINTER=sequential the fixture minter of spec/store §2.2 for
10
+ // the whole process (`d_0, d_1, …`, each
11
+ // prefix from 0); any other value is a
12
+ // usage error (exit 2)
13
+ // OMGBASE_SPEC_CLOCK=<RFC 3339> "now" is that instant for the whole
14
+ // process: every commit a verb stamps, every
15
+ // `ts` reported as current; not an instant →
16
+ // usage error (exit 2)
17
+ //
18
+ // A third seam is this engine's own, not the spec's (the Rust binary reads the
19
+ // same variable):
20
+ //
21
+ // OMGBASE_WRITER_LOCK_TIMEOUT_MS=<n> how long a write waits for the workspace
22
+ // writer lock (spec/sync §7: 5 s when unset)
23
+ // — for a test that proves a held lock fails
24
+ // a write without sitting out the full wait.
25
+ // Honored by every lock this process takes
26
+ // (a verb's kernel-level lock, `omg mcp`'s
27
+ // tool boundary, the watcher's checkpoints);
28
+ // empty = unset, not an integer → usage error
29
+ export const WRITER_LOCK_TIMEOUT_ENV = "OMGBASE_WRITER_LOCK_TIMEOUT_MS";
3
30
  let installed = null;
4
31
  /**
5
32
  * Read + install the seams (idempotent: a second call returns what the first
@@ -8,7 +35,7 @@ let installed = null;
8
35
  export function installSpecSeams(env = process.env) {
9
36
  if (installed)
10
37
  return installed;
11
- const seams = { minter: false, clock: null };
38
+ const seams = { minter: false, clock: null, writerLockTimeoutMs: null };
12
39
  const minter = env.OMGBASE_SPEC_MINTER;
13
40
  if (minter !== undefined && minter !== "") {
14
41
  if (minter !== "sequential") {
@@ -27,11 +54,20 @@ export function installSpecSeams(env = process.env) {
27
54
  }
28
55
  seams.clock = new Date().toISOString();
29
56
  }
57
+ const lockWait = env[WRITER_LOCK_TIMEOUT_ENV];
58
+ if (lockWait !== undefined && lockWait !== "") {
59
+ const ms = /^\s*\d+\s*$/.test(lockWait) ? Number(lockWait) : NaN;
60
+ if (!Number.isSafeInteger(ms)) {
61
+ throw new CliUsageError(`${WRITER_LOCK_TIMEOUT_ENV}=${JSON.stringify(lockWait)}: not a number of milliseconds`, `unset it, or e.g. ${WRITER_LOCK_TIMEOUT_ENV}=200 for a test run`);
62
+ }
63
+ setWriterLockDefaults({ timeoutMs: ms });
64
+ seams.writerLockTimeoutMs = ms;
65
+ }
30
66
  installed = seams;
31
67
  return seams;
32
68
  }
33
69
  /** The seams installed by `installSpecSeams` (none when it has not run). */
34
70
  export function activeSpecSeams() {
35
- return installed ?? { minter: false, clock: null };
71
+ return installed ?? { minter: false, clock: null, writerLockTimeoutMs: null };
36
72
  }
37
73
  //# sourceMappingURL=seams.js.map
@@ -43,16 +43,9 @@ export function deriveRows(value) {
43
43
  return o.hits.map(rowOf); // query/oqx
44
44
  if (Array.isArray(o.digests))
45
45
  return o.digests.map(rowOf); // log
46
- if (Array.isArray(o.results) && Array.isArray(o.revisions)) {
47
- // ApplyResult: the minted/affected ids, flattened (one row per id).
48
- const ids = [];
49
- for (const r of o.results) {
50
- if (Array.isArray(r.ids))
51
- for (const id of r.ids)
52
- if (typeof id === "string")
53
- ids.push(id);
54
- }
55
- return ids.map((id) => ({ ref: id, label: id, value: id }));
46
+ if (typeof o.plan_id === "string" && Array.isArray(o.ids)) {
47
+ // A write's Receipt as the verbs capture it: the ids the write names (one row per id).
48
+ return o.ids.filter((id) => typeof id === "string").map((id) => ({ ref: id, label: id, value: id }));
56
49
  }
57
50
  if (Array.isArray(o.in) || Array.isArray(o.out)) {
58
51
  // links: both directions, each edge's far node is the selectable ref.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omgbase",
3
- "version": "0.11.1",
3
+ "version": "0.16.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.11.1",
48
+ "@omgbase/core": "^0.19.0",
49
49
  "@omgbase/fs-adapter": "^0.2.0",
50
- "@omgbase/sync": "^0.5.1",
50
+ "@omgbase/sync": "^0.5.12",
51
51
  "yaml": "^2.9.0"
52
52
  },
53
53
  "devDependencies": {