@omgbase/core 0.11.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/dist/src/core/ids.d.ts +4 -0
  3. package/dist/src/core/ids.js +17 -0
  4. package/dist/src/core/ingest.d.ts +39 -3
  5. package/dist/src/core/ingest.js +233 -202
  6. package/dist/src/core/store/schema.d.ts +3 -2
  7. package/dist/src/core/store/schema.js +28 -3
  8. package/dist/src/core/store/store.d.ts +9 -0
  9. package/dist/src/core/store/store.js +11 -0
  10. package/dist/src/graph/history.js +15 -12
  11. package/dist/src/index.d.ts +9 -5
  12. package/dist/src/index.js +14 -7
  13. package/dist/src/mcp/arguments.d.ts +9 -2
  14. package/dist/src/mcp/arguments.js +15 -2
  15. package/dist/src/mcp/errors.d.ts +1 -1
  16. package/dist/src/mcp/errors.js +3 -0
  17. package/dist/src/mcp/reference.d.ts +1 -1
  18. package/dist/src/mcp/reference.js +9 -9
  19. package/dist/src/mcp/server.d.ts +22 -0
  20. package/dist/src/mcp/server.js +263 -186
  21. package/dist/src/mutate/apply.d.ts +93 -12
  22. package/dist/src/mutate/apply.js +203 -113
  23. package/dist/src/mutate/commit.d.ts +150 -0
  24. package/dist/src/mutate/commit.js +349 -0
  25. package/dist/src/mutate/doc-store.d.ts +3 -3
  26. package/dist/src/mutate/doc-store.js +21 -6
  27. package/dist/src/mutate/docs.d.ts +42 -87
  28. package/dist/src/mutate/docs.js +50 -204
  29. package/dist/src/mutate/effects.d.ts +331 -0
  30. package/dist/src/mutate/effects.js +981 -0
  31. package/dist/src/mutate/materialize.d.ts +49 -0
  32. package/dist/src/mutate/materialize.js +174 -0
  33. package/dist/src/mutate/opset.d.ts +27 -2
  34. package/dist/src/mutate/opset.js +12 -5
  35. package/dist/src/mutate/plan-update.d.ts +1 -46
  36. package/dist/src/mutate/plan-update.js +19 -67
  37. package/dist/src/mutate/plan.d.ts +200 -0
  38. package/dist/src/mutate/plan.js +472 -0
  39. package/dist/src/mutate/sugar.d.ts +135 -0
  40. package/dist/src/mutate/sugar.js +167 -0
  41. package/dist/src/mutate/tree.d.ts +4 -2
  42. package/dist/src/oqx-js/run.js +1 -1
  43. package/dist/src/spec-versions.d.ts +5 -5
  44. package/dist/src/spec-versions.js +5 -5
  45. package/dist/src/surface-paths.d.ts +17 -26
  46. package/dist/src/surface-paths.js +27 -22
  47. package/dist/src/sync/checkpoint.d.ts +5 -2
  48. package/dist/src/sync/checkpoint.js +10 -6
  49. package/dist/src/sync/freshness.d.ts +10 -7
  50. package/dist/src/sync/freshness.js +15 -17
  51. package/dist/src/sync/observe.d.ts +4 -1
  52. package/dist/src/sync/observe.js +56 -5
  53. package/dist/src/sync/recovery.d.ts +4 -0
  54. package/dist/src/sync/recovery.js +4 -1
  55. package/dist/src/sync/stat-cache.d.ts +7 -0
  56. package/dist/src/sync/stat-cache.js +22 -0
  57. package/dist/src/sync/tombstone.js +23 -19
  58. package/dist/src/sync/watcher.d.ts +2 -0
  59. package/dist/src/sync/watcher.js +4 -1
  60. package/dist/src/sync/writer-lock.d.ts +15 -0
  61. package/dist/src/sync/writer-lock.js +104 -17
  62. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
- export declare const SCHEMA_VERSION = 13;
1
+ export declare const SCHEMA_VERSION = 14;
2
2
  export declare const FILE_STATS_DDL = "\nCREATE TABLE IF NOT EXISTS file_stats (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n mtime_ns INTEGER NOT NULL,\n size INTEGER NOT NULL,\n hash BLOB NOT NULL,\n PRIMARY KEY (repo_id, path)\n);\n";
3
3
  export declare const PROPERTIES_DDL = "\nCREATE TABLE IF NOT EXISTS properties (\n prop_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT, -- NULL = document-scoped (frontmatter, computed)\n source TEXT NOT NULL CHECK (source IN ('frontmatter','inline','computed')),\n key TEXT NOT NULL, -- dotted, flattened: \"layer\",\"meta.owner\"; computed carry $: \"$title\"\n card TEXT NOT NULL CHECK (card IN ('scalar','list')),\n ord INTEGER NOT NULL DEFAULT 0,\n val_text TEXT,\n val_num REAL,\n val_bool INTEGER,\n val_json TEXT,\n type TEXT NOT NULL CHECK (type IN ('string','number','bool','null','json')),\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_props_doc ON properties(doc_id) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_text ON properties(repo_id, key, val_text) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_num ON properties(repo_id, key, val_num) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_src_key ON properties(repo_id, source, key) WHERE deleted_commit IS NULL;\n";
4
4
  export declare const SYNC_DDL = "\nCREATE TABLE IF NOT EXISTS adapters (\n name TEXT PRIMARY KEY, -- workspace-unique adapter name (e.g. \"fs\")\n command TEXT NOT NULL, -- argv[0] to spawn (e.g. \"omgbase-fs-adapter\")\n args TEXT NOT NULL DEFAULT '[]' -- fixed leading args (JSON string[])\n);\n\nCREATE TABLE IF NOT EXISTS sources (\n source_id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE, -- workspace-unique source name\n adapter TEXT NOT NULL REFERENCES adapters(name),\n config TEXT NOT NULL DEFAULT '{}', -- JSON object \u2192 rendered to flags (13 \u00A73.1)\n env TEXT NOT NULL DEFAULT '{}' -- JSON object \u2192 spawn env (secrets; 13 \u00A73.2)\n);\n\nCREATE TABLE IF NOT EXISTS attachments (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n source_id TEXT NOT NULL REFERENCES sources(source_id),\n PRIMARY KEY (repo_id, source_id)\n);\n\nCREATE TABLE IF NOT EXISTS sync_state (\n repo_id TEXT NOT NULL,\n source_id TEXT NOT NULL,\n path TEXT NOT NULL, -- '' reserved for the attachment-level cursor row\n revision TEXT, -- last-observed source revision for this path\n cursor TEXT, -- last poll/webhook cursor (attachment-level row)\n PRIMARY KEY (repo_id, source_id, path)\n);\n";
5
5
  export declare const WORKSPACE_SETTINGS_DDL = "\nCREATE TABLE IF NOT EXISTS workspace_settings (\n id INTEGER PRIMARY KEY CHECK (id = 0),\n settings TEXT NOT NULL DEFAULT '{}'\n);\nINSERT OR IGNORE INTO workspace_settings (id, settings) VALUES (0, '{}');\n";
6
6
  export declare const NODES_DDL = "\nCREATE TABLE IF NOT EXISTS nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT,\n kind TEXT NOT NULL,\n name TEXT,\n value TEXT,\n span_start INTEGER,\n span_end INTEGER,\n attrs TEXT NOT NULL DEFAULT '{}'\n);\nCREATE INDEX IF NOT EXISTS idx_nodes_doc ON nodes(doc_id);\nCREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name) WHERE name IS NOT NULL;\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name, value, content='nodes', content_rowid='rowid', tokenize='porter unicode61'\n);\n";
7
7
  export declare const DOC_EMBEDDINGS_DDL = "\nCREATE TABLE IF NOT EXISTS doc_embeddings (\n doc_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_hash BLOB NOT NULL,\n method TEXT NOT NULL CHECK (method IN ('whole','pooled')),\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (doc_id, model)\n);\n";
8
+ export declare const PENDING_MATERIALIZATIONS_DDL = "\nCREATE TABLE IF NOT EXISTS pending_materializations (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL, -- where the file must end up\n kind TEXT NOT NULL CHECK (kind IN ('write','rename','remove')),\n from_path TEXT, -- rename: the old path\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n rev_id TEXT REFERENCES revisions(rev_id), -- write/rename: the revision the bytes render (NULL for remove)\n rendered_hash BLOB NOT NULL, -- the bytes expected at path (remove: the bytes it is safe to unlink)\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n PRIMARY KEY (repo_id, path)\n);\n";
8
9
  export declare const MIGRATIONS: Record<number, string>;
9
- export declare const DDL = "\n-- ---- durable tables (02 \u00A73) --------------------------------------------------\nCREATE TABLE IF NOT EXISTS repos (\n repo_id TEXT PRIMARY KEY,\n slug TEXT NOT NULL UNIQUE,\n settings TEXT NOT NULL DEFAULT '{}'\n);\n-- A repo owns identity + history, NOT a filesystem (ADR-014). Where its bytes\n-- come from is a sources row (adapter=fs, config.root) joined via attachments;\n-- the former repos.root_path column was removed in schema v13.\n\nCREATE TABLE IF NOT EXISTS docs (\n doc_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL,\n format TEXT NOT NULL DEFAULT 'markdown',\n current_rev TEXT,\n file_hash BLOB,\n conflicted INTEGER NOT NULL DEFAULT 0,\n leading_trivia TEXT NOT NULL DEFAULT '',\n frontmatter_trivia TEXT, -- separator between frontmatter and first body block (NULL = no frontmatter)\n deleted_commit TEXT,\n UNIQUE (repo_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS blocks (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n parent_block TEXT,\n order_key TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n depth INTEGER NOT NULL,\n ancestor_path TEXT NOT NULL,\n type TEXT NOT NULL,\n attrs TEXT NOT NULL DEFAULT '{}',\n text TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n trivia_hash BLOB, -- trailing trivia blob hash (NULL = no trivia)\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_blocks_doc ON blocks(doc_id, parent_block, order_key) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(repo_id, type) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_rawhash ON blocks(raw_hash);\nCREATE INDEX IF NOT EXISTS idx_blocks_ancestor ON blocks(doc_id, ancestor_path);\n\nCREATE TABLE IF NOT EXISTS blobs (\n hash BLOB PRIMARY KEY,\n bytes BLOB NOT NULL,\n size INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS tree_nodes (\n hash BLOB PRIMARY KEY,\n entries TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS revisions (\n rev_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n seq INTEGER NOT NULL,\n root_tree BLOB NOT NULL REFERENCES tree_nodes(hash),\n frontmatter_blob BLOB REFERENCES blobs(hash),\n rendered_hash BLOB NOT NULL,\n path TEXT NOT NULL,\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n UNIQUE (doc_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS commits (\n commit_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n seq INTEGER NOT NULL,\n ts TEXT NOT NULL,\n origin TEXT NOT NULL CHECK (origin IN ('api','observed','import','projection')),\n actor TEXT,\n reason TEXT,\n checkpoint_id TEXT,\n ops TEXT,\n UNIQUE (repo_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS dispositions (\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n block_id TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN\n ('same','edited','moved','edited_moved','inserted','deleted',\n 'split_from','merged_into','copied_from','resurrected','bulk_rewrite')),\n confidence REAL,\n reason TEXT,\n matcher_v TEXT,\n detail TEXT NOT NULL DEFAULT '{}',\n PRIMARY KEY (commit_id, block_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n edge_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n src_doc TEXT NOT NULL,\n src_block TEXT,\n src_field TEXT,\n predicate TEXT NOT NULL,\n dst_kind TEXT NOT NULL CHECK (dst_kind IN ('document','block','external','collection')),\n dst_node TEXT NOT NULL,\n anchor TEXT,\n provenance TEXT NOT NULL CHECK (provenance IN ('link','frontmatter','inline_field','projected',\n 'yaml_ref','yaml_schema','yaml_extends','json_ref','json_schema')),\n via_node TEXT,\n from_commit TEXT NOT NULL,\n to_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_doc, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_node, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_temporal ON edges(from_commit, to_commit);\n\nCREATE TABLE IF NOT EXISTS external_nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n uri TEXT NOT NULL,\n title TEXT,\n UNIQUE (repo_id, uri)\n);\n\nCREATE TABLE IF NOT EXISTS collections (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n name TEXT NOT NULL,\n spec TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS checkpoints (\n id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n ts TEXT NOT NULL,\n files TEXT NOT NULL,\n git_head TEXT\n);\n\nCREATE TABLE IF NOT EXISTS resurrection_pool (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n type TEXT NOT NULL,\n deleted_commit TEXT NOT NULL,\n expires_ts TEXT NOT NULL\n);\n\n-- ---- derived tables (02 \u00A74; rebuildable) ------------------------------------\nCREATE TABLE IF NOT EXISTS sections (\n heading_block TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n level INTEGER NOT NULL,\n first_ordinal INTEGER NOT NULL,\n last_ordinal INTEGER NOT NULL,\n PRIMARY KEY (doc_id, heading_block)\n);\n\nCREATE TABLE IF NOT EXISTS doc_edges (\n src_doc TEXT NOT NULL,\n predicate TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n dst_kind TEXT NOT NULL,\n count INTEGER NOT NULL,\n samples TEXT NOT NULL,\n PRIMARY KEY (src_doc, predicate, dst_node)\n);\n\nCREATE TABLE IF NOT EXISTS block_changes (\n block_id TEXT NOT NULL,\n commit_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n PRIMARY KEY (block_id, commit_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS inferred_edges (\n src_node TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n predicate TEXT NOT NULL,\n method TEXT NOT NULL,\n score REAL NOT NULL,\n model_v TEXT NOT NULL,\n computed_at TEXT NOT NULL,\n PRIMARY KEY (src_node, dst_node, predicate, method)\n);\n\nCREATE TABLE IF NOT EXISTS embeddings (\n content_hash BLOB NOT NULL,\n ctx_hash BLOB NOT NULL,\n model TEXT NOT NULL,\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (content_hash, ctx_hash, model)\n);\n\n\nCREATE TABLE IF NOT EXISTS doc_embeddings (\n doc_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_hash BLOB NOT NULL,\n method TEXT NOT NULL CHECK (method IN ('whole','pooled')),\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (doc_id, model)\n);\n\n\nCREATE VIRTUAL TABLE IF NOT EXISTS blocks_fts USING fts5(\n text, content='blocks', content_rowid='rowid', tokenize='porter unicode61'\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT,\n kind TEXT NOT NULL,\n name TEXT,\n value TEXT,\n span_start INTEGER,\n span_end INTEGER,\n attrs TEXT NOT NULL DEFAULT '{}'\n);\nCREATE INDEX IF NOT EXISTS idx_nodes_doc ON nodes(doc_id);\nCREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name) WHERE name IS NOT NULL;\n\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name, value, content='nodes', content_rowid='rowid', tokenize='porter unicode61'\n);\n\n\nCREATE TABLE IF NOT EXISTS file_stats (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n mtime_ns INTEGER NOT NULL,\n size INTEGER NOT NULL,\n hash BLOB NOT NULL,\n PRIMARY KEY (repo_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS properties (\n prop_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT, -- NULL = document-scoped (frontmatter, computed)\n source TEXT NOT NULL CHECK (source IN ('frontmatter','inline','computed')),\n key TEXT NOT NULL, -- dotted, flattened: \"layer\",\"meta.owner\"; computed carry $: \"$title\"\n card TEXT NOT NULL CHECK (card IN ('scalar','list')),\n ord INTEGER NOT NULL DEFAULT 0,\n val_text TEXT,\n val_num REAL,\n val_bool INTEGER,\n val_json TEXT,\n type TEXT NOT NULL CHECK (type IN ('string','number','bool','null','json')),\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_props_doc ON properties(doc_id) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_text ON properties(repo_id, key, val_text) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_num ON properties(repo_id, key, val_num) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_src_key ON properties(repo_id, source, key) WHERE deleted_commit IS NULL;\n\n\nCREATE TABLE IF NOT EXISTS adapters (\n name TEXT PRIMARY KEY, -- workspace-unique adapter name (e.g. \"fs\")\n command TEXT NOT NULL, -- argv[0] to spawn (e.g. \"omgbase-fs-adapter\")\n args TEXT NOT NULL DEFAULT '[]' -- fixed leading args (JSON string[])\n);\n\nCREATE TABLE IF NOT EXISTS sources (\n source_id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE, -- workspace-unique source name\n adapter TEXT NOT NULL REFERENCES adapters(name),\n config TEXT NOT NULL DEFAULT '{}', -- JSON object \u2192 rendered to flags (13 \u00A73.1)\n env TEXT NOT NULL DEFAULT '{}' -- JSON object \u2192 spawn env (secrets; 13 \u00A73.2)\n);\n\nCREATE TABLE IF NOT EXISTS attachments (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n source_id TEXT NOT NULL REFERENCES sources(source_id),\n PRIMARY KEY (repo_id, source_id)\n);\n\nCREATE TABLE IF NOT EXISTS sync_state (\n repo_id TEXT NOT NULL,\n source_id TEXT NOT NULL,\n path TEXT NOT NULL, -- '' reserved for the attachment-level cursor row\n revision TEXT, -- last-observed source revision for this path\n cursor TEXT, -- last poll/webhook cursor (attachment-level row)\n PRIMARY KEY (repo_id, source_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS workspace_settings (\n id INTEGER PRIMARY KEY CHECK (id = 0),\n settings TEXT NOT NULL DEFAULT '{}'\n);\nINSERT OR IGNORE INTO workspace_settings (id, settings) VALUES (0, '{}');\n\n";
10
+ export declare const DDL = "\n-- ---- durable tables (02 \u00A73) --------------------------------------------------\nCREATE TABLE IF NOT EXISTS repos (\n repo_id TEXT PRIMARY KEY,\n slug TEXT NOT NULL UNIQUE,\n settings TEXT NOT NULL DEFAULT '{}'\n);\n-- A repo owns identity + history, NOT a filesystem (ADR-014). Where its bytes\n-- come from is a sources row (adapter=fs, config.root) joined via attachments;\n-- the former repos.root_path column was removed in schema v13.\n\nCREATE TABLE IF NOT EXISTS docs (\n doc_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL,\n format TEXT NOT NULL DEFAULT 'markdown',\n current_rev TEXT,\n file_hash BLOB,\n conflicted INTEGER NOT NULL DEFAULT 0,\n leading_trivia TEXT NOT NULL DEFAULT '',\n frontmatter_trivia TEXT, -- separator between frontmatter and first body block (NULL = no frontmatter)\n deleted_commit TEXT,\n UNIQUE (repo_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS blocks (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n parent_block TEXT,\n order_key TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n depth INTEGER NOT NULL,\n ancestor_path TEXT NOT NULL,\n type TEXT NOT NULL,\n attrs TEXT NOT NULL DEFAULT '{}',\n text TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n trivia_hash BLOB, -- trailing trivia blob hash (NULL = no trivia)\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_blocks_doc ON blocks(doc_id, parent_block, order_key) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(repo_id, type) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_blocks_rawhash ON blocks(raw_hash);\nCREATE INDEX IF NOT EXISTS idx_blocks_ancestor ON blocks(doc_id, ancestor_path);\n\nCREATE TABLE IF NOT EXISTS blobs (\n hash BLOB PRIMARY KEY,\n bytes BLOB NOT NULL,\n size INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS tree_nodes (\n hash BLOB PRIMARY KEY,\n entries TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS revisions (\n rev_id TEXT PRIMARY KEY,\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n seq INTEGER NOT NULL,\n root_tree BLOB NOT NULL REFERENCES tree_nodes(hash),\n frontmatter_blob BLOB REFERENCES blobs(hash),\n rendered_hash BLOB NOT NULL,\n path TEXT NOT NULL,\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n UNIQUE (doc_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS commits (\n commit_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n seq INTEGER NOT NULL,\n ts TEXT NOT NULL,\n origin TEXT NOT NULL CHECK (origin IN ('api','observed','import','projection')),\n actor TEXT,\n reason TEXT,\n checkpoint_id TEXT,\n ops TEXT,\n UNIQUE (repo_id, seq)\n);\n\nCREATE TABLE IF NOT EXISTS dispositions (\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n block_id TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN\n ('same','edited','moved','edited_moved','inserted','deleted',\n 'split_from','merged_into','copied_from','resurrected','bulk_rewrite')),\n confidence REAL,\n reason TEXT,\n matcher_v TEXT,\n detail TEXT NOT NULL DEFAULT '{}',\n PRIMARY KEY (commit_id, block_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n edge_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n src_doc TEXT NOT NULL,\n src_block TEXT,\n src_field TEXT,\n predicate TEXT NOT NULL,\n dst_kind TEXT NOT NULL CHECK (dst_kind IN ('document','block','external','collection')),\n dst_node TEXT NOT NULL,\n anchor TEXT,\n provenance TEXT NOT NULL CHECK (provenance IN ('link','frontmatter','inline_field','projected',\n 'yaml_ref','yaml_schema','yaml_extends','json_ref','json_schema')),\n via_node TEXT,\n from_commit TEXT NOT NULL,\n to_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_doc, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_node, predicate) WHERE to_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_edges_temporal ON edges(from_commit, to_commit);\n\nCREATE TABLE IF NOT EXISTS external_nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n uri TEXT NOT NULL,\n title TEXT,\n UNIQUE (repo_id, uri)\n);\n\nCREATE TABLE IF NOT EXISTS collections (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n name TEXT NOT NULL,\n spec TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS checkpoints (\n id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n ts TEXT NOT NULL,\n files TEXT NOT NULL,\n git_head TEXT\n);\n\nCREATE TABLE IF NOT EXISTS resurrection_pool (\n block_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n raw_hash BLOB NOT NULL,\n norm_hash BLOB NOT NULL,\n type TEXT NOT NULL,\n deleted_commit TEXT NOT NULL,\n expires_ts TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS pending_materializations (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n path TEXT NOT NULL, -- where the file must end up\n kind TEXT NOT NULL CHECK (kind IN ('write','rename','remove')),\n from_path TEXT, -- rename: the old path\n doc_id TEXT NOT NULL REFERENCES docs(doc_id),\n rev_id TEXT REFERENCES revisions(rev_id), -- write/rename: the revision the bytes render (NULL for remove)\n rendered_hash BLOB NOT NULL, -- the bytes expected at path (remove: the bytes it is safe to unlink)\n commit_id TEXT NOT NULL REFERENCES commits(commit_id),\n PRIMARY KEY (repo_id, path)\n);\n\n-- ---- derived tables (02 \u00A74; rebuildable) ------------------------------------\nCREATE TABLE IF NOT EXISTS sections (\n heading_block TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n level INTEGER NOT NULL,\n first_ordinal INTEGER NOT NULL,\n last_ordinal INTEGER NOT NULL,\n PRIMARY KEY (doc_id, heading_block)\n);\n\nCREATE TABLE IF NOT EXISTS doc_edges (\n src_doc TEXT NOT NULL,\n predicate TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n dst_kind TEXT NOT NULL,\n count INTEGER NOT NULL,\n samples TEXT NOT NULL,\n PRIMARY KEY (src_doc, predicate, dst_node)\n);\n\nCREATE TABLE IF NOT EXISTS block_changes (\n block_id TEXT NOT NULL,\n commit_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n PRIMARY KEY (block_id, commit_id, kind)\n);\n\nCREATE TABLE IF NOT EXISTS inferred_edges (\n src_node TEXT NOT NULL,\n dst_node TEXT NOT NULL,\n predicate TEXT NOT NULL,\n method TEXT NOT NULL,\n score REAL NOT NULL,\n model_v TEXT NOT NULL,\n computed_at TEXT NOT NULL,\n PRIMARY KEY (src_node, dst_node, predicate, method)\n);\n\nCREATE TABLE IF NOT EXISTS embeddings (\n content_hash BLOB NOT NULL,\n ctx_hash BLOB NOT NULL,\n model TEXT NOT NULL,\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (content_hash, ctx_hash, model)\n);\n\n\nCREATE TABLE IF NOT EXISTS doc_embeddings (\n doc_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_hash BLOB NOT NULL,\n method TEXT NOT NULL CHECK (method IN ('whole','pooled')),\n dim INTEGER NOT NULL,\n vec BLOB NOT NULL,\n PRIMARY KEY (doc_id, model)\n);\n\n\nCREATE VIRTUAL TABLE IF NOT EXISTS blocks_fts USING fts5(\n text, content='blocks', content_rowid='rowid', tokenize='porter unicode61'\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n node_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT,\n kind TEXT NOT NULL,\n name TEXT,\n value TEXT,\n span_start INTEGER,\n span_end INTEGER,\n attrs TEXT NOT NULL DEFAULT '{}'\n);\nCREATE INDEX IF NOT EXISTS idx_nodes_doc ON nodes(doc_id);\nCREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name) WHERE name IS NOT NULL;\n\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name, value, content='nodes', content_rowid='rowid', tokenize='porter unicode61'\n);\n\n\nCREATE TABLE IF NOT EXISTS file_stats (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n mtime_ns INTEGER NOT NULL,\n size INTEGER NOT NULL,\n hash BLOB NOT NULL,\n PRIMARY KEY (repo_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS properties (\n prop_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT, -- NULL = document-scoped (frontmatter, computed)\n source TEXT NOT NULL CHECK (source IN ('frontmatter','inline','computed')),\n key TEXT NOT NULL, -- dotted, flattened: \"layer\",\"meta.owner\"; computed carry $: \"$title\"\n card TEXT NOT NULL CHECK (card IN ('scalar','list')),\n ord INTEGER NOT NULL DEFAULT 0,\n val_text TEXT,\n val_num REAL,\n val_bool INTEGER,\n val_json TEXT,\n type TEXT NOT NULL CHECK (type IN ('string','number','bool','null','json')),\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_props_doc ON properties(doc_id) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_text ON properties(repo_id, key, val_text) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_num ON properties(repo_id, key, val_num) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_src_key ON properties(repo_id, source, key) WHERE deleted_commit IS NULL;\n\n\nCREATE TABLE IF NOT EXISTS adapters (\n name TEXT PRIMARY KEY, -- workspace-unique adapter name (e.g. \"fs\")\n command TEXT NOT NULL, -- argv[0] to spawn (e.g. \"omgbase-fs-adapter\")\n args TEXT NOT NULL DEFAULT '[]' -- fixed leading args (JSON string[])\n);\n\nCREATE TABLE IF NOT EXISTS sources (\n source_id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE, -- workspace-unique source name\n adapter TEXT NOT NULL REFERENCES adapters(name),\n config TEXT NOT NULL DEFAULT '{}', -- JSON object \u2192 rendered to flags (13 \u00A73.1)\n env TEXT NOT NULL DEFAULT '{}' -- JSON object \u2192 spawn env (secrets; 13 \u00A73.2)\n);\n\nCREATE TABLE IF NOT EXISTS attachments (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n source_id TEXT NOT NULL REFERENCES sources(source_id),\n PRIMARY KEY (repo_id, source_id)\n);\n\nCREATE TABLE IF NOT EXISTS sync_state (\n repo_id TEXT NOT NULL,\n source_id TEXT NOT NULL,\n path TEXT NOT NULL, -- '' reserved for the attachment-level cursor row\n revision TEXT, -- last-observed source revision for this path\n cursor TEXT, -- last poll/webhook cursor (attachment-level row)\n PRIMARY KEY (repo_id, source_id, path)\n);\n\n\nCREATE TABLE IF NOT EXISTS workspace_settings (\n id INTEGER PRIMARY KEY CHECK (id = 0),\n settings TEXT NOT NULL DEFAULT '{}'\n);\nINSERT OR IGNORE INTO workspace_settings (id, settings) VALUES (0, '{}');\n\n";
@@ -1,7 +1,7 @@
1
1
  // Full DDL — data-model §3 (durable) + §4 (derived). SQLite dialect.
2
2
  // Kept as one string so migrations and rebuild-index can apply it verbatim.
3
3
  // No dialect-specific SQL leaks above the store module (02 §8).
4
- export const SCHEMA_VERSION = 13;
4
+ export const SCHEMA_VERSION = 14;
5
5
  // file_stats (02 §4; derived, rebuildable by a full re-stat) backs the CLI
6
6
  // freshness sweep (11 §3.3): (mtime_ns, size) cheap-change detection so a
7
7
  // one-shot command re-ingests only files that changed on disk since last ingest.
@@ -136,10 +136,34 @@ CREATE TABLE IF NOT EXISTS doc_embeddings (
136
136
  PRIMARY KEY (doc_id, model)
137
137
  );
138
138
  `;
139
+ // pending_materializations (spec/store §5.7, store 14.0). The recorded intent
140
+ // of the api path's file half: the commit transaction inserts one row per file
141
+ // the request must leave on disk — `write` the revision's bytes at `path`,
142
+ // `rename` the file from `from_path` to `path`, `remove` the file at `path` —
143
+ // and the materializer deletes each row once the file is there (after COMMIT,
144
+ // per file, autocommit). A row that survives a crash is replayed by the next
145
+ // materializer (B', the freshness sweep's step 0, recovery) from
146
+ // `reconstruct at rev_id`; `rendered_hash` names the bytes expected at `path`
147
+ // (for `remove`: the bytes it is safe to unlink). No bytes column: the
148
+ // revision already holds them. One row per path: a later request for the same
149
+ // path supersedes the earlier intent (`INSERT OR REPLACE`).
150
+ export const PENDING_MATERIALIZATIONS_DDL = /* sql */ `
151
+ CREATE TABLE IF NOT EXISTS pending_materializations (
152
+ repo_id TEXT NOT NULL REFERENCES repos(repo_id),
153
+ path TEXT NOT NULL, -- where the file must end up
154
+ kind TEXT NOT NULL CHECK (kind IN ('write','rename','remove')),
155
+ from_path TEXT, -- rename: the old path
156
+ doc_id TEXT NOT NULL REFERENCES docs(doc_id),
157
+ rev_id TEXT REFERENCES revisions(rev_id), -- write/rename: the revision the bytes render (NULL for remove)
158
+ rendered_hash BLOB NOT NULL, -- the bytes expected at path (remove: the bytes it is safe to unlink)
159
+ commit_id TEXT NOT NULL REFERENCES commits(commit_id),
160
+ PRIMARY KEY (repo_id, path)
161
+ );
162
+ `;
139
163
  // Additive migrations keyed by the version they upgrade TO. Each runs inside a
140
164
  // transaction. Only forward, idempotent DDL (CREATE ... IF NOT EXISTS) — no
141
165
  // destructive changes. store.ts applies these in order for an older db.
142
- // Migrations 3, 4, 6, 7, 9, 11 are programmatic — see store.ts.
166
+ // Migrations 3, 4, 6, 7, 9, 11, 13 are programmatic — see store.ts.
143
167
  export const MIGRATIONS = {
144
168
  2: FILE_STATS_DDL,
145
169
  3: "", // handled programmatically in store.ts
@@ -153,6 +177,7 @@ export const MIGRATIONS = {
153
177
  11: "", // ALTER TABLE documents RENAME TO docs, handled programmatically in store.ts
154
178
  12: DOC_EMBEDDINGS_DDL, // doc-grain semantic vectors
155
179
  13: "", // drop repos.root_path (migrate → fs sources), handled programmatically in store.ts
180
+ 14: PENDING_MATERIALIZATIONS_DDL, // the api path's recorded file intent (spec/store §5.7)
156
181
  };
157
182
  export const DDL = /* sql */ `
158
183
  -- ---- durable tables (02 §3) --------------------------------------------------
@@ -304,7 +329,7 @@ CREATE TABLE IF NOT EXISTS resurrection_pool (
304
329
  deleted_commit TEXT NOT NULL,
305
330
  expires_ts TEXT NOT NULL
306
331
  );
307
-
332
+ ${PENDING_MATERIALIZATIONS_DDL}
308
333
  -- ---- derived tables (02 §4; rebuildable) ------------------------------------
309
334
  CREATE TABLE IF NOT EXISTS sections (
310
335
  heading_block TEXT NOT NULL,
@@ -23,6 +23,15 @@ export declare class Store {
23
23
  * commit). better-sqlite3 transactions are synchronous and serialized.
24
24
  */
25
25
  write<T>(fn: (db: Database.Database) => T): T;
26
+ /**
27
+ * Run fn inside a `BEGIN IMMEDIATE` transaction: the write lock is taken at
28
+ * BEGIN, so every read fn makes is already protected from another writer and
29
+ * a concurrent `BEGIN IMMEDIATE` waits (SQLITE_BUSY after the busy timeout).
30
+ * The commit phase of a request uses it (spec/mutate §4 step 5(c), 2.2) so
31
+ * the preconditions it re-validates cannot move before it writes. Nested
32
+ * inside an open transaction it is a savepoint, like `write`.
33
+ */
34
+ writeImmediate<T>(fn: (db: Database.Database) => T): T;
26
35
  pragma(name: string): unknown;
27
36
  close(): void;
28
37
  }
@@ -191,6 +191,17 @@ export class Store {
191
191
  write(fn) {
192
192
  return this.db.transaction(fn)(this.db);
193
193
  }
194
+ /**
195
+ * Run fn inside a `BEGIN IMMEDIATE` transaction: the write lock is taken at
196
+ * BEGIN, so every read fn makes is already protected from another writer and
197
+ * a concurrent `BEGIN IMMEDIATE` waits (SQLITE_BUSY after the busy timeout).
198
+ * The commit phase of a request uses it (spec/mutate §4 step 5(c), 2.2) so
199
+ * the preconditions it re-validates cannot move before it writes. Nested
200
+ * inside an open transaction it is a savepoint, like `write`.
201
+ */
202
+ writeImmediate(fn) {
203
+ return this.db.transaction(fn).immediate(this.db);
204
+ }
194
205
  pragma(name) {
195
206
  return this.db.pragma(name, { simple: true });
196
207
  }
@@ -278,12 +278,15 @@ export function changesSince(store, repoId, opts = {}) {
278
278
  const digests = page.map((c) => {
279
279
  const revs = store.db.prepare("SELECT r.doc_id AS doc, r.path AS path, r.rendered_hash AS rendered_hash FROM revisions r WHERE r.commit_id = ?").all(c.commit_id).map((r) => ({ id: r.doc, path: referencePath(r.path), hash: r.rendered_hash.toString("hex") }));
280
280
  const dispCounts = store.db.prepare("SELECT kind, count(*) n FROM dispositions WHERE commit_id = ? GROUP BY kind").all(c.commit_id);
281
- // A deletion or a move writes no revision: the tombstone names the commit
282
- // (`docs.deleted_commit`), a move only its reason (`move <from> -> <to>`).
281
+ // A deletion writes no revision: the tombstone names the commit
282
+ // (`docs.deleted_commit`). A move names itself by its reason (`move <from>
283
+ // -> <to>`) and, since spec/mutate 2.1, carries the inbound retarget's
284
+ // revisions under the same commit — so the move is read off the reason
285
+ // whether or not the commit has revisions.
283
286
  const deleted = revs.length === 0
284
287
  ? store.db.prepare("SELECT path FROM docs WHERE deleted_commit = ? ORDER BY path").all(c.commit_id).map((r) => referencePath(r.path))
285
288
  : [];
286
- const moved = revs.length === 0 ? parseMove(c.reason) : null;
289
+ const moved = parseMove(c.reason);
287
290
  const summary = renderSummary(c.origin, c.actor, revs, dispCounts, { deleted, moved: moved ? { from: referencePath(moved.from), to: referencePath(moved.to) } : null });
288
291
  return { commit: c.commit_id, seq: c.seq, ts: c.ts, origin: c.origin, actor: c.actor, summary, revisions: revs };
289
292
  });
@@ -387,18 +390,18 @@ function parseMove(reason) {
387
390
  }
388
391
  /**
389
392
  * One line per commit: `<origin>[(<actor>)]: <paths> — <n> <kind>, …`. A
390
- * commit that touched no revision — a document deletion (observed or api) or a
391
- * move — says so instead: `deleted <path>` / `moved <from> → <to>`
392
- * (spec/cli §6 `log`; the summary is unpinned by spec/surface).
393
+ * commit that touched no revision — a document deletion (observed or api) —
394
+ * says so instead: `deleted <path>`. A move says `moved <from> → <to>`, and
395
+ * when it also rewrote the inbound sources (spec/mutate §6, 2.1) their paths
396
+ * follow: `moved a → b; /index.md — 31 edited` (spec/cli §6 `log`; the summary
397
+ * is unpinned by spec/surface).
393
398
  */
394
399
  function renderSummary(origin, actor, revs, disp, extra = { deleted: [], moved: null }) {
395
400
  let subject = revs.map((r) => r.path).join(", ");
396
- if (revs.length === 0) {
397
- if (extra.deleted.length > 0)
398
- subject = `deleted ${extra.deleted.join(", ")}`;
399
- else if (extra.moved)
400
- subject = `moved ${extra.moved.from} → ${extra.moved.to}`;
401
- }
401
+ if (extra.moved)
402
+ subject = `moved ${extra.moved.from} → ${extra.moved.to}${revs.length > 0 ? `; ${subject}` : ""}`;
403
+ else if (revs.length === 0 && extra.deleted.length > 0)
404
+ subject = `deleted ${extra.deleted.join(", ")}`;
402
405
  const parts = disp.filter((d) => d.kind !== "same").map((d) => `${d.n} ${d.kind}`);
403
406
  const detail = parts.length > 0 ? ` — ${parts.join(", ")}` : "";
404
407
  if (origin === "api" || origin === "import")
@@ -16,7 +16,7 @@ export { Workspace, RepoSelectionError, selectRepo, type RepoRow } from "./sync/
16
16
  export { resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, deepMerge, type Settings, } from "./sync/settings.js";
17
17
  export { freshnessSweep, rebuildFileStats, recordFileStat, recordStat, detectDiskDrift, sweepPlan, sweepCandidates, loadStatCache, snapshotOf, type SweepResult, type DiskDrift, type SweepPlan, type StatCacheRow, type SnapshotEntry, } from "./sync/freshness.js";
18
18
  export { recoverRepo, type RecoveryResult } from "./sync/recovery.js";
19
- export { withWriterLock, writerLockFree, WriterLockTimeout } from "./sync/writer-lock.js";
19
+ export { withWriterLock, withWriterLockAsync, writerLockFree, writerLockHeld, setWriterLockDefaults, writerLockDefaults, WriterLockTimeout, type WriterLockOptions } from "./sync/writer-lock.js";
20
20
  export { WatchLease, watchLeaseLive } from "./sync/watch-lease.js";
21
21
  export { ingestDirectory, type IngestDirResult } from "./sync/attach.js";
22
22
  export { ensureRepo } from "./core/attach.js";
@@ -28,7 +28,7 @@ export { createExternalSource, type ExternalSourceSpec } from "./sync/external-s
28
28
  export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, type AdapterRow, type SourceRow, } from "./sync/sources.js";
29
29
  export { reconcileChanges, attachSource } from "./sync/driver.js";
30
30
  export { observeFile, observeMany, observeOne, observeDelete, observeBatch, type ObserveResult, type ObserveOneResult, type ObserveDeleteResult, type BatchItem, type BatchOutcome } from "./sync/observe.js";
31
- export { buildServer, type ServerContext } from "./mcp/server.js";
31
+ export { buildServer, WRITE_TOOLS, type ServerContext } from "./mcp/server.js";
32
32
  export { serveStdio, type ServeStdioHandle } from "./mcp/stdio.js";
33
33
  export { processCheckpoint, type CheckpointResult } from "./sync/checkpoint.js";
34
34
  export { reposStatus, syncStatus, reposList, type RepoStatus, type SyncStatus, type DiskStatus, type ReposList } from "./sync/admin.js";
@@ -60,12 +60,16 @@ export { sha256 } from "./core/hash.js";
60
60
  export { ingestFile, type IngestResult } from "./core/ingest.js";
61
61
  export { apply, type ApplyRequest, type ApplyResult, type Op } from "./mutate/apply.js";
62
62
  export { type DocStore, FsDocStore, NullDocStore, resolveDocStore } from "./mutate/doc-store.js";
63
+ export { materializePending, materializeRow, listPending, readPending, type PendingRow, type PendingKind } from "./mutate/materialize.js";
63
64
  export { type To, type At, type Expect } from "./mutate/ops.js";
64
65
  export { tasksComplete, sectionsAppend, sectionsRename, sectionsMove, listsInsertItem, linksRetarget, nodeSet, editablePropsFor, type RetargetHit, } from "./mutate/macros.js";
65
- export { docsCreate, docsMove, docsDelete, docsSetMeta, type DocOpContext, type DocOpResult, type DocMoveOptions, type DocMoveResult, } from "./mutate/docs.js";
66
+ export { canonicalDocPath, composeFile, splitFrontmatter, planInboundRetarget } from "./mutate/docs.js";
67
+ export { createEffects, moveDocEffects, deleteDocEffects, setMetaEffects, updateContentEffects, insertEffects, updateBlockEffects, moveBlocksEffects, removeBlocksEffects, splitEffects, mergeEffects, checkEffects, nodeSetEffects, appendEffects, docsAppendEffects, retargetEffects, guardOf, requireAbsentPath, applySugar, createdDoc, docOutcome, moveLinks, type Sugar, type SugarDoc, type SugarRequest, type DocOpOutcome, type MoveLinks, } from "./mutate/sugar.js";
68
+ export { planEffects, executePlan, executeByDigest, applyEffects, applyEffectsDetailed, substituteSymbols, admitPlan, canonicalJson, planDigest, PLAN_VERSION, PLANNER_V, type Plan, type PlanCore, type PlanOpset, type PlanPreconditions, type PlanPreview, type Receipt, type PlanEffectsRequest, type ExecuteRequest, } from "./mutate/plan.js";
69
+ export { validateEffects, EFFECT_OPS, type Effect, type EffectTarget, type EffectExpect, type ResolvedEffect, type CreateEntry, type RetargetPreview } from "./mutate/effects.js";
66
70
  export { inboundLinksTo, retargetLinksInRaw, type InboundLink } from "./graph/inbound-links.js";
67
- export { planUpdate, applyOpset, docsUpdate, type PlanUpdateOptions, type ApplyOpsetRequest, type DocsUpdateContext, type DocsUpdateResult, } from "./mutate/plan-update.js";
68
- export { opsetKernelOps, renderOpsetPlan, serializeOpset, parseOpset, type Opset, type PlanOp, type OpsetSummary, type OpsetPrecondition, type PlanDisposition, } from "./mutate/opset.js";
71
+ export { planUpdate, type PlanUpdateOptions } from "./mutate/plan-update.js";
72
+ export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, type IdentityPreview, serializeOpset, parseOpset, type Opset, type PlanOp, type OpsetSummary, type OpsetPrecondition, type PlanDisposition, } from "./mutate/opset.js";
69
73
  export { rebuildIndex, type RebuildTarget } from "./core/store/rebuild.js";
70
74
  export { runGc, type GcResult } from "./core/store/gc.js";
71
75
  export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
package/dist/src/index.js CHANGED
@@ -17,7 +17,7 @@ export { Workspace, RepoSelectionError, selectRepo } from "./sync/workspace.js";
17
17
  export { resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, deepMerge, } from "./sync/settings.js";
18
18
  export { freshnessSweep, rebuildFileStats, recordFileStat, recordStat, detectDiskDrift, sweepPlan, sweepCandidates, loadStatCache, snapshotOf, } from "./sync/freshness.js";
19
19
  export { recoverRepo } from "./sync/recovery.js";
20
- export { withWriterLock, writerLockFree, WriterLockTimeout } from "./sync/writer-lock.js";
20
+ export { withWriterLock, withWriterLockAsync, writerLockFree, writerLockHeld, setWriterLockDefaults, writerLockDefaults, WriterLockTimeout } from "./sync/writer-lock.js";
21
21
  export { WatchLease, watchLeaseLive } from "./sync/watch-lease.js";
22
22
  export { ingestDirectory } from "./sync/attach.js";
23
23
  export { ensureRepo } from "./core/attach.js";
@@ -28,7 +28,7 @@ export { createExternalSource } from "./sync/external-source.js";
28
28
  export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, } from "./sync/sources.js";
29
29
  export { reconcileChanges, attachSource } from "./sync/driver.js";
30
30
  export { observeFile, observeMany, observeOne, observeDelete, observeBatch } from "./sync/observe.js";
31
- export { buildServer } from "./mcp/server.js";
31
+ export { buildServer, WRITE_TOOLS } from "./mcp/server.js";
32
32
  export { serveStdio } from "./mcp/stdio.js";
33
33
  export { processCheckpoint } from "./sync/checkpoint.js";
34
34
  export { reposStatus, syncStatus, reposList } from "./sync/admin.js";
@@ -67,15 +67,22 @@ export { ingestFile } from "./core/ingest.js";
67
67
  // Mutation surface consumed by CLI write commands (11 §5.6).
68
68
  export { apply } from "./mutate/apply.js";
69
69
  export { FsDocStore, NullDocStore, resolveDocStore } from "./mutate/doc-store.js";
70
+ export { materializePending, materializeRow, listPending, readPending } from "./mutate/materialize.js";
70
71
  export {} from "./mutate/ops.js";
71
72
  export { tasksComplete, sectionsAppend, sectionsRename, sectionsMove, listsInsertItem, linksRetarget, nodeSet, editablePropsFor, } from "./mutate/macros.js";
72
- // Admin/maintenance (11 §5.9). Graph traversal is OQX `follow` now (see oqx/).
73
- export { docsCreate, docsMove, docsDelete, docsSetMeta, } from "./mutate/docs.js";
73
+ // The document operations' shared pieces (spec/mutate §6); the operations themselves are the effect builders below.
74
+ export { canonicalDocPath, composeFile, splitFrontmatter, planInboundRetarget } from "./mutate/docs.js";
75
+ // The effect builders (spec/mutate §6/§10, 3.0; spec/surface §4 "Sugar over effects", 4.0): one per operation,
76
+ // shared by the MCP server, the `omg` verbs and the fixture runner, plus `applySugar` and the Receipt adapters.
77
+ export { createEffects, moveDocEffects, deleteDocEffects, setMetaEffects, updateContentEffects, insertEffects, updateBlockEffects, moveBlocksEffects, removeBlocksEffects, splitEffects, mergeEffects, checkEffects, nodeSetEffects, appendEffects, docsAppendEffects, retargetEffects, guardOf, requireAbsentPath, applySugar, createdDoc, docOutcome, moveLinks, } from "./mutate/sugar.js";
78
+ // The Effect layer (ADR-021, spec/mutate §11): plan / execute / apply { effects } and the plan's shapes.
79
+ export { planEffects, executePlan, executeByDigest, applyEffects, applyEffectsDetailed, substituteSymbols, admitPlan, canonicalJson, planDigest, PLAN_VERSION, PLANNER_V, } from "./mutate/plan.js";
80
+ export { validateEffects, EFFECT_OPS } from "./mutate/effects.js";
74
81
  // Inbound-link discovery + destination-aware rewrite (docs_move's dangling report / retarget_inbound).
75
82
  export { inboundLinksTo, retargetLinksInRaw } from "./graph/inbound-links.js";
76
- // Whole-document update: reconcile → opset → inspect/apply.
77
- export { planUpdate, applyOpset, docsUpdate, } from "./mutate/plan-update.js";
78
- export { opsetKernelOps, renderOpsetPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
83
+ // Whole-document update: reconcile → opset (the `set content` effect's internal planner since 3.0; `preview.identity` surfaces it).
84
+ export { planUpdate } from "./mutate/plan-update.js";
85
+ export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
79
86
  export { rebuildIndex } from "./core/store/rebuild.js";
80
87
  export { runGc } from "./core/store/gc.js";
81
88
  export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
@@ -15,11 +15,18 @@ export declare function validateArguments(tool: string, shape: ArgShape, raw: un
15
15
  /**
16
16
  * What to say about an argument name the tool does not take. The names 3.0
17
17
  * retired are called out with their replacement (`doc` → `id`/`path`,
18
- * `block`/`node` → `id`, `heading` → `section`,
19
- * `markdown` → `text`) when the tool has the replacement; otherwise the
18
+ * `block`/`node` → `id`, `heading` → `section`, `markdown` → `text`) when the
19
+ * tool has the replacement, and so are the inputs 4.0 retired (spec/surface
20
+ * §4; ADR-021's retirement release): `ops` on a tool that takes `effects`
21
+ * (the kernel ops are a library/fixture contract), `dry_run` on any tool (the
22
+ * preview is `plan`), `attrs` on a tool that takes `checked`. Otherwise the
20
23
  * nearest valid name (Levenshtein ≤ 2, or one a prefix of the other).
21
24
  */
22
25
  export declare function unknownArgumentMessage(key: string, shape: ArgShape): string;
26
+ /** The retired-input messages of surface 4.0 (spec/surface §4), without the `(tool takes …)` tail. */
27
+ export declare const RETIRED_OPS = "`ops` is no longer an argument \u2014 `apply` takes `effects` (the kernel ops are a library/fixture contract since spec/mutate 3.0)";
28
+ export declare const RETIRED_DRY_RUN = "`dry_run` is no longer an argument \u2014 preview with `plan { effects: [...] }`";
29
+ export declare const RETIRED_ATTRS = "`attrs` is no longer an argument \u2014 pass `checked`";
23
30
  /** The nearest candidate: Levenshtein distance ≤ 2, or a prefix relation either way; ties → the bytewise-first. */
24
31
  export declare function nearest(name: string, candidates: readonly string[]): string | null;
25
32
  export declare function levenshtein(a: string, b: string): number;
@@ -63,8 +63,11 @@ function issueMessage(key, schema, issues) {
63
63
  /**
64
64
  * What to say about an argument name the tool does not take. The names 3.0
65
65
  * retired are called out with their replacement (`doc` → `id`/`path`,
66
- * `block`/`node` → `id`, `heading` → `section`,
67
- * `markdown` → `text`) when the tool has the replacement; otherwise the
66
+ * `block`/`node` → `id`, `heading` → `section`, `markdown` → `text`) when the
67
+ * tool has the replacement, and so are the inputs 4.0 retired (spec/surface
68
+ * §4; ADR-021's retirement release): `ops` on a tool that takes `effects`
69
+ * (the kernel ops are a library/fixture contract), `dry_run` on any tool (the
70
+ * preview is `plan`), `attrs` on a tool that takes `checked`. Otherwise the
68
71
  * nearest valid name (Levenshtein ≤ 2, or one a prefix of the other).
69
72
  */
70
73
  export function unknownArgumentMessage(key, shape) {
@@ -78,9 +81,19 @@ export function unknownArgumentMessage(key, shape) {
78
81
  return retired("`section` (the heading's text), or blocks_append with the heading's `id`");
79
82
  if (key === "markdown" && has("text"))
80
83
  return retired("`text`");
84
+ if (key === "ops" && has("effects"))
85
+ return RETIRED_OPS;
86
+ if (key === "dry_run")
87
+ return RETIRED_DRY_RUN;
88
+ if (key === "attrs" && has("checked"))
89
+ return RETIRED_ATTRS;
81
90
  const near = nearest(key, Object.keys(shape));
82
91
  return near === null ? `unknown argument \`${key}\`` : `unknown argument \`${key}\`; did you mean \`${near}\`?`;
83
92
  }
93
+ /** The retired-input messages of surface 4.0 (spec/surface §4), without the `(tool takes …)` tail. */
94
+ export const RETIRED_OPS = "`ops` is no longer an argument — `apply` takes `effects` (the kernel ops are a library/fixture contract since spec/mutate 3.0)";
95
+ export const RETIRED_DRY_RUN = "`dry_run` is no longer an argument — preview with `plan { effects: [...] }`";
96
+ export const RETIRED_ATTRS = "`attrs` is no longer an argument — pass `checked`";
84
97
  /** The nearest candidate: Levenshtein distance ≤ 2, or a prefix relation either way; ties → the bytewise-first. */
85
98
  export function nearest(name, candidates) {
86
99
  let best = null;
@@ -1,4 +1,4 @@
1
- export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container";
1
+ export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container" | "stale_plan" | "plan_not_convergent" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch";
2
2
  export interface EngineErrorBody {
3
3
  error: ErrorCode;
4
4
  message: string;
@@ -2,6 +2,7 @@ import { FilterInvalid } from "../search/cel/parser.js";
2
2
  import { CursorInvalid } from "../core/cursor.js";
3
3
  import { MutationError } from "../mutate/tree.js";
4
4
  import { RevisionNotFound } from "../graph/history.js";
5
+ import { WriterLockTimeout } from "../sync/writer-lock.js";
5
6
  export class EngineError extends Error {
6
7
  code;
7
8
  data;
@@ -27,6 +28,8 @@ export function errorBody(err) {
27
28
  return { error: err.code, message: err.message, data: err.data, retriable: Boolean(err.data.retriable) };
28
29
  if (err instanceof RevisionNotFound)
29
30
  return { error: "target_missing", message: err.message, data: { doc: err.docId, rev: err.rev }, retriable: false };
31
+ if (err instanceof WriterLockTimeout)
32
+ return { error: "repo_not_found", message: err.message, retriable: false };
30
33
  return { error: "repo_not_found", message: String(err), retriable: false };
31
34
  }
32
35
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- export declare const QUERY_SYNTAX = "# query \u2014 OQX syntax reference\n\nThe `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`). There\nis no {from, filter, order} envelope: every concern is a clause of the string.\n\n [select <items>] from <target> [where <pred>] [follow <dest> [{ \u2026 }], \u2026]\n [order by <expr> [asc|desc], \u2026] [limit N] [offset N]\n <target> count|exists|none|first|single { <block> } (scalar/one-row form: a bare target at the root scope)\n\nCLAUSE ORDER IS FIXED: select, from, where, follow, order by, limit, offset \u2014\neach at most once; an out-of-order clause is a parse error naming the order.\nOnly `select` may drop its keyword, and only as the first clause\n(`path, era from docs where \u2026`). A top-level predicate needs `where`, because\n`where` follows `from` (`era > 1600 from docs` is the clause-order error); a\nblock may LEAD with a predicate (see Sugar), but a predicate after a projection\nstill needs the keyword (`{ name where kind == \"md:task\" }`, not\n`{ name, kind == \"md:task\" }`); `from docs count` is an error (write\n`docs count { \u2026 }`). `where` and later `select` items may use the same body's\n`select` aliases (`select path, old: era < 1000 from docs where old`; an alias\nshadows a same-named field there). `and` / `or` / `not` are the connectives;\nthe symbols `&&` / `||` / `!` are accepted synonyms (the words are canonical and\nwhat `print` writes).\n\n## Sugar (OQX 0.17)\n\nEvery form here is shorthand for an explicit directive (the AST is the explicit form):\n nodes { kind == \"md:task\" } \u2261 nodes collect { where kind == \"md:task\" } \u2014 a receiver block with\n no consumer is collect; a block whose LEADING expression is a predicate\n (comparison, call, !/is/not, in, literal, (\u2026), a consumer test \u2014 anything\n but a bare name, a dotted path or a ^lift) is where-first. Bare names still\n project: nodes { name } \u2261 nodes collect { name }; filter a bare field with\n nodes { is checked }. Wherever a value stands (0.20), call arguments included:\n size(children { is checked }); x distinct { \u2026 } \u2261 x collect distinct { \u2026 }. After a\n follow destination too: follow children { depth 2 } is that destination's block.\n (a, b) a tuple (0.20): two or more expressions, concatenated one level flat \u2014 a value\n (size((tags, aliases)), \"x\" in (tags, aliases)) and a receiver ((a, b) { \u2026 },\n follow (before, after) { depth 2 }); (a) alone is grouping\n refs(x)[0] \u2261 refs(x) first { offset 0 } positional (integer literal or ${binding});\n out of range \u21D2 absent; refs(x)[0].path navigates the row\n ^docs[path == ^company] \u2261 ^docs first { where path == ^company } first match or absent\n ^docs[path == ^company]! \u2261 \u2026 single { \u2026 } required: exactly one, else filter_invalid\n title! required: title, or an error naming the expression (and the row's $key \u2014 its id)\n \u2014 never a filter, never a coercion (0!, \"\"! are values); tightest: a!.b vs a.b!\n is x / not x truthiness of x, and its negation (prefix; `not` \u2261 `!`)\n x is y / x is not y identity ($key: a store row's id, else structural) \u2014 compares rows, which == does not;\n x is null = absent; for scalars is \u2261 ==; comparison precedence, no chaining\n a and b / a or b the connectives (\u2261 && / ||: same precedence, short-circuit, value: data.title or path coalesces)\n boss: ^docs[path == ^manager], bossName: boss.title a select item may use the items to its LEFT\n Reserved words (never a bare field name): from where select is not and or, true false null.\n Contextual words (limit order offset follow collect count first distinct desc by in frontier depth) parse as\n fields in expression position (select o: order from docs).\n\nResults are LEAN hits \u2014 { id, path } + whatever `select` projects (or `values`,\n`count`, `exists`, `none`). The envelope keys ARE the attributes: copy a hit's\n`id` straight into `where id == \"\u2026\"` (no translation); `select id, path` is simply\nthe envelope (no duplicate columns); any OTHER projection whose output key would\nbe `id` or `path` must be aliased \u2014 `select data.path` is refused (\"projection\n`path` collides with the hit envelope \u2014 alias it (`p: data.path`)\"); a row\nsurfacing as a value (an empty `collect { }`) is the same `{ id, path }`. Every\nother tool spells a row the same way (`docs_read` \u2192 `{ id, path, rev, properties,\nraw }`, `blocks_read` \u2192 `{ id, type, text, raw, hash, \u2026 }`, `docs_list` \u2192 `{ id,\npath, updated_at, blocks }`, a revision \u2192 `{ id, path, hash }`): hydrate by id via\nblocks_read/docs_read.\n\n## Paths\n\nEvery path the surface returns is `/`-rooted \u2014 the form a reference is\nwritten in (`[x](/projects/oqx.md)`, `before: [/timeline/kickoff.md]`): a\nhit's `path`, an edge's `dst_path`, every tool's paths. Every path it\naccepts tolerates both forms (`/a.md` or `a.md`): tool arguments, `refs(x)`,\n`within(...)`, and a string LITERAL compared with `path`/`dst_path` (bare, or\nthrough a row handle: `doc.path`) by `==`/`!=` or passed to their\n`.startsWith(...)` (it is rooted first, so `path == \"a.md\"` and `path == \"/a.md\"`\nboth match). A property holding a reference compares directly \u2014 and is never\nrooted: `where ^path in list(after)`, `where customer == ^path`,\n`where data.path == \"/x.md\"` (the author's `path:` key, as written) \u2014 no\n`\"/\" + ...` glue.\n\n## Targets & field namespaces\n\nTHE NAME RULE: `$` belongs to OQX \u2014 $it, $key, $depth, $stop, $leaf, $frontier,\n$ordinal are values the LANGUAGE injects into a scope; omgbase adds NO $ names\n(a `$id` in a query is an unknown name, absent). Every engine attribute is a BARE\nsnake_case name that WINS on collision \u2014 the convention the relations (children,\nblocks, out, in, doc, frontmatter, \u2026) always followed: a bare name that is an\nengine attribute reads the engine's value, never yours. Your data is bare too;\nwhere a key of yours collides with an engine name, reach it through a source\nhandle: `data.<k>` (the merged authored view \u2014 frontmatter + inline, exactly the\nread a bare non-colliding key performs), `frontmatter.<k>`, `inline.<k>`. So\n`path` is the engine's /-rooted location and `data.path` your `path:` key;\n`tags` is your tags \u222A the body #hashtags and `data.tags` the authored list alone;\n`title` the computed title (the first H1) and `data.title` your `title:`;\n`entries(data)` lists every authored top-level key. Same value, same name\neverywhere: a hit's `id` is docs_read's `id` and the `id` argument of blocks_read.\n\n Reserved bare names per kind (engine attributes; handles; self; relations):\n docs id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes out in out_links in_links\n blocks id path doc_id raw hash updated_at type text attrs depth ordinal | block doc | children nodes out_links section\n nodes id path doc_id block_id kind name value attrs | section doc block | blocks subsections children\n links id path doc_id block_id via_id dst_id dst_path uri field dst_kind predicate provenance anchor | doc\n A key of yours with any of these names is `data.<k>` (or frontmatter.<k> /\n inline.<k> by source; attrs.<k> on a block or node).\n\ndocs:\n - bare identifier = a document PROPERTY (frontmatter + inline `key:: value`,\n unioned; nested via dots: meta.owner). YAML/JSON docs\n expose the parsed object's keys the same way.\n - source handles = data.<k> (merged authored view) / frontmatter.<k> /\n inline.<k>; the whole bag as a collection via entries(data)\n / entries(frontmatter) / entries(inline) (see entries()).\n - attributes = id, path (/-rooted, see Paths), updated_at (ISO-8601,\n sorts chronologically), raw (the whole file, frontmatter\n included), hash (its sha256, 64 hex), title (the computed\n title: the first H1; data.title is the authored key), tags\n (authored tags \u222A body #hashtags, deduped, authored first;\n data.tags is the authored list), format (the doc's format \u2014\n \"markdown\"/\"yaml\"/\"json\"; present on EVERY doc, so\n has(format) is always true and `format == \"yaml\"` is the\n filter).\n - relations = nodes, blocks, doc.out_links / doc.in_links (a doc's\n links as rows; the documents at their other end are\n out_links collect distinct { from dst } and\n in_links collect distinct { from doc } \u2014 there is no\n out / in relation).\n\nblocks:\n - bare fields = doc_id (the owning doc), type, text (normalized), ordinal\n (0-based among siblings), depth (0 at the top level), and\n every attrs key FLATTENED onto the row (bare `checked`\n reads attrs.checked; `attrs.<k>` still works; the\n structural fields win on collision; absent \u21D2 false).\n - attributes = id, path (the owning doc's path), raw (the block's raw\n source \u2014 a container's raw holds its children), hash\n (its sha256 \u2014 the value update/split expect as\n expect.content_hash), updated_at.\n - reach-through = doc.<key> / doc.path / doc.id / doc.format = the\n CONTAINING doc (doc.id and doc.path cost nothing \u2014 they\n read the row's own doc_id / joined path, no lookup).\n - relations = block.children, block.nodes, block.out_links, section.\n NOTE: inside a `follow`, $depth/$ordinal are ALWAYS the walk's metadata; a\n block's own nesting is `depth` / `ordinal` (no $) everywhere.\n\nnodes:\n - bare fields = doc_id, block_id (null for a block-less node), kind\n (md:task / md:link / md:section / yaml:\u2026), name, value,\n + attrs keys flattened (checked, level).\n - attributes = id, path.\n - reach-through = doc.<key> / doc.id / doc.path, block.type / block.text\n / block.id / block.path.\n - relations = section.blocks, section.children, section.subsections.\n\nlinks: (the authored link graph as rows \u2014 one per open link: the relation a link\n establishes; the md:link NODE is its occurrence in the text and via_id\n points at it \u2014 a frontmatter link has a row and no node)\n - bare fields = doc_id (the SOURCE doc), block_id (the source block; null\n for a frontmatter edge), via_id, dst_id (the destination\n node id: a doc id, an external node id or phantom:<path>),\n dst_path (target doc path, /-rooted; NULL when\n dangling/external), uri (external URL; NULL otherwise),\n predicate (\"references\"/\"embeds\"/freeform), provenance\n (\"link\"/\"frontmatter\"/\"inline_field\"/\u2026), dst_kind\n (\"document\"/\"external\"/\"collection\"), anchor, field (the\n frontmatter key / inline field it was authored in).\n - attributes = id, path (the SOURCE doc's path); doc.<key> reaches the\n source document. There is no from_commit: a query sees\n open edges only (history is blocks_history/changes_since).\n - text()/semantic() are N/A on links.\n\n## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL\n\n name the CURRENT row only \u2014 never falls through to an enclosing row\n or the repository; an absent field is simply absent.\n ^name one scope OUT (the enclosing row's fields/intrinsics/lifts);\n ^^name two scopes out. `^path` = the enclosing row's path.\n ^docs ^nodes ^blocks ^links\n THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root\n scope's row, one scope out from a top-level row, so ^docs there is\n the whole documents scan (uncorrelated until you add a ^ predicate);\n one caret per enclosing block (^^docs from depth two), or the\n absolute 0^docs from ANY depth. ^id / 0^id = the repository id.\n ^$it = the root object: ^$it.docs \u2261 ^docs, entries(^$it) names the\n four collections. At the root scope itself a bare target is the scan\n (docs count { \u2026 }, from docs, entries(docs)).\n ERRORS: a bare `docs` INSIDE a block reads a property of the current\n row, which has none \u2014 refused (\"did you mean ^docs\"); `^docs` AT THE\n TOP LEVEL reaches past the root \u2014 refused (write the bare `docs`);\n so is `$repo` (surface < 2.0; the message names the replacement).\n nodes/blocks on a doc row are that doc's RELATIONS, not the root.\n $it the current item itself (a row, or each ELEMENT when the\n receiver is a list property); ^$it = the enclosing row.\n $value is NOT an intrinsic (since oqx 0.15): it reads a\n property literally named $value, absent when there is none.\n $key the row's identity (0.20): a store row's id (doc/block/node/edge), an entries(x)\n entry's key, a follow occurrence's walk identity; project $key: <expr> to rekey the\n rows a block yields (a destination block's $key: is not a column).\n\n## Scalar semantics (@omgbase/oqx)\n\n comparisons == != < <= > >= strict & typed (5 == \"5\" is false); either\n side may be a field (field-vs-field OK).\n absence null and a missing field are the same \"absent\" value; two absents\n are EQUAL, so `absent != \"x\"` is TRUE and `absent == null` is TRUE.\n Relational (< <= > >=) on an absent operand \u2192 FALSE. Bare `f` in\n boolean position uses JS truthiness (absent/false/0/\"\" \u21D2 false;\n NOTE: \"false\", [] and {} are truthy). has(f) = explicit presence.\n boolean and or not grouping ( \u2026 ) \u2014 && || ! are accepted synonyms\n arithmetic + - * / % (+ concatenates if either side is a string)\n membership x in y array \u2192 typed membership; string \u2192 substring;\n object \u2192 key exists; range \u2192 coverage. Any list\n property works directly: \"pricing\" in tags. list(f)\n coerces scalar-or-list (absent \u2192 []) \u2014 use it when a\n key is sometimes scalar, sometimes a list.\n ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..\n era in 800..1680; works over ISO dates. range(prop) reads a\n range-shaped STRING property as an interval: \"2026-01-15\" in range(window).\n strings x.contains(\"s\") x.startsWith(\"p\") x.endsWith(\"s\") x.matches(\"^re$\")\n (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE \u2014\n fold with .lower(): title.lower().contains(\"aurora\").\n functions has(f) size(x) list(x) entries(x) range(s)\n literals \"str\" 'str' 123 1.5 true false null\n determinism no now()/clock/random. Lists/structs are not literals; ternary is\n not supported. Errors surface as filter_invalid.\n scalars vs lists a list-authored key never == a scalar (tags == \"x\" is false\n when tags is a list) \u2014 use `\"x\" in tags` / `in list(tags)`.\n\n## Domain predicates (omgbase)\n\n text(\"terms\") FTS5 PRUNING predicate (docs match via their blocks, nodes\n via the node index); composes anywhere, incl. nested scopes.\n semantic(\"phrase\") embedding cosine SCORE (docs/blocks): threshold it\n (semantic(\"x\") > 0.6), project it, or `order by semantic(\"x\")\n desc` for top-K. Needs a provider that has embedded the repository; else provider_unavailable.\n blocks target only:\n under(id_or_heading) block in the containment subtree / section range\n under_heading(\"Launch\") an ancestor section heading contains the text (ci)\n under_kind(type[, name]) an ancestor block has that type (name: its text\n contains / its key equals name)\n within(\"path\" | \"d_..\") containing doc = id, exact path, or path glob (*);\n either path form\n yaml_path(\"a.b\") YAML docs: the block at that key path\n json_pointer(\"#/a/b\") JSON docs: the block at that pointer\n has_edge(\"pred\"[, target]) an open authored edge with that predicate leaves here\n has_anchor() block carries an authored ^ref\n parent_type() == \"x\" parent block's type (must be compared)\n child_count() > 0 number of direct children (must be compared)\n refs(x) the live documents a property's document references name:\n x is a string, a list or absent; each \"/a/b.md\", \"a/b.md\"\n or \"d_\u2026\" element resolves to that doc's row; dangling and\n non-string elements are dropped. Yields docs rows, so it is\n a source, a receiver or a follow destination:\n follow refs(before), refs(after) walks a timeline both ways.\n The reverse direction needs no function: a document's\n path is already the reference form, so\n ^docs { where ^path in list(after) } is\n \"the documents whose `after` names me\".\n A HIT IS A STORE ROW: a top-level row that is not a doc/block/node/edge fails\n (filter_invalid). `follow before` over a list of paths\n reaches the STRINGS \u2014 write `follow refs(before)`.\n\n## Nested queries & consumers\n\n <receiver> collect|exists|none|count|first|single { <block> }\n receiver = a relation (nodes, blocks, doc.out_links, section.blocks, \u2026), a\n reach-through row (dst, doc: one row \u2014 dst { title }), a\n list property (tags), entries(x), or a root collection (^docs; 0^docs at depth \u2265 2).\n block = [select \u2026] [where \u2026] [follow \u2026] [order by \u2026] [limit N] [offset N]\n \u2014 the same fixed order as the top level (`from` optional; the\n receiver supplies the rows). A leading name/alias list is the\n projection (`select` dropped); a predicate always needs `where`.\n exists \u22651 row nodes exists { where kind == \"md:task\" and not checked }\n none 0 rows nodes none { where kind == \"md:task\" and not checked } (\"every task done\")\n count a number nodes count { where kind == \"md:task\" } >= 2\n collect rows (default) \u2014 in select for nested results; in where with a lift\n first / single zero-or-one (single errors on >1) \u2014 lookups in select\n distinct on any consumer: dedup by projected value \u2014 nodes count distinct { select kind }\n limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the\n consumer reduces: nodes exists { offset 1 } = at least two;\n first { \u2026 offset 1 } = the second.\n lift `^name:` in a where-collect binds values outward:\n select path, open from docs where nodes collect { ^open: value where kind == \"md:task\" and not checked }\n joins ^<t> exists { where slug == ^ref } semi-join; not \u2026 exists anti-join;\n ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } lookup;\n x in ^keys membership over a lifted set.\n\n## select \u2014 projection\n\n select a, alias: expr, nested: nodes collect { \u2026 }\n bare key \u2192 the attribute when it is one (id, path, title, \u2026), else the property\n (scalar-authored \u2192 scalar; list \u2192 array); data.<k> forces the property;\n a call/arithmetic needs an alias (n: size(tags)) unless in values mode;\n an output key of id/path other than the attribute itself needs an alias.\n raw whole file bytes (docs) \u2014 docs_read is cheaper for one doc.\n distinct select distinct type (dedup hits by projected value)\n values select <ONE item> values \u2192 bare values instead of records:\n from docs select distinct type values \u2192 values: [\"hub\",\"lab-note\",\u2026]\n select path values from docs \u2192 values: [\"/index.md\", \u2026]\n tags: tags collect { $it values } \u2192 a plain array\n h: nodes first { name values where kind == \"md:section\" order by first_ordinal }\n entries(x) a record as a collection ($key / $it per entry), key order:\n select fm: entries(data) collect { k: $key, v: $it }\n where entries(frontmatter) exists { where $key == \"era\" and $it > 1600 }\n entries(inline), entries(attrs), a nested map, or a list (index keys)\n $semantic_score is not a field \u2014 project semantic(\"\u2026\") under an alias instead.\n\n## order by, limit/offset, pagination\n\n order by era desc, path asc absent values sort LAST (asc); ties \u2192 (path, id)\n limit N / offset N define the result SET (integer literals);\n the tool's limit/cursor then page WITHIN it\n cursor opaque keyset on (path,id); a custom order by\n disables it (you get the top page + truncated)\n\n## Graph traversal (follow) and links\n\n The citation graph is the LINK ROWS walked through a destination block:\n dst is a link's destination document (absent for an external or dangling\n link \u2014 it then contributes no successor), doc its source.\n from docs where <seed> follow out_links collect { from dst } docs the seed links TO\n from docs where <seed> follow in_links collect { from doc } backlinks\n follow out_links collect { from dst depth 3 } bound (1..8, default 8) \u2014 a clause of the block\n follow out_links collect { from dst where layer == \"canon\" } keep only matching successors\n follow in_links collect { from doc where ^path in list(before) } ^ = the row being expanded\n (^^ = the walk's enclosing scope)\n follow out_links collect { from dst where predicate == \"depends_on\" } one predicate only\n follow out_links collect { from dst }, in_links collect { from doc }\n several destinations: their union\n (one step dedups by identity)\n follow ^docs { where out_links exists { where dst_path == ^^path } }\n a destination block over the root's\n documents, re-read per frontier row\n (^ = that row) = the backlinks walk\n follow out_links collect { from dst frontier type == \"practitioner\" } cut, keeping the frontier row\n from docs where type == \"process\" frontier !verified follow out_links collect { from dst }\n the BODY's frontier applies to the seeds:\n an unverified seed is kept, not expanded\n follow distinct out_links collect { from dst } / \u2026 { $key: group from dst } identity control ($key)\n follow refs(before), ^docs { where ^path in list(after) } forward over `before`, backward\n over every doc whose `after` names me\n Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|\n depth|cycle), $leaf/$frontier, $ordinal, $key \u2014 usable in select/order by and\n the top-level post-walk where, NOT metadata of the candidates a destination\n block's where reads.\n Destination blocks (OQX 0.20): a brace after ANY destination opens that\n destination's block \u2014 children { depth 2 }, refs(x) { depth 2 }, out_links\n collect { from dst depth 2 }, ^docs { where ^path in list(after) } \u2014 with the\n 0.17 body rules (where-first, projections) and two clauses of its own after\n `where`: frontier <pred> (occurrences reached through this destination that\n match are frontiers; ^ there is the walk's enclosing scope) and depth <n>\n (this destination expands a frontier row only while $depth < n). The body\n that carries the follow may write frontier <pred> too, after its where and\n before follow: it applies to the SEEDS (read like the body's where) \u2014 a\n matching seed stays in the result with $stop == \"frontier\" and is not\n expanded; each destination's frontier is its own. There is no options block;\n the old `by` is $key. A bare follow ^docs (no brace, no consumer) is an\n error naming both block spellings.\n Type-preserving relations: block.children, section.children, section.subsections;\n the citation graph is out_links collect { from dst } / in_links collect { from doc }.\n from links where dst_path == \"/notes/x.md\" inbound links to a doc, as rows\n select d: dst.title, k: dst_kind from links the destination document's title\n from docs where out_links exists { where dst.layer == \"draft\" } links into drafts\n select b: in_links collect distinct { from doc } from docs backlinks as { id, path }\n from links where predicate == \"depends_on\" select doc_id, dst_path\n from docs where not doc.in_links exists { } orphans (nothing links in)\n (The `graph` tool is a convenience wrapper that compiles to a follow query.)\n\n## semantic grain\n\nfrom docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.\ntext()/where prune the candidate set; only order by reweights.\n\n## Examples\n\n from docs where layer == \"working\"\n from docs where path.startsWith(\"/guides/\") and updated_at >= \"2026-08-01\"\n from docs where \"pricing\" in list(tags) select layer, tags\n from docs where inline.owner == \"alice\"\n from docs where title.lower().contains(\"q3 plan\")\n from docs where format == \"yaml\"\n from docs where type == \"practitioner\" order by era desc limit 2\n from docs where nodes none { where kind == \"md:task\" and not checked }\n from docs where tags exists { where $it == \"tria-prima\" }\n from docs where path == \"/x.md\" select fm: entries(data) collect { k: $key, v: $it }\n from docs select owner_id, owner: ^nodes single { where kind == \"person\" and attrs.id == ^owner_id }\n from blocks where type == \"task\" and not checked and under_heading(\"Launch\") and doc.layer == \"working\"\n from blocks where type == \"paragraph\" and has_edge(\"references\", \"d_92aaaaa\")\n select path, $depth from docs where path == \"/timeline/review.md\" follow refs(before), refs(after)\n select path, next: ^docs collect { path where ^path in list(before) } from docs where type == \"milestone\"\n select path, n: size(^docs), repo: ^id from docs limit 1\n select p: data.path, t: data.title from docs where has(data.path)\n docs { type == \"substance\" and not verified }\n select subject, process: ^docs[slug == ^subject]!.path from docs where type == \"lab-note\"\n select path, back: nodes first { select p: 0^docs collect { path where ^^path in list(after) } values } from docs where type == \"milestone\"\n select path, prior: refs(before) collect { path, when } from docs where type == \"milestone\"\n from blocks order by semantic(\"identity preservation across edits\") desc limit 10\n from links where dst_kind == \"external\" select uri\n docs count { where layer == \"canon\" }\n\nCommon mistake: `$path`/`$id` (the 2.x spelling) \u2014 omgbase has no $ names; write\n`path`/`id`. Another: `where tags == \"x\"` on a list-valued key is false \u2014 write\n`\"x\" in tags`. Another: `select data.path` \u2014 alias it (`p: data.path`).\n";
1
+ export declare const QUERY_SYNTAX = "# query \u2014 OQX syntax reference\n\nThe `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`). There\nis no {from, filter, order} envelope: every concern is a clause of the string.\n\n [select <items>] from <target> [where <pred>] [frontier <pred>] [follow <dest> [{ \u2026 }], \u2026]\n [order by <expr> [asc|desc], \u2026] [limit N] [offset N]\n <target> count|exists|none|first|single { <block> } (scalar/one-row form: a bare target at the root scope)\n\nCLAUSE ORDER IS FIXED: select, from, where, frontier, follow, order by, limit, offset \u2014\neach at most once; an out-of-order clause is a parse error naming the order.\nOnly `select` may drop its keyword, and only as the first clause\n(`path, era from docs where \u2026`). A top-level predicate needs `where`, because\n`where` follows `from` (`era > 1600 from docs` is the clause-order error); a\nblock may LEAD with a predicate (see Sugar), but a predicate after a projection\nstill needs the keyword (`{ name where kind == \"md:task\" }`, not\n`{ name, kind == \"md:task\" }`); `from docs count` is an error (write\n`docs count { \u2026 }`). `where` and later `select` items may use the same body's\n`select` aliases (`select path, old: era < 1000 from docs where old`; an alias\nshadows a same-named field there). `and` / `or` / `not` are the connectives;\nthe symbols `&&` / `||` / `!` are accepted synonyms (the words are canonical and\nwhat `print` writes).\n\n## Sugar (OQX 0.17)\n\nEvery form here is shorthand for an explicit directive (the AST is the explicit form):\n nodes { kind == \"md:task\" } \u2261 nodes collect { where kind == \"md:task\" } \u2014 a receiver block with\n no consumer is collect; a block whose LEADING expression is a predicate\n (comparison, call, !/is/not, in, literal, (\u2026), a consumer test \u2014 anything\n but a bare name, a dotted path or a ^lift) is where-first. Bare names still\n project: nodes { name } \u2261 nodes collect { name }; filter a bare field with\n nodes { is checked }. Wherever a value stands (0.20), call arguments included:\n size(children { is checked }); x distinct { \u2026 } \u2261 x collect distinct { \u2026 }. After a\n follow destination too: follow children { depth 2 } is that destination's block.\n (a, b) a tuple (0.20): two or more expressions, concatenated one level flat \u2014 a value\n (size((tags, aliases)), \"x\" in (tags, aliases)) and a receiver ((a, b) { \u2026 },\n follow (before, after) { depth 2 }); (a) alone is grouping\n refs(x)[0] \u2261 refs(x) first { offset 0 } positional (integer literal or ${binding});\n out of range \u21D2 absent; refs(x)[0].path navigates the row\n ^docs[path == ^company] \u2261 ^docs first { where path == ^company } first match or absent\n ^docs[path == ^company]! \u2261 \u2026 single { \u2026 } required: exactly one, else filter_invalid\n title! required: title, or an error naming the expression (and the row's $key \u2014 its id)\n \u2014 never a filter, never a coercion (0!, \"\"! are values); tightest: a!.b vs a.b!\n is x / not x truthiness of x, and its negation (prefix; `not` \u2261 `!`)\n x is y / x is not y identity ($key: a store row's id, else structural) \u2014 compares rows, which == does not;\n x is null = absent; for scalars is \u2261 ==; comparison precedence, no chaining\n a and b / a or b the connectives (\u2261 && / ||: same precedence, short-circuit, value: data.title or path coalesces)\n boss: ^docs[path == ^manager], bossName: boss.title a select item may use the items to its LEFT\n Reserved words (never a bare field name): from where select is not and or, true false null.\n Contextual words (limit order offset follow collect count first distinct desc by in frontier depth) parse as\n fields in expression position (select o: order from docs).\n\nResults are LEAN hits \u2014 { id, path } + whatever `select` projects (or `values`,\n`count`, `exists`, `none`). The envelope keys ARE the attributes: copy a hit's\n`id` straight into `where id == \"\u2026\"` (no translation); `select id, path` is simply\nthe envelope (no duplicate columns); any OTHER projection whose output key would\nbe `id` or `path` must be aliased \u2014 `select data.path` is refused (\"projection\n`path` collides with the hit envelope \u2014 alias it (`p: data.path`)\"); a row\nsurfacing as a value (an empty `collect { }`) is the same `{ id, path }`. Every\nother tool spells a row the same way (`docs_read` \u2192 `{ id, path, rev, properties,\nraw }`, `blocks_read` \u2192 `{ id, type, text, raw, hash, \u2026 }`, `docs_list` \u2192 `{ id,\npath, updated_at, blocks }`, a revision \u2192 `{ id, path, hash }`): hydrate by id via\nblocks_read/docs_read.\n\n## Paths\n\nEvery path the surface returns is `/`-rooted \u2014 the form a reference is\nwritten in (`[x](/projects/oqx.md)`, `before: [/timeline/kickoff.md]`): a\nhit's `path`, an edge's `dst_path`, every tool's paths. Every path it\naccepts tolerates both forms (`/a.md` or `a.md`): tool arguments, `refs(x)`,\n`within(...)`, and a string LITERAL compared with `path`/`dst_path` (bare, or\nthrough a row handle: `doc.path`) by `==`/`!=` or passed to their\n`.startsWith(...)` (it is rooted first, so `path == \"a.md\"` and `path == \"/a.md\"`\nboth match). A property holding a reference compares directly \u2014 and is never\nrooted: `where ^path in list(after)`, `where customer == ^path`,\n`where data.path == \"/x.md\"` (the author's `path:` key, as written) \u2014 no\n`\"/\" + ...` glue.\n\n## Targets & field namespaces\n\nTHE NAME RULE: `$` belongs to OQX \u2014 $it, $key, $depth, $stop, $leaf, $frontier,\n$ordinal are values the LANGUAGE injects into a scope; omgbase adds NO $ names\n(a `$id` in a query is an unknown name, absent). Every engine attribute is a BARE\nsnake_case name that WINS on collision \u2014 the convention the relations (children,\nblocks, out, in, doc, frontmatter, \u2026) always followed: a bare name that is an\nengine attribute reads the engine's value, never yours. Your data is bare too;\nwhere a key of yours collides with an engine name, reach it through a source\nhandle: `data.<k>` (the merged authored view \u2014 frontmatter + inline, exactly the\nread a bare non-colliding key performs), `frontmatter.<k>`, `inline.<k>`. So\n`path` is the engine's /-rooted location and `data.path` your `path:` key;\n`tags` is your tags \u222A the body #hashtags and `data.tags` the authored list alone;\n`title` the computed title (the first H1) and `data.title` your `title:`;\n`entries(data)` lists every authored top-level key. Same value, same name\neverywhere: a hit's `id` is docs_read's `id` and the `id` argument of blocks_read.\n\n Reserved bare names per kind (engine attributes; handles; self; relations):\n docs id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes out in out_links in_links\n blocks id path doc_id raw hash updated_at type text attrs depth ordinal | block doc | children nodes out_links section\n nodes id path doc_id block_id kind name value attrs | section doc block | blocks subsections children\n links id path doc_id block_id via_id dst_id dst_path uri field dst_kind predicate provenance anchor | doc\n A key of yours with any of these names is `data.<k>` (or frontmatter.<k> /\n inline.<k> by source; attrs.<k> on a block or node).\n\ndocs:\n - bare identifier = a document PROPERTY (frontmatter + inline `key:: value`,\n unioned; nested via dots: meta.owner). YAML/JSON docs\n expose the parsed object's keys the same way.\n - source handles = data.<k> (merged authored view) / frontmatter.<k> /\n inline.<k>; the whole bag as a collection via entries(data)\n / entries(frontmatter) / entries(inline) (see entries()).\n - attributes = id, path (/-rooted, see Paths), updated_at (ISO-8601,\n sorts chronologically), raw (the whole file, frontmatter\n included), hash (its sha256, 64 hex), title (the computed\n title: the first H1; data.title is the authored key), tags\n (authored tags \u222A body #hashtags, deduped, authored first;\n data.tags is the authored list), format (the doc's format \u2014\n \"markdown\"/\"yaml\"/\"json\"; present on EVERY doc, so\n has(format) is always true and `format == \"yaml\"` is the\n filter).\n - relations = nodes, blocks, doc.out_links / doc.in_links (a doc's\n links as rows; the documents at their other end are\n out_links collect distinct { from dst } and\n in_links collect distinct { from doc } \u2014 there is no\n out / in relation).\n\nblocks:\n - bare fields = doc_id (the owning doc), type, text (normalized), ordinal\n (0-based among siblings), depth (0 at the top level), and\n every attrs key FLATTENED onto the row (bare `checked`\n reads attrs.checked; `attrs.<k>` still works; the\n structural fields win on collision; absent \u21D2 false).\n - attributes = id, path (the owning doc's path), raw (the block's raw\n source \u2014 a container's raw holds its children), hash\n (its sha256 \u2014 the value update/split expect as\n expect.content_hash), updated_at.\n - reach-through = doc.<key> / doc.path / doc.id / doc.format = the\n CONTAINING doc (doc.id and doc.path cost nothing \u2014 they\n read the row's own doc_id / joined path, no lookup).\n - relations = block.children, block.nodes, block.out_links, section.\n NOTE: inside a `follow`, $depth/$ordinal are ALWAYS the walk's metadata; a\n block's own nesting is `depth` / `ordinal` (no $) everywhere.\n\nnodes:\n - bare fields = doc_id, block_id (null for a block-less node), kind\n (md:task / md:link / md:section / yaml:\u2026), name, value,\n + attrs keys flattened (checked, level).\n - attributes = id, path.\n - reach-through = doc.<key> / doc.id / doc.path, block.type / block.text\n / block.id / block.path.\n - relations = section.blocks, section.children, section.subsections.\n\nlinks: (the authored link graph as rows \u2014 one per open link: the relation a link\n establishes; the md:link NODE is its occurrence in the text and via_id\n points at it \u2014 a frontmatter link has a row and no node)\n - bare fields = doc_id (the SOURCE doc), block_id (the source block; null\n for a frontmatter edge), via_id, dst_id (the destination\n node id: a doc id, an external node id or phantom:<path>),\n dst_path (target doc path, /-rooted; NULL when\n dangling/external), uri (external URL; NULL otherwise),\n predicate (\"references\"/\"embeds\"/freeform), provenance\n (\"link\"/\"frontmatter\"/\"inline_field\"/\u2026), dst_kind\n (\"document\"/\"external\"/\"collection\"), anchor, field (the\n frontmatter key / inline field it was authored in).\n - attributes = id, path (the SOURCE doc's path); doc.<key> reaches the\n source document. There is no from_commit: a query sees\n open edges only (history is blocks_history/changes_since).\n - text()/semantic() are N/A on links.\n\n## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL\n\n name the CURRENT row only \u2014 never falls through to an enclosing row\n or the repository; an absent field is simply absent.\n ^name one scope OUT (the enclosing row's fields/intrinsics/lifts);\n ^^name two scopes out. `^path` = the enclosing row's path.\n ^docs ^nodes ^blocks ^links\n THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root\n scope's row, one scope out from a top-level row, so ^docs there is\n the whole documents scan (uncorrelated until you add a ^ predicate);\n one caret per enclosing block (^^docs from depth two), or the\n absolute 0^docs from ANY depth. ^id / 0^id = the repository id.\n ^$it = the root object: ^$it.docs \u2261 ^docs, entries(^$it) names the\n four collections. At the root scope itself a bare target is the scan\n (docs count { \u2026 }, from docs, entries(docs)).\n ERRORS: a bare `docs` INSIDE a block reads a property of the current\n row, which has none \u2014 refused (\"did you mean ^docs\"); `^docs` AT THE\n TOP LEVEL reaches past the root \u2014 refused (write the bare `docs`);\n so is `$repo` (surface < 2.0; the message names the replacement).\n nodes/blocks on a doc row are that doc's RELATIONS, not the root.\n $it the current item itself (a row, or each ELEMENT when the\n receiver is a list property); ^$it = the enclosing row.\n $value is NOT an intrinsic (since oqx 0.15): it reads a\n property literally named $value, absent when there is none.\n $key the row's identity (0.20): a store row's id (doc/block/node/edge), an entries(x)\n entry's key, a follow occurrence's walk identity; project $key: <expr> to rekey the\n rows a block yields (a destination block's $key: is not a column).\n\n## Scalar semantics (@omgbase/oqx)\n\n comparisons == != < <= > >= strict & typed (5 == \"5\" is false); either\n side may be a field (field-vs-field OK).\n absence null and a missing field are the same \"absent\" value; two absents\n are EQUAL, so `absent != \"x\"` is TRUE and `absent == null` is TRUE.\n Relational (< <= > >=) on an absent operand \u2192 FALSE. Bare `f` in\n boolean position uses JS truthiness (absent/false/0/\"\" \u21D2 false;\n NOTE: \"false\", [] and {} are truthy). has(f) = explicit presence.\n boolean and or not grouping ( \u2026 ) \u2014 && || ! are accepted synonyms\n arithmetic + - * / % (+ concatenates if either side is a string)\n membership x in y array \u2192 typed membership; string \u2192 substring;\n object \u2192 key exists; range \u2192 coverage. Any list\n property works directly: \"pricing\" in tags. list(f)\n coerces scalar-or-list (absent \u2192 []) \u2014 use it when a\n key is sometimes scalar, sometimes a list.\n ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..\n era in 800..1680; works over ISO dates. range(prop) reads a\n range-shaped STRING property as an interval: \"2026-01-15\" in range(window).\n strings x.contains(\"s\") x.startsWith(\"p\") x.endsWith(\"s\") x.matches(\"^re$\")\n (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE \u2014\n fold with .lower(): title.lower().contains(\"aurora\").\n functions has(f) size(x) list(x) entries(x) range(s)\n literals \"str\" 'str' 123 1.5 true false null\n determinism no now()/clock/random. Lists/structs are not literals; ternary is\n not supported. Errors surface as filter_invalid.\n scalars vs lists a list-authored key never == a scalar (tags == \"x\" is false\n when tags is a list) \u2014 use `\"x\" in tags` / `in list(tags)`.\n\n## Domain predicates (omgbase)\n\n text(\"terms\") FTS5 PRUNING predicate (docs match via their blocks, nodes\n via the node index); composes anywhere, incl. nested scopes.\n semantic(\"phrase\") embedding cosine SCORE (docs/blocks): threshold it\n (semantic(\"x\") > 0.6), project it, or `order by semantic(\"x\")\n desc` for top-K. Needs a provider that has embedded the repository; else provider_unavailable.\n blocks target only:\n under(id_or_heading) block in the containment subtree / section range\n under_heading(\"Launch\") an ancestor section heading contains the text (ci)\n under_kind(type[, name]) an ancestor block has that type (name: its text\n contains / its key equals name)\n within(\"path\" | \"d_..\") containing doc = id, exact path, or path glob (*);\n either path form\n yaml_path(\"a.b\") YAML docs: the block at that key path\n json_pointer(\"#/a/b\") JSON docs: the block at that pointer\n has_edge(\"pred\"[, target]) an open authored edge with that predicate leaves here\n has_anchor() block carries an authored ^ref\n parent_type() == \"x\" parent block's type (must be compared)\n child_count() > 0 number of direct children (must be compared)\n refs(x) the live documents a property's document references name:\n x is a string, a list or absent; each \"/a/b.md\", \"a/b.md\"\n or \"d_\u2026\" element resolves to that doc's row; dangling and\n non-string elements are dropped. Yields docs rows, so it is\n a source, a receiver or a follow destination:\n follow refs(before), refs(after) walks a timeline both ways.\n The reverse direction needs no function: a document's\n path is already the reference form, so\n ^docs { where ^path in list(after) } is\n \"the documents whose `after` names me\".\n A HIT IS A STORE ROW: a top-level row that is not a doc/block/node/edge fails\n (filter_invalid). `follow before` over a list of paths\n reaches the STRINGS \u2014 write `follow refs(before)`.\n\n## Nested queries & consumers\n\n <receiver> collect|exists|none|count|first|single { <block> }\n receiver = a relation (nodes, blocks, doc.out_links, section.blocks, \u2026), a\n reach-through row (dst, doc: one row \u2014 dst { title }), a\n list property (tags), entries(x), or a root collection (^docs; 0^docs at depth \u2265 2).\n block = [select \u2026] [where \u2026] [frontier \u2026] [follow \u2026] [order by \u2026] [limit N] [offset N]\n \u2014 the same fixed order as the top level (`from` optional; the\n receiver supplies the rows). A leading name/alias list is the\n projection (`select` dropped); a predicate always needs `where`.\n exists \u22651 row nodes exists { where kind == \"md:task\" and not checked }\n none 0 rows nodes none { where kind == \"md:task\" and not checked } (\"every task done\")\n count a number nodes count { where kind == \"md:task\" } >= 2\n collect rows (default) \u2014 in select for nested results; in where with a lift\n first / single zero-or-one (single errors on >1) \u2014 lookups in select\n distinct on any consumer: dedup by projected value \u2014 nodes count distinct { select kind }\n limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the\n consumer reduces: nodes exists { offset 1 } = at least two;\n first { \u2026 offset 1 } = the second.\n lift `^name:` in a where-collect binds values outward:\n select path, open from docs where nodes collect { ^open: value where kind == \"md:task\" and not checked }\n joins ^<t> exists { where slug == ^ref } semi-join; not \u2026 exists anti-join;\n ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } lookup;\n x in ^keys membership over a lifted set.\n\n## select \u2014 projection\n\n select a, alias: expr, nested: nodes collect { \u2026 }\n bare key \u2192 the attribute when it is one (id, path, title, \u2026), else the property\n (scalar-authored \u2192 scalar; list \u2192 array); data.<k> forces the property;\n a call/arithmetic needs an alias (n: size(tags)) unless in values mode;\n an output key of id/path other than the attribute itself needs an alias.\n raw whole file bytes (docs) \u2014 docs_read is cheaper for one doc.\n distinct select distinct type (dedup hits by projected value)\n values select <ONE item> values \u2192 bare values instead of records:\n select distinct type values from docs \u2192 values: [\"hub\",\"lab-note\",\u2026]\n select path values from docs \u2192 values: [\"/index.md\", \u2026]\n tags: tags collect { $it values } \u2192 a plain array\n h: nodes first { name values where kind == \"md:section\" order by first_ordinal }\n entries(x) a record as a collection ($key / $it per entry), key order:\n select fm: entries(data) collect { k: $key, v: $it }\n where entries(frontmatter) exists { where $key == \"era\" and $it > 1600 }\n entries(inline), entries(attrs), a nested map, or a list (index keys)\n $semantic_score is not a field \u2014 project semantic(\"\u2026\") under an alias instead.\n\n## order by, limit/offset, pagination\n\n order by era desc, path asc absent values sort LAST (asc); ties \u2192 (path, id)\n limit N / offset N define the result SET (integer literals);\n the tool's limit/cursor then page WITHIN it\n cursor opaque keyset on (path,id); a custom order by\n disables it (you get the top page + truncated)\n\n## Graph traversal (follow) and links\n\n The citation graph is the LINK ROWS walked through a destination block:\n dst is a link's destination document (absent for an external or dangling\n link \u2014 it then contributes no successor), doc its source.\n from docs where <seed> follow out_links collect { from dst } docs the seed links TO\n from docs where <seed> follow in_links collect { from doc } backlinks\n follow out_links collect { from dst depth 3 } bound (1..8, default 8) \u2014 a clause of the block\n follow out_links collect { from dst where layer == \"canon\" } keep only matching successors\n follow in_links collect { from doc where ^path in list(before) } ^ = the row being expanded\n (^^ = the walk's enclosing scope)\n follow out_links collect { from dst where predicate == \"depends_on\" } one predicate only\n follow out_links collect { from dst }, in_links collect { from doc }\n several destinations: their union\n (one step dedups by identity)\n follow ^docs { where out_links exists { where dst_path == ^^path } }\n a destination block over the root's\n documents, re-read per frontier row\n (^ = that row) = the backlinks walk\n follow out_links collect { from dst frontier type == \"practitioner\" } cut, keeping the frontier row\n from docs where type == \"process\" frontier !verified follow out_links collect { from dst }\n the BODY's frontier applies to the seeds:\n an unverified seed is kept, not expanded\n follow distinct out_links collect { from dst } / \u2026 { $key: group from dst } identity control ($key)\n follow refs(before), ^docs { where ^path in list(after) } forward over `before`, backward\n over every doc whose `after` names me\n Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|\n depth|cycle), $leaf/$frontier, $ordinal, $key \u2014 usable in select/order by and\n the top-level post-walk where, NOT metadata of the candidates a destination\n block's where reads.\n Destination blocks (OQX 0.20): a brace after ANY destination opens that\n destination's block \u2014 children { depth 2 }, refs(x) { depth 2 }, out_links\n collect { from dst depth 2 }, ^docs { where ^path in list(after) } \u2014 with the\n 0.17 body rules (where-first, projections) and two clauses of its own after\n `where`: frontier <pred> (occurrences reached through this destination that\n match are frontiers; ^ there is the walk's enclosing scope) and depth <n>\n (this destination expands a frontier row only while $depth < n). The body\n that carries the follow may write frontier <pred> too, after its where and\n before follow: it applies to the SEEDS (read like the body's where) \u2014 a\n matching seed stays in the result with $stop == \"frontier\" and is not\n expanded; each destination's frontier is its own. There is no options block;\n the old `by` is $key. A bare follow ^docs (no brace, no consumer) is an\n error naming both block spellings.\n Type-preserving relations: block.children, section.children, section.subsections;\n the citation graph is out_links collect { from dst } / in_links collect { from doc }.\n from links where dst_path == \"/notes/x.md\" inbound links to a doc, as rows\n select d: dst.title, k: dst_kind from links the destination document's title\n from docs where out_links exists { where dst.layer == \"draft\" } links into drafts\n select b: in_links collect distinct { from doc } from docs backlinks as { id, path }\n select doc_id, dst_path from links where predicate == \"depends_on\"\n from docs where not doc.in_links exists { } orphans (nothing links in)\n (The `graph` tool is a convenience wrapper that compiles to a follow query.)\n\n## semantic grain\n\nfrom docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.\ntext()/where prune the candidate set; only order by reweights.\n\n## Examples\n\n from docs where layer == \"working\"\n from docs where path.startsWith(\"/guides/\") and updated_at >= \"2026-08-01\"\n select layer, tags from docs where \"pricing\" in list(tags)\n from docs where inline.owner == \"alice\"\n from docs where title.lower().contains(\"q3 plan\")\n from docs where format == \"yaml\"\n from docs where type == \"practitioner\" order by era desc limit 2\n from docs where nodes none { where kind == \"md:task\" and not checked }\n from docs where tags exists { where $it == \"tria-prima\" }\n select fm: entries(data) collect { k: $key, v: $it } from docs where path == \"/x.md\"\n select owner_id, owner: ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } from docs\n from blocks where type == \"task\" and not checked and under_heading(\"Launch\") and doc.layer == \"working\"\n from blocks where type == \"paragraph\" and has_edge(\"references\", \"d_92aaaaa\")\n select path, $depth from docs where path == \"/timeline/review.md\" follow refs(before), refs(after)\n select path, next: ^docs collect { path where ^path in list(before) } from docs where type == \"milestone\"\n select path, n: size(^docs), repo: ^id from docs limit 1\n select p: data.path, t: data.title from docs where has(data.path)\n docs { type == \"substance\" and not verified }\n select subject, process: ^docs[slug == ^subject]!.path from docs where type == \"lab-note\"\n select path, back: nodes first { select p: 0^docs collect { path where ^^path in list(after) } values } from docs where type == \"milestone\"\n select path, prior: refs(before) collect { path, when } from docs where type == \"milestone\"\n from blocks order by semantic(\"identity preservation across edits\") desc limit 10\n select uri from links where dst_kind == \"external\"\n docs count { where layer == \"canon\" }\n\nCommon mistake: `$path`/`$id` (the 2.x spelling) \u2014 omgbase has no $ names; write\n`path`/`id`. Another: `where tags == \"x\"` on a list-valued key is false \u2014 write\n`\"x\" in tags`. Another: `select data.path` \u2014 alias it (`p: data.path`).\n";