datanotes 3.3.1 → 3.4.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 (68) hide show
  1. package/SECURITY.md +5 -3
  2. package/dist/api/extensions.cjs +141 -5
  3. package/dist/api/extensions.js +141 -5
  4. package/dist/api/index.cjs +141 -5
  5. package/dist/api/index.js +141 -5
  6. package/dist/api/mcp.cjs +141 -5
  7. package/dist/api/mcp.js +141 -5
  8. package/dist/datanotes.cjs +1672 -600
  9. package/dist/docs/administration/extensions.md +2 -2
  10. package/dist/docs/api/operations/add_column.md +1 -0
  11. package/dist/docs/api/operations/check_table.md +1 -0
  12. package/dist/docs/api/operations/create_table.md +1 -0
  13. package/dist/docs/api/operations/describe_database.md +2 -0
  14. package/dist/docs/api/operations/describe_history.md +1 -0
  15. package/dist/docs/api/operations/describe_semantic_index.md +1 -0
  16. package/dist/docs/api/operations/describe_table.md +1 -0
  17. package/dist/docs/api/operations/graph_neighbors.md +1 -0
  18. package/dist/docs/api/operations/graph_path.md +1 -0
  19. package/dist/docs/api/operations/graph_traverse.md +1 -0
  20. package/dist/docs/api/operations/infer_table.md +1 -0
  21. package/dist/docs/api/operations/list_changes.md +1 -0
  22. package/dist/docs/api/operations/list_folder.md +1 -0
  23. package/dist/docs/api/operations/list_link_targets.md +1 -0
  24. package/dist/docs/api/operations/list_migrations.md +1 -0
  25. package/dist/docs/api/operations/list_tables.md +2 -0
  26. package/dist/docs/api/operations/list_webhooks.md +1 -0
  27. package/dist/docs/api/operations/read_note.md +1 -0
  28. package/dist/docs/api/operations/search.md +39 -0
  29. package/dist/docs/api/operations/search_notes.md +1 -0
  30. package/dist/docs/api/operations/select_rows.md +1 -0
  31. package/dist/docs/api/operations/semantic_search.md +1 -0
  32. package/dist/docs/api/operations/table_changes.md +1 -0
  33. package/dist/docs/api/operations/tags/queries.md +1 -0
  34. package/dist/docs/api/operations/tags/tokens.md +1 -1
  35. package/dist/docs/api/operations/tokens_create.md +1 -0
  36. package/dist/docs/api/operations/tokens_update.md +2 -1
  37. package/dist/docs/api.md +5 -4
  38. package/dist/docs/develop/build-an-app.md +2 -1
  39. package/dist/docs/develop/change-feed-and-webhooks.md +1 -1
  40. package/dist/docs/develop/http-api.md +1 -1
  41. package/dist/docs/guides/data-quality/check-constraints.md +10 -10
  42. package/dist/docs/guides/data-quality/hand-edits.md +8 -8
  43. package/dist/docs/guides/database-objects/bases.md +11 -11
  44. package/dist/docs/guides/database-objects/catalogs-and-schemas.md +10 -10
  45. package/dist/docs/guides/database-objects/columns.md +241 -38
  46. package/dist/docs/guides/database-objects/relations.md +28 -28
  47. package/dist/docs/guides/database-objects/tables.md +26 -26
  48. package/dist/docs/guides/database-objects/views.md +19 -19
  49. package/dist/docs/guides/history/change-data-feed.md +34 -34
  50. package/dist/docs/guides/history/journal-and-undo.md +12 -12
  51. package/dist/docs/guides/history/restore.md +20 -20
  52. package/dist/docs/guides/history/table-history.md +31 -31
  53. package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
  54. package/dist/docs/guides/read-and-write/import-notes.md +20 -20
  55. package/dist/docs/guides/read-and-write/write-rows.md +10 -10
  56. package/dist/docs/guides/search/hybrid.md +162 -0
  57. package/dist/docs/guides/search/text.md +1 -1
  58. package/dist/docs/guides/security/security-model.md +5 -3
  59. package/dist/docs/guides/security/tokens.md +121 -8
  60. package/dist/docs/reference/cli.md +1 -1
  61. package/dist/docs/reference/glossary.md +1 -1
  62. package/dist/docs/reference/operations/auxiliary.md +2 -2
  63. package/dist/docs/reference/operations/queries.md +16 -0
  64. package/dist/docs/reference/operations/schema.md +2 -2
  65. package/dist/docs/reference/operations.md +2 -1
  66. package/dist/docs/release-notes.md +43 -0
  67. package/dist/docs/start/connect-agents.md +10 -3
  68. package/package.json +1 -1
@@ -264,7 +264,7 @@ var init_catalog = __esm({
264
264
  },
265
265
  {
266
266
  "name": "list_tables",
267
- "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description`, `aliases` (other words for what it records) and `tags`. Tables are\ndefined in `metastore.yaml` (catalog \u2192 schema \u2192 table): read it freely, but it is read-only for\nagents and changes only through the schema operations (create_table, add_column, \u2026). Returns `{tables, views}`: `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
267
+ "description": "List every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it\nwherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and,\nwhen declared, `description`, `aliases` (other words for what it records) and `tags`. Tables are\ndefined in `metastore.yaml` (catalog \u2192 schema \u2192 table): read it freely, but it is read-only for\nagents and changes only through the schema operations (create_table, add_column, \u2026). Returns `{tables, views}`: `views` lists the stored views (same fields, no\npath): select them like tables. `system.information_schema` is read with `select_rows`.\nA token with `read` patterns sees only the tables and views it may read.\n\nCall this first to find the table for what the user reports (match their words against\nname, description and aliases), and before creating tables or columns so you reuse existing\nones instead of creating duplicates.",
268
268
  "inputSchema": {
269
269
  "additionalProperties": false,
270
270
  "properties": {},
@@ -529,7 +529,7 @@ var init_catalog = __esm({
529
529
  },
530
530
  "generated": {
531
531
  "type": "string",
532
- "description": "Expression of a generated column."
532
+ "description": 'Expression of a generated column: a column, `date(col)\xB1N`, a template like `{name} {surname}`, `link(<template>)`, or an aggregate of a json list of objects like `count(feeds, side = "left")` or `sum(feeds.min)`.'
533
533
  },
534
534
  "dense": {
535
535
  "type": "boolean",
@@ -650,7 +650,7 @@ var init_catalog = __esm({
650
650
  },
651
651
  {
652
652
  "name": "create_table",
653
- "description": 'Create a table: `name`, in `catalog` and `schema` (default: the database\'s defaults); the definition\ngoes in metastore.yaml, the rows in `<root>/<catalog>/<schema>/<name>/`. Names: lowercase letters,\ndigits, `_` (no dots: pass the catalog and the schema as their own arguments).\n- `description` (required): what a row of the table records. Every column needs its own `description`\n too, what it holds: the table is refused otherwise, with every missing one listed. Descriptions are how\n agents and people understand the schema, so write them for a reader who never saw the notes. The\n engine writes those of `id` and of `identity` "uuid" / "created" columns.\n- `columns`: specs like `{"name": "kind", "type": "enum", "values": ["a","b"], "required": true,\n "description": "What kind of entry the row is"}`.\n Types: string, integer, number, boolean, date, datetime, enum, link, list (+ `items`), json.\n Attributes: required, default (`"{now}"`/`"{today}"`), min, max, step, values, `ordered` (enum in\n order: filters/avg use the position), ref (link target, full `catalog.schema.table` or "self"),\n unique, label, description, aliases, `generated` (e.g. `"date(occurred_at)"`, `"{name} {surname}"`),\n `identity` ("filename" | "created"), `vocabulary` (allowed values = names of another table\'s rows),\n `hierarchy: true` (one parent link to its own table), `acyclic: true` (links to rows of its own table that never loop back, such as tasks blocked by tasks; with `inverse`, the rule holds on both sides), `dense` (written on every row),\n `inverse` on a link column: the column of the `ref` table that links back (`friends` \u2194\n `friends`, `children` \u2194 `parents`, `partner` \u2194 `partner`); writes keep both sides in step.\n- `time_column`: rows named by that date/datetime; otherwise `filename` (e.g. `"{name}"`).\n- The table\'s options, as alter_table names them: `aliases` (how agents find it, with the\n description), `partition`, `exclude`, `archive`, `history` (keep its versions; default: the\n database\'s setting), `autolink_target` (true / false: may automatic links link its rows? default:\n yes, unless its rows are named by a date or a time, as with `time_column`).\n- `row_template`: markdown body of new rows. Shared columns come from bases: `set_table_bases`.\n- `instructions`: how to fill a row, for agents (Markdown); describe_table returns them.\n- `tags`: key/value labels of the table, strings (`{"domain": "health"}`); list_tables and\n describe_table return them, alter_table changes them.\nEnum columns: `values` and, for each, its meaning in `value_descriptions` (`{"stated": "the user said it", \u2026}`): describe_table shows them and a refused value lists them.\nGenerated link columns: `generated: "link({occurred_at:YYYY-MM-DD})"` with `ref` = the days table links each row to the row of `ref` named by the template (its folder read from the schema; the target may not exist yet).\nFiltered link targets (link columns): `ref_where` (a `where` every target must match) and `ref_by` (a column of the row picks the target level: `{"column": "kind", "levels": ["area", "project", "workstream"]}` or `{"column": "kind", "map": {"area": null, "project": ["area"]}}`; null = top level, no link; `target_column` when the target names it differently: an enum target column must have every value the map allows, a string one takes any value). A link to no row of the table, or to the row itself, is refused too. Existing rows that break them refuse the change; `list_link_targets` lists the valid targets of a row.\n\nThe schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).',
653
+ "description": 'Create a table: `name`, in `catalog` and `schema` (default: the database\'s defaults); the definition\ngoes in metastore.yaml, the rows in `<root>/<catalog>/<schema>/<name>/`. Names: lowercase letters,\ndigits, `_` (no dots: pass the catalog and the schema as their own arguments).\n- `description` (required): what a row of the table records. Every column needs its own `description`\n too, what it holds: the table is refused otherwise, with every missing one listed. Descriptions are how\n agents and people understand the schema, so write them for a reader who never saw the notes. The\n engine writes those of `id` and of `identity` "uuid" / "created" columns.\n- `columns`: specs like `{"name": "kind", "type": "enum", "values": ["a","b"], "required": true,\n "description": "What kind of entry the row is"}`.\n Types: string, integer, number, boolean, date, datetime, enum, link, list (+ `items`), json.\n Attributes: required, default (`"{now}"`/`"{today}"`), min, max, step, values, `ordered` (enum in\n order: filters/avg use the position), ref (link target, full `catalog.schema.table` or "self"),\n unique, label, description, aliases, `generated` (e.g. `"date(occurred_at)"`, `"{name} {surname}"`),\n `identity` ("filename" | "created"), `vocabulary` (allowed values = names of another table\'s rows),\n `hierarchy: true` (one parent link to its own table), `acyclic: true` (links to rows of its own table that never loop back, such as tasks blocked by tasks; with `inverse`, the rule holds on both sides), `dense` (written on every row),\n `inverse` on a link column: the column of the `ref` table that links back (`friends` \u2194\n `friends`, `children` \u2194 `parents`, `partner` \u2194 `partner`); writes keep both sides in step.\n- `time_column`: rows named by that date/datetime; otherwise `filename` (e.g. `"{name}"`).\n- The table\'s options, as alter_table names them: `aliases` (how agents find it, with the\n description), `partition`, `exclude`, `archive`, `history` (keep its versions; default: the\n database\'s setting), `autolink_target` (true / false: may automatic links link its rows? default:\n yes, unless its rows are named by a date or a time, as with `time_column`).\n- `row_template`: markdown body of new rows. Shared columns come from bases: `set_table_bases`.\n- `instructions`: how to fill a row, for agents (Markdown); describe_table returns them.\n- `tags`: key/value labels of the table, strings (`{"domain": "health"}`); list_tables and\n describe_table return them, alter_table changes them.\nEnum columns: `values` and, for each, its meaning in `value_descriptions` (`{"stated": "the user said it", \u2026}`): describe_table shows them and a refused value lists them.\nGenerated link columns: `generated: "link({occurred_at:YYYY-MM-DD})"` with `ref` = the days table links each row to the row of `ref` named by the template (its folder read from the schema; the target may not exist yet).\nGenerated aggregates of a json column holding a list of objects (integer or number columns; avg: number): `count(feeds)`, `count(feeds.min)` (the items with a min), `sum(feeds.min)`, `avg(\u2026)`, `min(\u2026)`, `max(\u2026)`, with an optional filter on one field: `count(feeds, side = "left")`, `sum(feeds.min, min > 0)` (=, !=, <, <=, >, >=; "text", a number, true or false). Recomputed on every write; no list: count and sum 0, avg/min/max empty; a field that is not a number: empty.\nFiltered link targets (link columns): `ref_where` (a `where` every target must match) and `ref_by` (a column of the row picks the target level: `{"column": "kind", "levels": ["area", "project", "workstream"]}` or `{"column": "kind", "map": {"area": null, "project": ["area"]}}`; null = top level, no link; `target_column` when the target names it differently: an enum target column must have every value the map allows, a string one takes any value). A link to no row of the table, or to the row itself, is refused too. Existing rows that break them refuse the change; `list_link_targets` lists the valid targets of a row.\n\nThe schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).',
654
654
  "inputSchema": {
655
655
  "additionalProperties": false,
656
656
  "properties": {
@@ -1744,7 +1744,7 @@ var init_catalog = __esm({
1744
1744
  },
1745
1745
  {
1746
1746
  "name": "add_column",
1747
- "description": 'Add a typed column. Sparse by default (written only on rows that have a value \u2014 use for\nmetrics, e.g. `{"name": "metric_mood", "type": "integer", "min": 1, "max": 5, "label":\n"Mood", "aliases": ["mood"], "description": "Mood of the day, 1 = bad, 5 = great"}`).\n`description` is required: what the column holds, for the agents and people who read the schema\n(the engine writes it for `identity` "uuid" / "created" columns).\n`dense: true` in the spec adds it to every row with its `default`. Call `describe_table` first: never add\na column whose meaning an existing column (name/label/aliases/description) already covers.\nColumn names are permanent identifiers; only `rename_column` changes them.\nEnum columns: `values` and, for each, its meaning in `value_descriptions` (`{"stated": "the user said it", \u2026}`): describe_table shows them and a refused value lists them.\nGenerated link columns: `generated: "link({occurred_at:YYYY-MM-DD})"` with `ref` = the days table links each row to the row of `ref` named by the template (its folder read from the schema; the target may not exist yet).\nFiltered link targets (link columns): `ref_where` (a `where` every target must match) and `ref_by` (a column of the row picks the target level: `{"column": "kind", "levels": ["area", "project", "workstream"]}` or `{"column": "kind", "map": {"area": null, "project": ["area"]}}`; null = top level, no link; `target_column` when the target names it differently: an enum target column must have every value the map allows, a string one takes any value). A link to no row of the table, or to the row itself, is refused too. Existing rows that break them refuse the change; `list_link_targets` lists the valid targets of a row.\nAcyclic links: `acyclic: true` on a link (or list of links) to the table itself refuses every write that closes a cycle, on either side of an `inverse`; refused while existing rows form one (they are listed).\n\nThe schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).',
1747
+ "description": 'Add a typed column. Sparse by default (written only on rows that have a value \u2014 use for\nmetrics, e.g. `{"name": "metric_mood", "type": "integer", "min": 1, "max": 5, "label":\n"Mood", "aliases": ["mood"], "description": "Mood of the day, 1 = bad, 5 = great"}`).\n`description` is required: what the column holds, for the agents and people who read the schema\n(the engine writes it for `identity` "uuid" / "created" columns).\n`dense: true` in the spec adds it to every row with its `default`. Call `describe_table` first: never add\na column whose meaning an existing column (name/label/aliases/description) already covers.\nColumn names are permanent identifiers; only `rename_column` changes them.\nEnum columns: `values` and, for each, its meaning in `value_descriptions` (`{"stated": "the user said it", \u2026}`): describe_table shows them and a refused value lists them.\nGenerated link columns: `generated: "link({occurred_at:YYYY-MM-DD})"` with `ref` = the days table links each row to the row of `ref` named by the template (its folder read from the schema; the target may not exist yet).\nGenerated aggregates of a json column holding a list of objects (integer or number columns; avg: number): `count(feeds)`, `count(feeds.min)` (the items with a min), `sum(feeds.min)`, `avg(\u2026)`, `min(\u2026)`, `max(\u2026)`, with an optional filter on one field: `count(feeds, side = "left")`, `sum(feeds.min, min > 0)` (=, !=, <, <=, >, >=; "text", a number, true or false). Recomputed on every write; no list: count and sum 0, avg/min/max empty; a field that is not a number: empty.\nFiltered link targets (link columns): `ref_where` (a `where` every target must match) and `ref_by` (a column of the row picks the target level: `{"column": "kind", "levels": ["area", "project", "workstream"]}` or `{"column": "kind", "map": {"area": null, "project": ["area"]}}`; null = top level, no link; `target_column` when the target names it differently: an enum target column must have every value the map allows, a string one takes any value). A link to no row of the table, or to the row itself, is refused too. Existing rows that break them refuse the change; `list_link_targets` lists the valid targets of a row.\nAcyclic links: `acyclic: true` on a link (or list of links) to the table itself refuses every write that closes a cycle, on either side of an `inverse`; refused while existing rows form one (they are listed).\n\nThe schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).',
1748
1748
  "inputSchema": {
1749
1749
  "additionalProperties": false,
1750
1750
  "properties": {
@@ -5143,7 +5143,7 @@ var init_catalog = __esm({
5143
5143
  },
5144
5144
  {
5145
5145
  "name": "describe_database",
5146
- "description": "Describe the database: its folder (`root`), `metastore` (`metastore.yaml`: read it freely,\nbut it is read-only for agents and changes only through the schema operations), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone,\nhistory, automatic links),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 `schemas` \u2192 `tables` (with `row_count`, `tags`, and `unreadable_count` when some rows' front matter cannot be read) and `views`;\ncatalogs and schemas show their `comment` (set with `comment_on`). A table or base defined before\ndescriptions were required has `missing_descriptions`: what lacks one, for alter_table / alter_column\n(define_base for a base) to fill, so every table and column says what it holds.",
5146
+ "description": "Describe the database: its folder (`root`), `metastore` (`metastore.yaml`: read it freely,\nbut it is read-only for agents and changes only through the schema operations), configuration\n(default catalog and schema, row partition and file-name formats, archive folder, time zone,\nhistory, automatic links),\njournal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 `schemas` \u2192 `tables` (with `row_count`, `tags`, and `unreadable_count` when some rows' front matter cannot be read) and `views`;\ncatalogs and schemas show their `comment` (set with `comment_on`). A table or base defined before\ndescriptions were required has `missing_descriptions`: what lacks one, for alter_table / alter_column\n(define_base for a base) to fill, so every table and column says what it holds.\nA token with `read` patterns sees only the tables, views and bases it may read.",
5147
5147
  "inputSchema": {
5148
5148
  "additionalProperties": false,
5149
5149
  "properties": {},
@@ -11707,6 +11707,141 @@ var init_catalog = __esm({
11707
11707
  ]
11708
11708
  }
11709
11709
  },
11710
+ {
11711
+ "name": "search",
11712
+ "description": "Search the notes by their words and, when semantic search is on, by their meaning:\n`{query, results: [{path, table?, score, heading, snippet, matched_by}], meaning}`, one result per\nnote, best first. It always works: with semantic search off it ranks by words alone (BM25:\nrare words count more; a word in the note's name or properties counts more), with it on it fuses\nboth rankings (Reciprocal Rank Fusion), so a note found only by its words or only by its meaning\nis still there. `matched_by` says which (`words`, `meaning`); `meaning` is `on`, `off` or `failed`\n(the embedding provider failed: words only, `meaning_error` says why). Query syntax as search_docs:\n`\"a phrase\"` and `+word` must appear, `-word` must not (both rankings); words in any language,\ncase and accents ignored, no stemming. Filters: `tables` (qualified or short names), `folder`,\n`min_score` (the meaning ranking's cosine floor). `limit`: default 10, max 100. Then read_note /\nselect_rows for the details.",
11713
+ "inputSchema": {
11714
+ "additionalProperties": false,
11715
+ "properties": {
11716
+ "query": {
11717
+ "type": "string"
11718
+ },
11719
+ "limit": {
11720
+ "default": 10,
11721
+ "type": "integer"
11722
+ },
11723
+ "tables": {
11724
+ "anyOf": [
11725
+ {
11726
+ "items": {
11727
+ "type": "string"
11728
+ },
11729
+ "type": "array"
11730
+ },
11731
+ {
11732
+ "type": "null"
11733
+ }
11734
+ ],
11735
+ "default": null
11736
+ },
11737
+ "folder": {
11738
+ "anyOf": [
11739
+ {
11740
+ "type": "string"
11741
+ },
11742
+ {
11743
+ "type": "null"
11744
+ }
11745
+ ],
11746
+ "default": null
11747
+ },
11748
+ "min_score": {
11749
+ "anyOf": [
11750
+ {
11751
+ "type": "number"
11752
+ },
11753
+ {
11754
+ "type": "null"
11755
+ }
11756
+ ],
11757
+ "default": null
11758
+ }
11759
+ },
11760
+ "required": [
11761
+ "query"
11762
+ ],
11763
+ "type": "object"
11764
+ },
11765
+ "annotations": {
11766
+ "title": "Search",
11767
+ "readOnlyHint": true,
11768
+ "destructiveHint": false,
11769
+ "idempotentHint": true,
11770
+ "openWorldHint": false
11771
+ },
11772
+ "responseSchema": {
11773
+ "type": "object",
11774
+ "properties": {
11775
+ "query": {
11776
+ "type": "string"
11777
+ },
11778
+ "results": {
11779
+ "type": "array",
11780
+ "items": {
11781
+ "type": "object",
11782
+ "properties": {
11783
+ "path": {
11784
+ "type": "string"
11785
+ },
11786
+ "table": {
11787
+ "type": "string",
11788
+ "description": "Qualified table of a row note."
11789
+ },
11790
+ "score": {
11791
+ "type": "number",
11792
+ "description": "BM25 when ranked by words alone; the Reciprocal Rank Fusion score when fused."
11793
+ },
11794
+ "heading": {
11795
+ "type": "string",
11796
+ "description": "Heading above the match (empty before the first heading)."
11797
+ },
11798
+ "snippet": {
11799
+ "type": "string",
11800
+ "description": "About 200 characters around the first match, or the best passage by meaning."
11801
+ },
11802
+ "matched_by": {
11803
+ "type": "array",
11804
+ "items": {
11805
+ "type": "string",
11806
+ "enum": [
11807
+ "words",
11808
+ "meaning"
11809
+ ]
11810
+ },
11811
+ "description": "The rankings the note is in."
11812
+ }
11813
+ },
11814
+ "required": [
11815
+ "path",
11816
+ "score",
11817
+ "heading",
11818
+ "snippet",
11819
+ "matched_by"
11820
+ ]
11821
+ },
11822
+ "description": "One result per note, best first."
11823
+ },
11824
+ "meaning": {
11825
+ "type": "string",
11826
+ "enum": [
11827
+ "on",
11828
+ "off",
11829
+ "failed"
11830
+ ],
11831
+ "description": "on: words and meaning fused; off: semantic search is off, words only; failed: the embedding provider failed, words only."
11832
+ },
11833
+ "meaning_error": {
11834
+ "type": "string",
11835
+ "description": "Why the meaning ranking failed (with meaning: failed)."
11836
+ }
11837
+ },
11838
+ "required": [
11839
+ "query",
11840
+ "results",
11841
+ "meaning"
11842
+ ]
11843
+ }
11844
+ },
11710
11845
  {
11711
11846
  "name": "describe_semantic_index",
11712
11847
  "description": "The semantic search index: `enabled`, `provider`, `model`, `files`, `passages`, `pending`,\n`running`, `last_sync`, `last_error`. sync_semantic_index brings it up to date.",
@@ -17839,6 +17974,7 @@ var init_catalog = __esm({
17839
17974
  list_folder: "queries",
17840
17975
  search_notes: "queries",
17841
17976
  semantic_search: "queries",
17977
+ search: "queries",
17842
17978
  graph_neighbors: "queries",
17843
17979
  graph_traverse: "queries",
17844
17980
  graph_path: "queries",
@@ -18558,14 +18694,14 @@ var require_identity = __commonJS({
18558
18694
  var ALIAS = Symbol.for("yaml.alias");
18559
18695
  var DOC = Symbol.for("yaml.document");
18560
18696
  var MAP = Symbol.for("yaml.map");
18561
- var PAIR = Symbol.for("yaml.pair");
18697
+ var PAIR2 = Symbol.for("yaml.pair");
18562
18698
  var SCALAR = Symbol.for("yaml.scalar");
18563
18699
  var SEQ = Symbol.for("yaml.seq");
18564
18700
  var NODE_TYPE = Symbol.for("yaml.node.type");
18565
18701
  var isAlias = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === ALIAS;
18566
18702
  var isDocument = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === DOC;
18567
18703
  var isMap2 = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === MAP;
18568
- var isPair = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === PAIR;
18704
+ var isPair = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === PAIR2;
18569
18705
  var isScalar2 = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SCALAR;
18570
18706
  var isSeq = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SEQ;
18571
18707
  function isCollection(node2) {
@@ -18593,7 +18729,7 @@ var require_identity = __commonJS({
18593
18729
  exports2.DOC = DOC;
18594
18730
  exports2.MAP = MAP;
18595
18731
  exports2.NODE_TYPE = NODE_TYPE;
18596
- exports2.PAIR = PAIR;
18732
+ exports2.PAIR = PAIR2;
18597
18733
  exports2.SCALAR = SCALAR;
18598
18734
  exports2.SEQ = SEQ;
18599
18735
  exports2.hasAnchor = hasAnchor;
@@ -24080,8 +24216,8 @@ var require_cst_visit = __commonJS({
24080
24216
  visit.REMOVE = REMOVE;
24081
24217
  visit.itemAtPath = (cst, path23) => {
24082
24218
  let item = cst;
24083
- for (const [field2, index] of path23) {
24084
- const tok = item?.[field2];
24219
+ for (const [field3, index] of path23) {
24220
+ const tok = item?.[field3];
24085
24221
  if (tok && "items" in tok) {
24086
24222
  item = tok.items[index];
24087
24223
  } else
@@ -24091,8 +24227,8 @@ var require_cst_visit = __commonJS({
24091
24227
  };
24092
24228
  visit.parentCollection = (cst, path23) => {
24093
24229
  const parent2 = visit.itemAtPath(cst, path23.slice(0, -1));
24094
- const field2 = path23[path23.length - 1][0];
24095
- const coll = parent2?.[field2];
24230
+ const field3 = path23[path23.length - 1][0];
24231
+ const coll = parent2?.[field3];
24096
24232
  if (coll && "items" in coll)
24097
24233
  return coll;
24098
24234
  throw new Error("Parent collection not found");
@@ -24101,11 +24237,11 @@ var require_cst_visit = __commonJS({
24101
24237
  let ctrl = visitor(item, path23);
24102
24238
  if (typeof ctrl === "symbol")
24103
24239
  return ctrl;
24104
- for (const field2 of ["key", "value"]) {
24105
- const token = item[field2];
24240
+ for (const field3 of ["key", "value"]) {
24241
+ const token = item[field3];
24106
24242
  if (token && "items" in token) {
24107
24243
  for (let i = 0; i < token.items.length; ++i) {
24108
- const ci = _visit(Object.freeze(path23.concat([[field2, i]])), token.items[i], visitor);
24244
+ const ci = _visit(Object.freeze(path23.concat([[field3, i]])), token.items[i], visitor);
24109
24245
  if (typeof ci === "number")
24110
24246
  i = ci - 1;
24111
24247
  else if (ci === BREAK)
@@ -24115,7 +24251,7 @@ var require_cst_visit = __commonJS({
24115
24251
  i -= 1;
24116
24252
  }
24117
24253
  }
24118
- if (typeof ctrl === "function" && field2 === "key")
24254
+ if (typeof ctrl === "function" && field3 === "key")
24119
24255
  ctrl = ctrl(item, path23);
24120
24256
  }
24121
24257
  }
@@ -26239,6 +26375,7 @@ var init_hints = __esm({
26239
26375
  renameColumn: "/guides/database-objects/columns.md#rename-a-column",
26240
26376
  generated: "/guides/data-quality/hand-edits.md#recompute-generated-columns-after-a-hand-edit",
26241
26377
  computed: "/guides/database-objects/columns.md#compute-a-column-from-other-columns",
26378
+ jsonAggregates: "/guides/database-objects/columns.md#count-and-total-the-items-of-a-json-list",
26242
26379
  partition: "/guides/database-objects/columns.md#partition-rows-into-folders",
26243
26380
  filters: "/guides/read-and-write/read-and-filter.md#compare-values-with-operators",
26244
26381
  logical: "/guides/read-and-write/read-and-filter.md#combine-filters-with-or-and-and-not",
@@ -26297,10 +26434,12 @@ var init_hints = __esm({
26297
26434
  graph: "/guides/search/link-graph.md#walk-the-links-from-a-note",
26298
26435
  graphTables: "/guides/search/link-graph.md#read-the-whole-graph-in-one-call",
26299
26436
  semantic: "/guides/search/semantic.md#turn-on-semantic-search",
26437
+ search: "/guides/search/hybrid.md#search-by-words-and-meaning",
26300
26438
  docsSearch: "/guides/agents/docs-offline.md#search-by-words-and-by-meaning",
26301
26439
  tokens: "/guides/security/tokens.md#choose-what-a-token-can-do",
26302
26440
  manageTokens: "/guides/security/tokens.md#manage-your-tokens",
26303
26441
  tokenAutolink: "/guides/security/tokens.md#turn-automatic-links-off-for-a-sync-program",
26442
+ tokenRead: "/guides/security/tokens.md#limit-what-a-token-can-read",
26304
26443
  reportAgents: "/guides/security/who-did-what.md#report-what-an-agent-does",
26305
26444
  auth: "/develop/http-api.md#authenticate-your-requests",
26306
26445
  routes: "/develop/http-api.md#call-the-engines-routes",
@@ -26703,6 +26842,64 @@ var init_datetime = __esm({
26703
26842
  });
26704
26843
 
26705
26844
  // src/core/generated.ts
26845
+ function filterValue(text5) {
26846
+ if (text5 === "true" || text5 === "false") return text5 === "true";
26847
+ if (NUMBER_LITERAL.test(text5)) return Number(text5);
26848
+ if (/^'[^']*'$/.test(text5)) return text5.slice(1, -1);
26849
+ if (/^"[\s\S]*"$/.test(text5)) {
26850
+ try {
26851
+ return JSON.parse(text5);
26852
+ } catch {
26853
+ return void 0;
26854
+ }
26855
+ }
26856
+ return void 0;
26857
+ }
26858
+ function parseAggregate(expr) {
26859
+ const e = expr.trim();
26860
+ if (!AGGREGATE_START.test(e)) return null;
26861
+ const fail2 = (message) => new GeneratedExpressionError(message, AGGREGATE_GUIDANCE);
26862
+ const m = e.match(AGGREGATE_EXPR);
26863
+ if (!m) throw fail2(`"${expr}" is not an aggregate: write count(column), count(column, field = value) or sum|avg|min|max(column.field), with an optional filter`);
26864
+ const fn = m[1];
26865
+ const column = m[2];
26866
+ if (fn !== "count" && m[3] === void 0) throw fail2(`"${expr}": ${fn}() needs a field of the items, like ${fn}(${column}.minutes)`);
26867
+ const out2 = { fn, column, ...m[3] !== void 0 ? { field: m[3] } : {} };
26868
+ if (m[4] === void 0) return out2;
26869
+ const f = m[4].match(FILTER_EXPR);
26870
+ if (!f) throw fail2(`"${expr}": the filter "${m[4]}" must compare a field with a value, like side = "left"`);
26871
+ const op2 = f[2];
26872
+ const raw = f[3].trim();
26873
+ const value = filterValue(raw);
26874
+ if (value === void 0) throw fail2(`"${expr}": the filter value ${raw} is not "text", a number, true or false`);
26875
+ if (op2 !== "=" && op2 !== "!=" && typeof value !== "number") throw fail2(`"${expr}": ${op2} compares numbers: the filter value must be a number, like min ${op2} 5`);
26876
+ return { ...out2, filter: { field: f[1], op: op2, value } };
26877
+ }
26878
+ function passes(item, filter) {
26879
+ const v = fieldOf(item, filter.field);
26880
+ if (filter.op === "=") return v === filter.value;
26881
+ if (filter.op === "!=") return v !== filter.value;
26882
+ if (typeof v !== "number" || !Number.isFinite(v)) return false;
26883
+ const n = filter.value;
26884
+ if (filter.op === "<") return v < n;
26885
+ if (filter.op === "<=") return v <= n;
26886
+ if (filter.op === ">") return v > n;
26887
+ return v >= n;
26888
+ }
26889
+ function evalAggregate(a, value) {
26890
+ const list4 = value === void 0 || value === null ? [] : value;
26891
+ if (!Array.isArray(list4)) return null;
26892
+ const items3 = a.filter ? list4.filter((x) => passes(x, a.filter)) : list4;
26893
+ if (a.field === void 0) return items3.length;
26894
+ const values = items3.map((x) => fieldOf(x, a.field)).filter((v) => v !== void 0 && v !== null);
26895
+ if (a.fn === "count") return values.length;
26896
+ if (values.some((v) => typeof v !== "number" || !Number.isFinite(v))) return null;
26897
+ const nums = values;
26898
+ if (a.fn === "sum") return tidy(nums.reduce((s, n) => s + n, 0));
26899
+ if (nums.length === 0) return null;
26900
+ if (a.fn === "avg") return tidy(nums.reduce((s, n) => s + n, 0) / nums.length);
26901
+ return a.fn === "min" ? Math.min(...nums) : Math.max(...nums);
26902
+ }
26706
26903
  function placeholderCols(m) {
26707
26904
  return m[2] ? m[2].split(",").map((c) => c.trim()) : [m[3]];
26708
26905
  }
@@ -26714,6 +26911,8 @@ function isTemplateExpression(expr) {
26714
26911
  }
26715
26912
  function generatedRefs(expr) {
26716
26913
  const e = expr.trim();
26914
+ const agg = parseAggregate(expr);
26915
+ if (agg) return [agg.column];
26717
26916
  const lm = e.match(LINK_EXPR);
26718
26917
  if (lm) {
26719
26918
  if (!lm[1].includes("{")) throw new GeneratedExpressionError(`"${expr}": link() needs a name template with at least one {column}`);
@@ -26755,6 +26954,8 @@ function display(value) {
26755
26954
  function evalGenerated(expr, get, path23, linkFolder) {
26756
26955
  const e = expr.trim();
26757
26956
  const read3 = (col) => col === "path" ? path23 : get(col);
26957
+ const agg = parseAggregate(e);
26958
+ if (agg) return evalAggregate(agg, read3(agg.column));
26758
26959
  const lm = e.match(LINK_EXPR);
26759
26960
  if (lm) {
26760
26961
  const tpl = lm[1];
@@ -26766,8 +26967,8 @@ function evalGenerated(expr, get, path23, linkFolder) {
26766
26967
  }
26767
26968
  if (isTemplateExpression(e)) {
26768
26969
  const out2 = e.replace(PLACEHOLDER_RE, (...args) => placeholderText(args, read3));
26769
- const tidy = out2.replace(/\s+/g, " ").replace(/\(\s*\)|\[\s*\]/g, "").replace(/\s+/g, " ").trim();
26770
- return tidy === "" ? null : tidy;
26970
+ const tidy2 = out2.replace(/\s+/g, " ").replace(/\(\s*\)|\[\s*\]/g, "").replace(/\s+/g, " ").trim();
26971
+ return tidy2 === "" ? null : tidy2;
26771
26972
  }
26772
26973
  const m = e.match(DATE_EXPR);
26773
26974
  if (m) {
@@ -26785,18 +26986,30 @@ function placeholderText(m, read3) {
26785
26986
  if (m[1] === "max") return [...texts].sort().pop();
26786
26987
  return texts[0];
26787
26988
  }
26788
- var GeneratedExpressionError, PLACEHOLDER_RE, LINK_EXPR, DATE_EXPR, COLUMN_EXPR;
26989
+ var GeneratedExpressionError, AGGREGATE_GUIDANCE, AGGREGATE_START, AGGREGATE_EXPR, FILTER_EXPR, NUMBER_LITERAL, isObject, fieldOf, tidy, PLACEHOLDER_RE, LINK_EXPR, DATE_EXPR, COLUMN_EXPR;
26789
26990
  var init_generated = __esm({
26790
26991
  "src/core/generated.ts"() {
26791
26992
  "use strict";
26792
26993
  init_validate();
26793
26994
  init_datetime();
26794
- GeneratedExpressionError = class extends Error {
26995
+ init_hints();
26996
+ GeneratedExpressionError = class extends GuidedError {
26795
26997
  constructor() {
26796
26998
  super(...arguments);
26797
26999
  this.name = "GeneratedExpressionError";
26798
27000
  }
26799
27001
  };
27002
+ AGGREGATE_GUIDANCE = guide(
27003
+ `An aggregate reads a json column holding a list of objects: count(feeds), count(feeds, side = "left"), count(feeds.min) (the items with a min), sum(feeds.min), avg|min|max(feeds.min, side != "right"). A filter compares one field of the item with "text", a number, true or false, using =, !=, <, <=, > or >=.`,
27004
+ "jsonAggregates"
27005
+ );
27006
+ AGGREGATE_START = /^(count|sum|avg|min|max)\s*\(/;
27007
+ AGGREGATE_EXPR = /^(count|sum|avg|min|max)\s*\(\s*([A-Za-z][A-Za-z0-9_-]*)(?:\.([A-Za-z_][A-Za-z0-9_-]*))?\s*(?:,\s*([\s\S]*?))?\s*\)$/;
27008
+ FILTER_EXPR = /^([A-Za-z_][A-Za-z0-9_-]*)\s*(!=|<=|>=|=|<|>)\s*([\s\S]*)$/;
27009
+ NUMBER_LITERAL = /^-?\d+(?:\.\d+)?$/;
27010
+ isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
27011
+ fieldOf = (item, field3) => isObject(item) ? item[field3] : void 0;
27012
+ tidy = (n) => Number(n.toPrecision(15));
26800
27013
  PLACEHOLDER_RE = /\{(?:(min|max|coalesce)\(\s*([A-Za-z][A-Za-z0-9_-]*(?:\s*,\s*[A-Za-z][A-Za-z0-9_-]*)*)\s*\)|([A-Za-z][A-Za-z0-9_-]*))(?::([^}]+))?\}/g;
26801
27014
  LINK_EXPR = /^link\(\s*([\s\S]+?)\s*\)$/;
26802
27015
  DATE_EXPR = /^date\(\s*([A-Za-z][A-Za-z0-9_-]*)\s*\)\s*(?:([+-])\s*(\d+))?$/;
@@ -27147,7 +27360,7 @@ function checkGeneratedColumns(specs, known) {
27147
27360
  try {
27148
27361
  refs = generatedRefs(c.generated);
27149
27362
  } catch (e) {
27150
- throw new MalformedColumnError(index, messageOf(e));
27363
+ throw new MalformedColumnError(index, messageOf(e), guidanceOf(e) ?? void 0);
27151
27364
  }
27152
27365
  if (isTemplateExpression(c.generated) && c.type !== "string") {
27153
27366
  throw new MalformedColumnError(index, `a template expression makes text: "${c.name}" must be a string column`);
@@ -27164,8 +27377,37 @@ function checkGeneratedColumns(specs, known) {
27164
27377
  throw new MalformedColumnError(index, `"generated" reads generated column "${ref2}", declared after it`);
27165
27378
  }
27166
27379
  }
27380
+ const agg = parseAggregate(c.generated);
27381
+ if (agg) checkAggregate(c, index, agg, specs);
27167
27382
  });
27168
27383
  }
27384
+ function checkAggregate(c, index, agg, specs) {
27385
+ const source = specs.find((s) => s.name === agg.column);
27386
+ if (source?.type !== "json" && !(source?.type === "list" && source.items === "json")) {
27387
+ const what = !source ? `"${agg.column}" is ${agg.column === "path" ? "the row's path" : "an untyped field"}` : source.type === "list" ? (
27388
+ // parseColumn gives every list its items ("string" when the spec has none)
27389
+ `"${agg.column}" is a list of ${source.items}`
27390
+ ) : `"${agg.column}" has type ${source.type}`;
27391
+ throw new MalformedColumnError(
27392
+ index,
27393
+ `"generated" aggregates "${agg.column}", which is not a json column`,
27394
+ guide(`${agg.fn}() reads a json column (type json, or a list of json) holding a list of objects, such as [{side: left, min: 12}]; ${what}. describe_table shows the type of each column.`, "jsonAggregates")
27395
+ );
27396
+ }
27397
+ const fits = agg.fn === "avg" ? c.type === "number" : c.type === "number" || c.type === "integer";
27398
+ if (fits) return;
27399
+ if (agg.fn === "count") {
27400
+ throw new MalformedColumnError(index, `count() makes a whole number: "${c.name}" must be an integer or number column`, guide(`Give "${c.name}" type "integer" or "number".`, "jsonAggregates"));
27401
+ }
27402
+ if (agg.fn === "avg") {
27403
+ throw new MalformedColumnError(index, `avg() makes a number: "${c.name}" must be a number column`, guide(`Give "${c.name}" type "number": an average is seldom a whole number.`, "jsonAggregates"));
27404
+ }
27405
+ throw new MalformedColumnError(
27406
+ index,
27407
+ `${agg.fn}() makes a number: "${c.name}" must be a number or integer column`,
27408
+ guide(`Give "${c.name}" type "number", or "integer" when the field holds only whole numbers.`, "jsonAggregates")
27409
+ );
27410
+ }
27169
27411
  function tableDefinitionFromOrdered(ref2, schemaPath, ordered, settings) {
27170
27412
  const reserved = new Set(TABLE_META_KEYS);
27171
27413
  const options = parseTableOptions(fmGet(ordered, TABLE_KEY));
@@ -28817,6 +29059,89 @@ var init_database_move = __esm({
28817
29059
  }
28818
29060
  });
28819
29061
 
29062
+ // src/core/read-scope.ts
29063
+ function runWithReadScope(scope, fn) {
29064
+ if (!storage2) {
29065
+ if (scope) throw new Error("read permissions need Node's AsyncLocalStorage");
29066
+ return fn();
29067
+ }
29068
+ if (!scope && !storage2.getStore()) return fn();
29069
+ return storage2.run({ scope }, fn);
29070
+ }
29071
+ function currentReadScope() {
29072
+ return storage2?.getStore()?.scope ?? null;
29073
+ }
29074
+ function matchesTablePattern(full, patterns) {
29075
+ return patterns.some((p) => {
29076
+ if (p === "*") return true;
29077
+ if (p.endsWith(".*")) return full.startsWith(p.slice(0, -1));
29078
+ return p === full;
29079
+ });
29080
+ }
29081
+ function tableReadable(full, scope = currentReadScope()) {
29082
+ return scope === null || matchesTablePattern(full, scope.patterns);
29083
+ }
29084
+ function tableReadDenied(token, full) {
29085
+ return new ReadDeniedError(`token ${token} may not read ${full}`);
29086
+ }
29087
+ function pathReadDenied(token, path23) {
29088
+ return new ReadDeniedError(`token ${token} may not read the note ${path23}`);
29089
+ }
29090
+ function assertTableReadable(full) {
29091
+ const scope = currentReadScope();
29092
+ if (scope && !matchesTablePattern(full, scope.patterns)) throw tableReadDenied(scope.token, full);
29093
+ }
29094
+ function tableOfPath(app, settings, path23) {
29095
+ const idx = vaultIndex(app, settings);
29096
+ if (idx && !idx.hasFailures) return idx.note(path23)?.table ?? null;
29097
+ const model = cachedMetastore(app, settings);
29098
+ const file = app.vault.getAbstractFileByPath(path23);
29099
+ if (!model || !file || !("extension" in file)) return null;
29100
+ let value;
29101
+ try {
29102
+ value = app.metadataCache.getFileCache(file)?.frontmatter?.[settings.tableKey];
29103
+ } catch {
29104
+ return null;
29105
+ }
29106
+ return rowTableFull(model, value);
29107
+ }
29108
+ function pathReadable(app, settings, path23, scope = currentReadScope()) {
29109
+ if (scope === null) return true;
29110
+ const table = tableOfPath(app, settings, path23);
29111
+ return table !== null && matchesTablePattern(table, scope.patterns);
29112
+ }
29113
+ function assertPathReadable(app, settings, path23) {
29114
+ const scope = currentReadScope();
29115
+ if (scope && !pathReadable(app, settings, path23, scope)) throw pathReadDenied(scope.token, path23);
29116
+ }
29117
+ var storage2, READ_SCOPE_SUPPORTED, READ_DENIED_GUIDANCE, ReadDeniedError;
29118
+ var init_read_scope = __esm({
29119
+ "src/core/read-scope.ts"() {
29120
+ "use strict";
29121
+ init_hints();
29122
+ init_metastore();
29123
+ init_vault_index();
29124
+ storage2 = null;
29125
+ try {
29126
+ const { AsyncLocalStorage: AsyncLocalStorage2 } = require("node:async_hooks");
29127
+ storage2 = new AsyncLocalStorage2();
29128
+ } catch {
29129
+ storage2 = null;
29130
+ }
29131
+ READ_SCOPE_SUPPORTED = storage2 !== null;
29132
+ READ_DENIED_GUIDANCE = guide(
29133
+ `The owner grants reads with tokens/update/ and "read" patterns (catalog.schema.table, catalog.schema.*, catalog.* or *). list_tables lists the tables this token may read.`,
29134
+ "tokenRead"
29135
+ );
29136
+ ReadDeniedError = class extends GuidedError {
29137
+ constructor(message) {
29138
+ super(message, READ_DENIED_GUIDANCE);
29139
+ this.name = "ReadDeniedError";
29140
+ }
29141
+ };
29142
+ }
29143
+ });
29144
+
28820
29145
  // src/core/journal.ts
28821
29146
  function pad(n) {
28822
29147
  return String(n).padStart(2, "0");
@@ -28984,7 +29309,11 @@ async function readJournal(app, settings, migrationId) {
28984
29309
  async function markUndone(app, settings, migrationId, undoneBy, now) {
28985
29310
  await appendLine(app, journalFileFor(settings, migrationId), { type: "undone", migration_id: migrationId, undone_by: undoneBy, at: now.toISOString() });
28986
29311
  }
29312
+ function tableOfEntry(e) {
29313
+ return e.schema?.includes("#") ? e.schema.slice(e.schema.indexOf("#") + 1) : void 0;
29314
+ }
28987
29315
  async function listMigrations(app, settings, input = {}) {
29316
+ const scope = currentReadScope();
28988
29317
  const files = (await storeList(app, journalDir(settings))).files.filter((f) => f.endsWith(".jsonl")).sort().reverse();
28989
29318
  const limit = Math.max(1, Math.min(input.limit ?? 50, 1e3));
28990
29319
  const since = input.since ? Date.parse(input.since) : NaN;
@@ -29000,13 +29329,17 @@ async function listMigrations(app, settings, input = {}) {
29000
29329
  if (input.op && e.op !== input.op) continue;
29001
29330
  if (input.table && !(e.schema ?? "").includes(input.table) && !e.summary.includes(input.table)) continue;
29002
29331
  if (!Number.isNaN(since) && Date.parse(e.executed_at) < since) continue;
29332
+ if (scope) {
29333
+ const table = tableOfEntry(e);
29334
+ if (table === void 0 || !tableReadable(table, scope)) continue;
29335
+ }
29003
29336
  total++;
29004
29337
  if (out2.length >= limit) continue;
29005
29338
  out2.push({
29006
29339
  migration_id: e.migration_id,
29007
29340
  op: e.op,
29008
29341
  summary: e.summary,
29009
- ...e.schema?.includes("#") ? { table: e.schema.slice(e.schema.indexOf("#") + 1) } : {},
29342
+ ...tableOfEntry(e) !== void 0 ? { table: tableOfEntry(e) } : {},
29010
29343
  executed_at: e.executed_at,
29011
29344
  notes_changed: e.rows.length,
29012
29345
  files_changed: e.creates.length + e.moves.length + e.references.length + (e.deleted?.length ?? 0),
@@ -29116,6 +29449,7 @@ var init_journal = __esm({
29116
29449
  init_store();
29117
29450
  init_database_move();
29118
29451
  init_tracing();
29452
+ init_read_scope();
29119
29453
  BLOB_MIN = 2048;
29120
29454
  MigrationNotFoundError = class extends Error {
29121
29455
  constructor(migrationId) {
@@ -31624,10 +31958,146 @@ var init_link_paths = __esm({
31624
31958
  }
31625
31959
  });
31626
31960
 
31961
+ // src/core/query-tables.ts
31962
+ function refOf2(model, name, kind) {
31963
+ try {
31964
+ return resolveCatalogRef(model, name, kind)?.full ?? null;
31965
+ } catch {
31966
+ return null;
31967
+ }
31968
+ }
31969
+ function isSystemTable(name) {
31970
+ const t = name.trim();
31971
+ if (t.startsWith(`${SYSTEM_CATALOG}.`)) return true;
31972
+ const parts = t.split(".");
31973
+ return parts.length === 2 && parts[0] === "information_schema";
31974
+ }
31975
+ function queryTables(q, model, out2, depth) {
31976
+ const text5 = JSON.stringify(q);
31977
+ const r = { vault: false, today: text5.includes("{today"), now: text5.includes("{now") };
31978
+ if (depth > 8) return { ...r, vault: true };
31979
+ if (text5.includes("within") || text5.includes("{this")) r.vault = true;
31980
+ if (DOTTED_KEY.test(text5) && !linkedTables(q, model, out2)) r.vault = true;
31981
+ if (text5.includes("file_backlink_count")) r.links = true;
31982
+ if (Object.keys(q).some((k) => !PLAIN_SELECT_KEYS.has(k))) r.vault = true;
31983
+ const sources = [q.table];
31984
+ if (q.join && typeof q.join === "object") sources.push(q.join.table);
31985
+ if (Array.isArray(q.union)) for (const b of q.union) sources.push(b && typeof b === "object" ? b.table : void 0);
31986
+ for (const name of sources) {
31987
+ if (typeof name === "string" && isSystemTable(name)) {
31988
+ r.store = true;
31989
+ continue;
31990
+ }
31991
+ if (typeof name !== "string" || name.startsWith("base:")) {
31992
+ r.vault = true;
31993
+ continue;
31994
+ }
31995
+ const table = refOf2(model, name, "table");
31996
+ const view = refOf2(model, name, "view");
31997
+ if (!table && !view) r.vault = true;
31998
+ if (table) out2.add(table);
31999
+ const v = view ? viewQuery(model, view) : null;
32000
+ if (view && !v) r.vault = true;
32001
+ if (v) {
32002
+ const sub = queryTables(v, model, out2, depth + 1);
32003
+ r.vault ||= sub.vault;
32004
+ r.links ||= sub.links;
32005
+ r.store ||= sub.store;
32006
+ r.today ||= sub.today;
32007
+ r.now ||= sub.now;
32008
+ }
32009
+ }
32010
+ return r;
32011
+ }
32012
+ function plainTable(model, name) {
32013
+ return typeof name === "string" && !refOf2(model, name, "view") ? refOf2(model, name, "table") : null;
32014
+ }
32015
+ function linkedTables(q, model, out2) {
32016
+ const join = isRecord2(q.join) ? q.join : null;
32017
+ const branches = Array.isArray(q.union) ? q.union : null;
32018
+ const rest = {
32019
+ ...q,
32020
+ where: void 0,
32021
+ join: join ? { ...join, where: void 0 } : q.join,
32022
+ union: branches ? branches.map((b) => isRecord2(b) ? { ...b, where: void 0 } : b) : q.union
32023
+ };
32024
+ if (DOTTED_KEY.test(JSON.stringify(rest))) return false;
32025
+ const roots = branches ? branches.map((b) => isRecord2(b) ? plainTable(model, b.table) : null) : [plainTable(model, q.table)];
32026
+ if (roots.length === 0) return false;
32027
+ for (const table of roots) if (!followKeys(q.where, table, model, out2)) return false;
32028
+ if (join && !followKeys(join.where, plainTable(model, join.table), model, out2)) return false;
32029
+ for (const b of branches ?? []) if (isRecord2(b) && !followKeys(b.where, plainTable(model, b.table), model, out2)) return false;
32030
+ return true;
32031
+ }
32032
+ function followKeys(where, table, model, out2) {
32033
+ if (where === null || typeof where !== "object") return true;
32034
+ if (Array.isArray(where)) return where.every((w) => followKeys(w, table, model, out2));
32035
+ for (const [key2, cond] of Object.entries(where)) {
32036
+ if (key2 === "and" || key2 === "or" || key2 === "not") {
32037
+ if (!followKeys(cond, table, model, out2)) return false;
32038
+ } else if (key2.indexOf(".") > 0 && (table === null || !followPath(key2, table, model, out2))) return false;
32039
+ }
32040
+ return true;
32041
+ }
32042
+ function followPath(key2, table, model, out2) {
32043
+ let current2 = table;
32044
+ for (const column of key2.split(".").slice(0, -1)) {
32045
+ const next = linkTarget(model, current2, column);
32046
+ if (!next) return false;
32047
+ out2.add(next);
32048
+ current2 = next;
32049
+ }
32050
+ return true;
32051
+ }
32052
+ function linkTarget(model, full, column) {
32053
+ const t = catalogTables(model).find((x) => x.ref.full === full);
32054
+ if (!t) return null;
32055
+ let columns;
32056
+ try {
32057
+ columns = effectiveColumns(model, t.ref, t.entry);
32058
+ } catch {
32059
+ return null;
32060
+ }
32061
+ const c = columns.find((x) => x.name === column);
32062
+ const link = c !== void 0 && (c.type === "link" || c.type === "list" && c.items === "link");
32063
+ return link && typeof c.ref === "string" ? refOf2(model, c.ref, "table") : null;
32064
+ }
32065
+ function viewQuery(model, full) {
32066
+ const [catalog, schema, name] = full.split(".");
32067
+ const q = model.catalogs[catalog]?.schemas[schema]?.views[name]?.query;
32068
+ return q && typeof q === "object" ? q : null;
32069
+ }
32070
+ function tablesReadByQuery(q, model) {
32071
+ const found = /* @__PURE__ */ new Set();
32072
+ const r = queryTables(q, model, found, 0);
32073
+ return { tables: [...found].sort(), complete: !r.vault };
32074
+ }
32075
+ function tablesReadByView(model, viewFull) {
32076
+ const q = viewQuery(model, viewFull);
32077
+ return q ? tablesReadByQuery(q, model) : { tables: [], complete: false };
32078
+ }
32079
+ var PLAIN_SELECT_KEYS, DOTTED_KEY, isRecord2;
32080
+ var init_query_tables = __esm({
32081
+ "src/core/query-tables.ts"() {
32082
+ "use strict";
32083
+ init_metastore();
32084
+ PLAIN_SELECT_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where", "order_by", "limit", "offset", "group_by", "aggregates", "include_body", "join", "union"]);
32085
+ DOTTED_KEY = /"[^"]*\.[^"]*"\s*:/;
32086
+ isRecord2 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
32087
+ }
32088
+ });
32089
+
31627
32090
  // src/core/table-registry.ts
32091
+ function viewReadable(model, viewFull, scope = currentReadScope()) {
32092
+ if (scope === null) return true;
32093
+ if (!tableReadable(viewFull, scope)) return false;
32094
+ const reads = tablesReadByView(model, viewFull);
32095
+ return reads.complete && reads.tables.every((t) => tableReadable(t, scope));
32096
+ }
31628
32097
  function tableNotFound(ref2, model, kind = "table") {
31629
- const tables = model ? catalogTables(model).map(({ ref: r, entry }) => ({ r, aliases: entry.aliases })) : [];
31630
- const views = model ? catalogViews(model).map(({ ref: r, view }) => ({ r, aliases: view.aliases })) : [];
32098
+ const scope = currentReadScope();
32099
+ const tables = model ? catalogTables(model).filter(({ ref: r }) => tableReadable(r.full, scope)).map(({ ref: r, entry }) => ({ r, aliases: entry.aliases })) : [];
32100
+ const views = model ? catalogViews(model).filter(({ ref: r }) => viewReadable(model, r.full, scope)).map(({ ref: r, view }) => ({ r, aliases: view.aliases })) : [];
31631
32101
  if (model && kind === "table") {
31632
32102
  const view = views.find(({ r }) => r.full === ref2.trim() || r.table === ref2.trim() || `${r.schema}.${r.table}` === ref2.trim());
31633
32103
  if (view) {
@@ -31654,7 +32124,7 @@ function tableNotFound(ref2, model, kind = "table") {
31654
32124
  guide(
31655
32125
  sentences(
31656
32126
  near.length > 0 ? `Did you mean ${near.join(" or ")}?` : "",
31657
- kind !== "view" ? tableNames.length > 0 ? `Tables: ${nameList(tableNames)}.` : "There are no tables yet (create_table makes one)." : "",
32127
+ kind !== "view" ? tableNames.length > 0 ? `Tables: ${nameList(tableNames)}.` : scope ? `Token ${scope.token} may read no table.` : "There are no tables yet (create_table makes one)." : "",
31658
32128
  kind !== "table" && viewNames.length > 0 ? `Views: ${nameList(viewNames)}.` : kind === "view" ? "There are no views." : "",
31659
32129
  "list_tables lists them with their descriptions and aliases."
31660
32130
  ),
@@ -31791,6 +32261,8 @@ var init_table_registry = __esm({
31791
32261
  init_vault_index();
31792
32262
  init_hints();
31793
32263
  init_db_paths();
32264
+ init_read_scope();
32265
+ init_query_tables();
31794
32266
  TableNotFoundError = class extends GuidedError {
31795
32267
  constructor(ref2, guidance) {
31796
32268
  super(`Table not found: ${ref2}`, guidance);
@@ -32099,57 +32571,68 @@ async function describeDatabase(app, settings) {
32099
32571
  const config2 = await readDatabaseConfig(app, settings);
32100
32572
  const ms = await loadMetastore(app, settings);
32101
32573
  const catalogs = [];
32574
+ const scope = currentReadScope();
32575
+ const readable = (full) => tableReadable(full, scope);
32102
32576
  if (ms) {
32103
- const byTable = new Map(catalogTables(ms.model).map((x) => [x.ref.full, catalogLoadedTable(settings, ms, x.ref)]));
32577
+ const byTable = new Map(catalogTables(ms.model).filter((x) => readable(x.ref.full)).map((x) => [x.ref.full, catalogLoadedTable(settings, ms, x.ref)]));
32104
32578
  const rowFiles = catalogRowFilesOf(app, settings, [...byTable].map(([full, t]) => ({ full, def: t.def })));
32105
32579
  const unreadable = /* @__PURE__ */ new Map();
32106
32580
  for (const u of await unreadableRows(app, settings, ms.model)) unreadable.set(u.table, (unreadable.get(u.table) ?? 0) + 1);
32107
32581
  for (const [catalog, cat] of Object.entries(ms.model.catalogs)) {
32582
+ const schemas = Object.entries(cat.schemas).map(([schema, sch]) => ({
32583
+ schema,
32584
+ ...sch.description ? { description: sch.description } : {},
32585
+ tables: Object.entries(sch.tables).filter(([name]) => readable(`${catalog}.${schema}.${name}`)).map(([name, entry]) => {
32586
+ const full = `${catalog}.${schema}.${name}`;
32587
+ const t = byTable.get(full);
32588
+ const tags = tableTags(entry);
32589
+ const missing = missingDescriptions(t.def.options.description, t.def.columns);
32590
+ return {
32591
+ name,
32592
+ qualified: full,
32593
+ row_count: rowFiles.get(full).length,
32594
+ ...entry.description ? { description: entry.description } : {},
32595
+ ...tags ? { tags } : {},
32596
+ ...missing ? { missing_descriptions: missing } : {},
32597
+ ...unreadable.has(full) ? { unreadable_count: unreadable.get(full) } : {}
32598
+ };
32599
+ }),
32600
+ views: Object.entries(sch.views).filter(([name]) => viewReadable(ms.model, `${catalog}.${schema}.${name}`, scope)).map(([name, v]) => ({
32601
+ name,
32602
+ qualified: `${catalog}.${schema}.${name}`,
32603
+ ...v.description ? { description: v.description } : {}
32604
+ }))
32605
+ })).filter((x) => !scope || x.tables.length > 0 || x.views.length > 0);
32606
+ if (scope && schemas.length === 0) continue;
32108
32607
  catalogs.push({
32109
32608
  catalog,
32110
32609
  ...cat.description ? { description: cat.description } : {},
32111
32610
  ...catalog === config2.default_catalog ? { default: true } : {},
32112
- schemas: Object.entries(cat.schemas).map(([schema, sch]) => ({
32113
- schema,
32114
- ...sch.description ? { description: sch.description } : {},
32115
- tables: Object.entries(sch.tables).map(([name, entry]) => {
32116
- const full = `${catalog}.${schema}.${name}`;
32117
- const t = byTable.get(full);
32118
- const tags = tableTags(entry);
32119
- const missing = missingDescriptions(t.def.options.description, t.def.columns);
32120
- return {
32121
- name,
32122
- qualified: full,
32123
- row_count: rowFiles.get(full).length,
32124
- ...entry.description ? { description: entry.description } : {},
32125
- ...tags ? { tags } : {},
32126
- ...missing ? { missing_descriptions: missing } : {},
32127
- ...unreadable.has(full) ? { unreadable_count: unreadable.get(full) } : {}
32128
- };
32129
- }),
32130
- views: Object.entries(sch.views).map(([name, v]) => ({
32131
- name,
32132
- qualified: `${catalog}.${schema}.${name}`,
32133
- ...v.description ? { description: v.description } : {}
32134
- }))
32135
- }))
32611
+ schemas
32136
32612
  });
32137
32613
  }
32138
32614
  }
32615
+ const extending = (name, seen = /* @__PURE__ */ new Set()) => {
32616
+ if (!ms || seen.has(name)) return [];
32617
+ seen.add(name);
32618
+ const direct = catalogTables(ms.model).filter(({ entry }) => (entry.bases ?? []).includes(name)).map(({ ref: ref2 }) => ref2.full);
32619
+ const via = Object.entries(ms.model.bases).filter(([, b]) => (b.bases ?? []).includes(name)).flatMap(([n]) => extending(n, seen));
32620
+ return [...direct, ...via];
32621
+ };
32139
32622
  return {
32140
32623
  root,
32141
32624
  metastore: metastorePath(settings),
32142
32625
  journal: storePath("journal"),
32143
32626
  config: config2,
32144
32627
  catalogs,
32145
- bases: ms ? Object.entries(ms.model.bases).map(([name, b]) => {
32628
+ bases: ms ? Object.entries(ms.model.bases).filter(([name]) => !scope || extending(name).some(readable)).map(([name, b]) => {
32146
32629
  const missing = missingDescriptions(b.description, b.columns);
32147
32630
  return {
32148
32631
  name,
32149
32632
  ...b.description ? { description: b.description } : {},
32150
32633
  ...b.bases ? { bases: b.bases } : {},
32151
32634
  columns: b.columns.map((c) => String(c.name)),
32152
- tables: catalogTables(ms.model).filter(({ entry }) => (entry.bases ?? []).includes(name)).map(({ ref: ref2 }) => ref2.full),
32635
+ tables: catalogTables(ms.model).filter(({ ref: ref2, entry }) => (entry.bases ?? []).includes(name) && readable(ref2.full)).map(({ ref: ref2 }) => ref2.full),
32153
32636
  ...missing ? { missing_descriptions: { base: missing.table, columns: missing.columns } } : {}
32154
32637
  };
32155
32638
  }) : []
@@ -32392,6 +32875,8 @@ var init_database = __esm({
32392
32875
  init_descriptions();
32393
32876
  init_db_paths();
32394
32877
  init_unreadable();
32878
+ init_read_scope();
32879
+ init_table_registry();
32395
32880
  DatabaseConfigError = class extends GuidedError {
32396
32881
  constructor() {
32397
32882
  super(...arguments);
@@ -32419,7 +32904,7 @@ var init_database = __esm({
32419
32904
 
32420
32905
  // src/core/write-check.ts
32421
32906
  function staleWrite(app) {
32422
- const ctx = storage2?.getStore();
32907
+ const ctx = storage3?.getStore();
32423
32908
  if (ctx === void 0 || ctx.gen === generation(app)) return false;
32424
32909
  ctx.stale = true;
32425
32910
  return true;
@@ -32427,12 +32912,12 @@ function staleWrite(app) {
32427
32912
  function wrote(app) {
32428
32913
  const next = generation(app) + 1;
32429
32914
  GENERATION.set(app, next);
32430
- const ctx = storage2?.getStore();
32915
+ const ctx = storage3?.getStore();
32431
32916
  if (ctx) ctx.gen = next;
32432
32917
  }
32433
32918
  async function runWrite(app, fn, isStale) {
32434
- if (!storage2) return fn();
32435
- const s = storage2;
32919
+ if (!storage3) return fn();
32920
+ const s = storage3;
32436
32921
  let ctx = { gen: 0, stale: false };
32437
32922
  const attempt = () => s.run(ctx = { gen: generation(app), stale: false }, fn);
32438
32923
  let result = await attempt();
@@ -32442,19 +32927,19 @@ async function runWrite(app, fn, isStale) {
32442
32927
  function holdsStaleWrite(value) {
32443
32928
  return JSON.stringify(value ?? null).includes(STALE_WRITE);
32444
32929
  }
32445
- var STALE_WRITE, WRITE_ATTEMPTS, GENERATION, storage2, generation;
32930
+ var STALE_WRITE, WRITE_ATTEMPTS, GENERATION, storage3, generation;
32446
32931
  var init_write_check = __esm({
32447
32932
  "src/core/write-check.ts"() {
32448
32933
  "use strict";
32449
32934
  STALE_WRITE = "the database changed while this write was being checked";
32450
32935
  WRITE_ATTEMPTS = 5;
32451
32936
  GENERATION = /* @__PURE__ */ new WeakMap();
32452
- storage2 = null;
32937
+ storage3 = null;
32453
32938
  try {
32454
32939
  const { AsyncLocalStorage: AsyncLocalStorage2 } = require("node:async_hooks");
32455
- storage2 = new AsyncLocalStorage2();
32940
+ storage3 = new AsyncLocalStorage2();
32456
32941
  } catch {
32457
- storage2 = null;
32942
+ storage3 = null;
32458
32943
  }
32459
32944
  generation = (app) => GENERATION.get(app) ?? 0;
32460
32945
  }
@@ -32591,10 +33076,10 @@ var init_query = __esm({
32591
33076
  });
32592
33077
 
32593
33078
  // src/core/this-tokens.ts
32594
- function hostValue(host2, field2) {
32595
- const own2 = THIS_NOTE_FIELDS.get(field2);
33079
+ function hostValue(host2, field3) {
33080
+ const own2 = THIS_NOTE_FIELDS.get(field3);
32596
33081
  if (own2) return own2(host2);
32597
- return field2.split(".").reduce((v, k) => v && typeof v === "object" ? v[k] : void 0, host2.frontmatter);
33082
+ return field3.split(".").reduce((v, k) => v && typeof v === "object" ? v[k] : void 0, host2.frontmatter);
32598
33083
  }
32599
33084
  function shiftDate3(value, sign, n, unit) {
32600
33085
  const s = typeof value === "string" ? value : value instanceof Date ? value.toISOString() : null;
@@ -32815,6 +33300,7 @@ async function resolveWithin(app, settings, t, where) {
32815
33300
  const viaLink = via && (via.type === "link" || via.type === "list" && via.items === "link");
32816
33301
  if (!via || !viaLink || !via.ref) throw new OperationInputError(`where "${col}": "${head}" is not a link column with ref`, notALink(head, t));
32817
33302
  const target2 = via.ref === t.catalog.ref.full ? t : await loadTable(app, settings, via.ref);
33303
+ assertTableReadable(target2.catalog.ref.full);
32818
33304
  const sub = await resolveWithin(app, settings, target2, { [tail]: cond });
32819
33305
  const pred = compileWhere(sub, target2, settings, rowGetter(app, target2, settings));
32820
33306
  const hits = (await loadRows(app, settings, target2)).filter(pred).map((r) => `[[${basenameOf(r.file.path)}]]`);
@@ -32830,6 +33316,7 @@ async function resolveWithin(app, settings, t, where) {
32830
33316
  const isLink2 = spec2 && (spec2.type === "link" || spec2.type === "list" && spec2.items === "link");
32831
33317
  if (!spec2 || !isLink2 || !spec2.ref) throw new OperationInputError(`"within" needs a link column with ref: "${col}" is not one`, notALink(col, t));
32832
33318
  const target = spec2.ref === t.catalog.ref.full ? t : await loadTable(app, settings, spec2.ref);
33319
+ assertTableReadable(target.catalog.ref.full);
32833
33320
  const h = target.def.columns.find((c) => c.hierarchy);
32834
33321
  if (!h) throw new OperationInputError(`"within": ${spec2.ref} has no hierarchy column (e.g. parent with hierarchy: true)`, guide(`"within" follows a tree: give ${spec2.ref} a parent link with hierarchy: true (add_column), or filter with "eq" / "in" instead.`, "tree"));
32835
33322
  const raw = cond.within;
@@ -32932,6 +33419,7 @@ var init_where = __esm({
32932
33419
  init_path_templates();
32933
33420
  init_this_tokens();
32934
33421
  init_hints();
33422
+ init_read_scope();
32935
33423
  LOGICAL_HINT = guide(`"and" and "or" take a list of where objects, "not" one: {"or": [{"status": "watched"}, {"rating": {"gte": 4}}]}.`, "logical");
32936
33424
  LOGICAL_KEYS = /* @__PURE__ */ new Set(["or", "and", "not"]);
32937
33425
  OPERATORS = /* @__PURE__ */ new Set(["eq", "ne", "gt", "gte", "lt", "lte", "in", "contains", "icontains", "exists", "within"]);
@@ -33297,12 +33785,16 @@ async function linkTargetRule(t, settings, links, c, loadSource = true) {
33297
33785
  }
33298
33786
  const out2 = [];
33299
33787
  const shown = () => {
33788
+ const scope = currentReadScope();
33789
+ if (scope && [target.catalog.ref.full, ...links.reads(target, c.ref_where)].some((full) => !tableReadable(full, scope))) {
33790
+ return `the link must name a row of ${c.name}'s target table`;
33791
+ }
33300
33792
  const ok2 = targets(d).filter((p) => p !== row.file.path).map(basenameOf);
33301
- return ok2.length === 0 ? "none" : `${ok2.slice(0, VALID_TARGETS_SHOWN).join(", ")}${ok2.length > VALID_TARGETS_SHOWN ? ` (+${ok2.length - VALID_TARGETS_SHOWN} more)` : ""}`;
33793
+ return `valid: ${ok2.length === 0 ? "none" : `${ok2.slice(0, VALID_TARGETS_SHOWN).join(", ")}${ok2.length > VALID_TARGETS_SHOWN ? ` (+${ok2.length - VALID_TARGETS_SHOWN} more)` : ""}`}`;
33302
33794
  };
33303
33795
  const missing = given.filter((_, i) => paths[i] == null || !rows.has(paths[i])).map((x) => `[[${basenameOf(parseWikilink(x) ?? String(x))}]]`);
33304
33796
  if (missing.length > 0) {
33305
- out2.push({ column: c.name, code: "broken_link", message: `${missing.join(", ")} ${missing.length > 1 ? "are not rows" : "is not a row"} of ${target.catalog.ref.full}; valid: ${shown()}`, value: v });
33797
+ out2.push({ column: c.name, code: "broken_link", message: `${missing.join(", ")} ${missing.length > 1 ? "are not rows" : "is not a row"} of ${target.catalog.ref.full}; ${shown()}`, value: v });
33306
33798
  }
33307
33799
  if (paths.includes(row.file.path)) {
33308
33800
  out2.push({ column: c.name, code: "ref", message: `${list4 ? `${c.name} cannot hold` : `a ${c.name} cannot be`} the row itself`, value: v });
@@ -33313,7 +33805,7 @@ async function linkTargetRule(t, settings, links, c, loadSource = true) {
33313
33805
  });
33314
33806
  if (bad.length > 0) {
33315
33807
  const what = list4 ? `${bad.length > 1 ? "are not valid targets" : "is not a valid target"} in ${c.name}` : `is not a valid ${c.name}`;
33316
- out2.push({ column: c.name, code: "ref", message: `${bad.map((p) => `[[${basenameOf(p)}]]`).join(", ")} ${what}${of} (needs ${needs(d)}); valid: ${shown()}`, value: v });
33808
+ out2.push({ column: c.name, code: "ref", message: `${bad.map((p) => `[[${basenameOf(p)}]]`).join(", ")} ${what}${of} (needs ${needs(d)}); ${shown()}`, value: v });
33317
33809
  }
33318
33810
  return out2;
33319
33811
  };
@@ -33408,6 +33900,7 @@ function findRow(rows, ref2, table) {
33408
33900
  }
33409
33901
  async function linkOptionsImpl(app, settings, input) {
33410
33902
  const t = await loadTable(app, settings, input.table);
33903
+ assertTableReadable(t.catalog.ref.full);
33411
33904
  const c = refLinkColumn(t, input.column);
33412
33905
  if (!c) {
33413
33906
  const links2 = t.def.columns.filter((x) => refLinkColumn(t, x.name)).map((x) => x.name);
@@ -33426,6 +33919,9 @@ async function linkOptionsImpl(app, settings, input) {
33426
33919
  const links = new CheckLinks(app, settings);
33427
33920
  const given = input.row !== void 0 && input.row !== null && String(input.row).trim() !== "";
33428
33921
  const rule = await linkTargetRule(t, settings, links, c, given);
33922
+ if (currentReadScope()) {
33923
+ for (const full of [rule.target.catalog.ref.full, ...links.reads(rule.target, c.ref_where)]) assertTableReadable(full);
33924
+ }
33429
33925
  let ordered = [];
33430
33926
  let self;
33431
33927
  if (given) {
@@ -33484,6 +33980,7 @@ var init_link_targets = __esm({
33484
33980
  init_where();
33485
33981
  init_check_links();
33486
33982
  init_hints();
33983
+ init_read_scope();
33487
33984
  VALID_TARGETS_SHOWN = 20;
33488
33985
  }
33489
33986
  });
@@ -33491,9 +33988,51 @@ var init_link_targets = __esm({
33491
33988
  // src/core/changeset-checks.ts
33492
33989
  var changeset_checks_exports = {};
33493
33990
  __export(changeset_checks_exports, {
33991
+ HIDDEN_CHECK_MESSAGE: () => HIDDEN_CHECK_MESSAGE,
33494
33992
  linkedCheckViolations: () => linkedCheckViolations,
33495
- metastoreLeftBy: () => metastoreLeftBy
33993
+ metastoreLeftBy: () => metastoreLeftBy,
33994
+ pathHidden: () => pathHidden
33496
33995
  });
33996
+ function pathHidden(app, settings, path23, scope = currentReadScope()) {
33997
+ if (scope === null || path23 === "" || app.vault.getAbstractFileByPath(path23) === null) return false;
33998
+ return !pathReadable(app, settings, path23, scope);
33999
+ }
34000
+ function movedOffHiddenRows(app, settings, cs, written2, consequential, out2, scope) {
34001
+ const reported = new Map(written2);
34002
+ for (const m of cs.moves) reported.set(m.from, written2.get(m.to) ?? m.from);
34003
+ for (const p of cs.deletes) reported.set(p, p);
34004
+ const visible = (p) => !pathHidden(app, settings, p, scope);
34005
+ const moved = /* @__PURE__ */ new Map();
34006
+ const add = (path23, v) => {
34007
+ const list4 = moved.get(path23) ?? [];
34008
+ if (!list4.some((x) => x.code === v.code && x.column === v.column && x.message === v.message)) list4.push(v);
34009
+ moved.set(path23, list4);
34010
+ };
34011
+ for (const [path23, violations] of out2) {
34012
+ if (visible(path23)) {
34013
+ for (const v of violations) add(path23, v);
34014
+ continue;
34015
+ }
34016
+ const file = app.vault.getAbstractFileByPath(path23);
34017
+ const fm = app.metadataCache.getFileCache(file)?.frontmatter ?? {};
34018
+ const targets = [];
34019
+ for (const [key2, value] of Object.entries(fm)) {
34020
+ for (const x of Array.isArray(value) ? value : [value]) {
34021
+ const lp = parseWikilink(x);
34022
+ const dest = lp == null ? null : app.metadataCache.getFirstLinkpathDest(lp, path23);
34023
+ const to = dest ? reported.get(dest.path) : void 0;
34024
+ if (to !== void 0 && visible(to)) targets.push([to, key2]);
34025
+ }
34026
+ }
34027
+ const own2 = [...new Set(written2.values())].filter((p) => !consequential.has(p) && visible(p)).sort();
34028
+ for (const v of violations) {
34029
+ for (const [to, column] of targets) add(to, { column, code: v.code, message: HIDDEN_CHECK_MESSAGE });
34030
+ if (targets.length > 0) continue;
34031
+ for (const to of own2.length > 0 ? own2 : [""]) add(to, { code: v.code, message: HIDDEN_CHECK_MESSAGE });
34032
+ }
34033
+ }
34034
+ return moved;
34035
+ }
33497
34036
  async function metastoreLeftBy(app, settings, cs) {
33498
34037
  const path23 = metastorePath(settings);
33499
34038
  const text5 = cs.texts.filter((t) => t.path === path23).pop()?.after;
@@ -33593,9 +34132,9 @@ async function linkedCheckViolationsImpl(app, settings, cs, consequential = /* @
33593
34132
  const affected = restored || [...after.reads(s, where)].some((full) => after.changes(full));
33594
34133
  if (!affected && !rows.some((r) => written2.has(r.file.path))) continue;
33595
34134
  const heads = [...linkHeads(where)];
33596
- let passes;
34135
+ let passes2;
33597
34136
  try {
33598
- passes = compileCheck(where, s, settings, after);
34137
+ passes2 = compileCheck(where, s, settings, after);
33599
34138
  } catch (e) {
33600
34139
  for (const r of rows) {
33601
34140
  const p = written2.get(r.file.path);
@@ -33607,7 +34146,7 @@ async function linkedCheckViolationsImpl(app, settings, cs, consequential = /* @
33607
34146
  for (const r of rows) {
33608
34147
  const own2 = written2.get(r.file.path);
33609
34148
  if (own2 === void 0 && !affected) continue;
33610
- if (passes(r)) continue;
34149
+ if (passes2(r)) continue;
33611
34150
  const message = `violates CHECK ${name}: ${JSON.stringify(where)}`;
33612
34151
  if (own2 === void 0 && restored) {
33613
34152
  add(r.file.path, { constraint: name, code: "check", message: `${message} (CHECK ${name} comes back with this change)`, ...ownHint });
@@ -33625,7 +34164,9 @@ async function linkedCheckViolationsImpl(app, settings, cs, consequential = /* @
33625
34164
  }
33626
34165
  }
33627
34166
  }
33628
- return [...out2].map(([path23, violations]) => ({ path: path23, violations }));
34167
+ const scope = currentReadScope();
34168
+ const answer = scope ? movedOffHiddenRows(app, settings, cs, written2, consequential, out2, scope) : out2;
34169
+ return [...answer].map(([path23, violations]) => ({ path: path23, violations }));
33629
34170
  }
33630
34171
  function linkedCheckViolations(...args) {
33631
34172
  return phase("engine.checks", async (span) => {
@@ -33634,6 +34175,7 @@ function linkedCheckViolations(...args) {
33634
34175
  return v;
33635
34176
  }, { "datanotes.checks.kind": "linked" });
33636
34177
  }
34178
+ var HIDDEN_CHECK_MESSAGE;
33637
34179
  var init_changeset_checks = __esm({
33638
34180
  "src/core/changeset-checks.ts"() {
33639
34181
  "use strict";
@@ -33645,6 +34187,9 @@ var init_changeset_checks = __esm({
33645
34187
  init_link_targets();
33646
34188
  init_message();
33647
34189
  init_tracing();
34190
+ init_read_scope();
34191
+ init_validate();
34192
+ HIDDEN_CHECK_MESSAGE = "a linked row of a table this token may not read breaks one of its CHECKs";
33648
34193
  }
33649
34194
  });
33650
34195
 
@@ -33897,10 +34442,13 @@ async function checkOne(app, settings, table, everyUnreadable) {
33897
34442
  async function checkImpl(app, settings, input) {
33898
34443
  const limit = Math.min(Math.max(Math.trunc(input.limit ?? CHECK_LIMIT), 1), CHECK_MAX);
33899
34444
  const offset = Math.max(Math.trunc(input.offset ?? 0), 0);
33900
- const rowsOf = (list4, table) => list4.map((r) => ({ path: r.path, ...table ? { table } : {}, violations: r.violations.map(checkViolation) }));
34445
+ const scope = currentReadScope();
34446
+ const rowsOf = (list4, table) => list4.filter((r) => scope === null || pathReadable(app, settings, r.path, scope) || r.violations.every((v) => v.code === "unreadable")).map((r) => ({ path: r.path, ...table ? { table } : {}, violations: r.violations.map(checkViolation) }));
33901
34447
  if (input.table) {
34448
+ if (scope) assertTableReadable((await loadTable(app, settings, input.table)).catalog.ref.full);
33902
34449
  const r = await checkOne(app, settings, input.table);
33903
- return { table: r.table, rows_checked: r.rows, total: r.violations.length, violations: rowsOf(r.violations).slice(offset, offset + limit) };
34450
+ const shown = rowsOf(r.violations);
34451
+ return { table: r.table, rows_checked: r.rows, total: shown.length, violations: shown.slice(offset, offset + limit) };
33904
34452
  }
33905
34453
  const model = (await loadMetastore(app, settings))?.model;
33906
34454
  const all = [];
@@ -33908,7 +34456,7 @@ async function checkImpl(app, settings, input) {
33908
34456
  let rows = 0;
33909
34457
  let tables = 0;
33910
34458
  const everyUnreadable = model ? { model, rows: await unreadableRows(app, settings, model) } : void 0;
33911
- for (const full of (model ? catalogTables(model).map(({ ref: ref2 }) => ref2.full) : []).sort()) {
34459
+ for (const full of (model ? catalogTables(model).map(({ ref: ref2 }) => ref2.full) : []).filter((full2) => tableReadable(full2, scope)).sort()) {
33912
34460
  let r;
33913
34461
  try {
33914
34462
  r = await checkOne(app, settings, full, everyUnreadable);
@@ -33965,6 +34513,7 @@ var init_row_checks = __esm({
33965
34513
  init_acyclic();
33966
34514
  init_unreadable();
33967
34515
  init_tracing();
34516
+ init_read_scope();
33968
34517
  CHECK_LIMIT = 500;
33969
34518
  CHECK_MAX = 5e3;
33970
34519
  }
@@ -34198,9 +34747,9 @@ async function checkExternalEdits(app, settings, input) {
34198
34747
  const s = await table(ref2.full);
34199
34748
  for (const [name, where] of checks) {
34200
34749
  await after.prepare(s, where);
34201
- let passes;
34750
+ let passes2;
34202
34751
  try {
34203
- passes = compileCheck(where, s, settings, after);
34752
+ passes2 = compileCheck(where, s, settings, after);
34204
34753
  } catch {
34205
34754
  continue;
34206
34755
  }
@@ -34218,7 +34767,7 @@ async function checkExternalEdits(app, settings, input) {
34218
34767
  return failure2(passed(r));
34219
34768
  };
34220
34769
  const chains = linkChains(where);
34221
- for (const r of after.rowsOf(s)) await report(s, chains, r, () => failure2(passes(r)), before ? prior(r) : void 0);
34770
+ for (const r of after.rowsOf(s)) await report(s, chains, r, () => failure2(passes2(r)), before ? prior(r) : void 0);
34222
34771
  }
34223
34772
  for (const c of filteredLinkColumns(s).filter((x) => filtered.has(x.name))) {
34224
34773
  const check2 = await linkTargetCheck(s, settings, after, c);
@@ -36555,10 +37104,10 @@ function pbValue(v) {
36555
37104
  if (typeof v === "boolean") return new Pb().uint(2, v ? 1 : 0);
36556
37105
  return Number.isInteger(v) ? new Pb().uint(3, BigInt(v)) : new Pb().double(4, v);
36557
37106
  }
36558
- function pbAttrs(m, field2, a) {
37107
+ function pbAttrs(m, field3, a) {
36559
37108
  for (const [k, v] of Object.entries(a)) {
36560
37109
  if (v === void 0 || v === null) continue;
36561
- m.message(field2, new Pb().string(1, k).message(2, pbValue(v)));
37110
+ m.message(field3, new Pb().string(1, k).message(2, pbValue(v)));
36562
37111
  }
36563
37112
  return m;
36564
37113
  }
@@ -36650,48 +37199,48 @@ var init_otlp = __esm({
36650
37199
  }
36651
37200
  this.parts.push(Number(x));
36652
37201
  }
36653
- tag(field2, wire) {
36654
- this.varint(field2 << 3 | wire);
37202
+ tag(field3, wire) {
37203
+ this.varint(field3 << 3 | wire);
36655
37204
  }
36656
- uint(field2, v) {
36657
- this.tag(field2, 0);
37205
+ uint(field3, v) {
37206
+ this.tag(field3, 0);
36658
37207
  this.varint(v);
36659
37208
  return this;
36660
37209
  }
36661
- fixed32(field2, v) {
36662
- this.tag(field2, 5);
37210
+ fixed32(field3, v) {
37211
+ this.tag(field3, 5);
36663
37212
  for (let i = 0; i < 4; i++) this.parts.push(v >>> 8 * i & 255);
36664
37213
  return this;
36665
37214
  }
36666
- fixed64(field2, v) {
36667
- this.tag(field2, 1);
37215
+ fixed64(field3, v) {
37216
+ this.tag(field3, 1);
36668
37217
  for (let i = 0; i < 8; i++) this.parts.push(Number(v >> BigInt(8 * i) & BigInt(255)));
36669
37218
  return this;
36670
37219
  }
36671
- double(field2, v) {
36672
- this.tag(field2, 1);
37220
+ double(field3, v) {
37221
+ this.tag(field3, 1);
36673
37222
  const b = new DataView(new ArrayBuffer(8));
36674
37223
  b.setFloat64(0, v, true);
36675
37224
  for (let i = 0; i < 8; i++) this.parts.push(b.getUint8(i));
36676
37225
  return this;
36677
37226
  }
36678
- bytes(field2, data) {
36679
- this.tag(field2, 2);
37227
+ bytes(field3, data) {
37228
+ this.tag(field3, 2);
36680
37229
  this.varint(data.length);
36681
37230
  for (const x of data) this.parts.push(x);
36682
37231
  return this;
36683
37232
  }
36684
37233
  /** An id given in hex (trace and span ids), as bytes. */
36685
- hex(field2, hex) {
37234
+ hex(field3, hex) {
36686
37235
  const out2 = [];
36687
37236
  for (let i = 0; i < hex.length; i += 2) out2.push(parseInt(hex.slice(i, i + 2), 16));
36688
- return this.bytes(field2, out2);
37237
+ return this.bytes(field3, out2);
36689
37238
  }
36690
- string(field2, s) {
36691
- return this.bytes(field2, new TextEncoder().encode(s));
37239
+ string(field3, s) {
37240
+ return this.bytes(field3, new TextEncoder().encode(s));
36692
37241
  }
36693
- message(field2, m) {
36694
- return this.bytes(field2, m.finish());
37242
+ message(field3, m) {
37243
+ return this.bytes(field3, m.finish());
36695
37244
  }
36696
37245
  finish() {
36697
37246
  return Uint8Array.from(this.parts);
@@ -36939,23 +37488,23 @@ var init_redact = __esm({
36939
37488
 
36940
37489
  // src/api/extension-record.ts
36941
37490
  function recordProblems(v, prefix = "") {
36942
- if (!isObject(v)) return [`"${prefix.replace(/\.$/, "") || "record"}" must be an object`];
37491
+ if (!isObject2(v)) return [`"${prefix.replace(/\.$/, "") || "record"}" must be an object`];
36943
37492
  const out2 = [];
36944
- const bad = (field2, what) => out2.push(`"${prefix}${field2}" must be ${what}`);
37493
+ const bad = (field3, what) => out2.push(`"${prefix}${field3}" must be ${what}`);
36945
37494
  if (typeof v.name !== "string" || !RECORD_NAME.test(v.name)) bad("name", "2 to 40 lowercase letters, digits or -, starting with a letter");
36946
37495
  for (const k of ["version", "commit", "manifest_sha256", "entry_sha256", "token", "installed_at", "updated_at"]) if (!nonEmpty(v[k])) bad(k, "a non-empty string");
36947
37496
  if (typeof v.description !== "string") bad("description", "a string");
36948
37497
  if (typeof v.api !== "number" || !Number.isInteger(v.api) || v.api < 1) bad("api", "an integer of 1 or more");
36949
- if (!isObject(v.source)) bad("source", "an object");
37498
+ if (!isObject2(v.source)) bad("source", "an object");
36950
37499
  else if (v.source.kind !== "git" && v.source.kind !== "local") bad("source.kind", '"git" or "local"');
36951
- if (!isObject(v.tables) || Object.values(v.tables).some((t) => !isObject(t) || typeof t.table !== "string" || typeof t.owned !== "boolean")) bad("tables", "an object of roles");
37500
+ if (!isObject2(v.tables) || Object.values(v.tables).some((t) => !isObject2(t) || typeof t.table !== "string" || typeof t.owned !== "boolean")) bad("tables", "an object of roles");
36952
37501
  if (!isStrings(v.tools)) bad("tools", "a list of strings");
36953
37502
  if (!isStrings(v.write)) bad("write", "a list of strings");
36954
37503
  if (v.migration_id !== null && typeof v.migration_id !== "string") bad("migration_id", "a string or null");
36955
37504
  if (v.previous !== null) out2.push(...recordProblems(v.previous, `${prefix}previous.`));
36956
37505
  return out2;
36957
37506
  }
36958
- var RECORD_NAME, RECORD_COMMIT, DATABASE_ID, MAX_RECORD_CHARS, isObject, isStrings, nonEmpty;
37507
+ var RECORD_NAME, RECORD_COMMIT, DATABASE_ID, MAX_RECORD_CHARS, isObject2, isStrings, nonEmpty;
36959
37508
  var init_extension_record = __esm({
36960
37509
  "src/api/extension-record.ts"() {
36961
37510
  "use strict";
@@ -36963,7 +37512,7 @@ var init_extension_record = __esm({
36963
37512
  RECORD_COMMIT = /^(?:[0-9a-f]{40}|local-[0-9a-z.-]+)$/;
36964
37513
  DATABASE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
36965
37514
  MAX_RECORD_CHARS = 1e6;
36966
- isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
37515
+ isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
36967
37516
  isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
36968
37517
  nonEmpty = (v) => typeof v === "string" && v !== "";
36969
37518
  }
@@ -37566,7 +38115,7 @@ function satisfiesEngine(range, engineVersion) {
37566
38115
  }
37567
38116
  return true;
37568
38117
  }
37569
- function isObject2(v) {
38118
+ function isObject3(v) {
37570
38119
  return typeof v === "object" && v !== null && !Array.isArray(v);
37571
38120
  }
37572
38121
  function bindRoles(value, tables) {
@@ -37580,7 +38129,7 @@ function bindRoles(value, tables) {
37580
38129
  return exact ? table(exact[1]) : v.replace(new RegExp(ROLE_TEXT.source, "g"), (_m, role) => table(role));
37581
38130
  }
37582
38131
  if (Array.isArray(v)) return v.map(walk);
37583
- if (isObject2(v)) {
38132
+ if (isObject3(v)) {
37584
38133
  const out2 = {};
37585
38134
  for (const k of Object.keys(v)) out2[k] = walk(v[k]);
37586
38135
  return out2;
@@ -37601,7 +38150,7 @@ function rolesNamed(value) {
37601
38150
  const re = new RegExp(ROLE_TEXT.source, "g");
37602
38151
  for (let m = re.exec(v); m; m = re.exec(v)) add(m[1]);
37603
38152
  } else if (Array.isArray(v)) v.forEach(walk);
37604
- else if (isObject2(v)) Object.keys(v).forEach((k) => walk(v[k]));
38153
+ else if (isObject3(v)) Object.keys(v).forEach((k) => walk(v[k]));
37605
38154
  };
37606
38155
  walk(value);
37607
38156
  return out2;
@@ -37616,7 +38165,7 @@ function parseManifest(text5, file = MANIFEST_FILE) {
37616
38165
  return validateManifest(raw, file);
37617
38166
  }
37618
38167
  function validateManifest(raw, file = MANIFEST_FILE) {
37619
- if (!isObject2(raw)) throw new ExtensionManifestError(["the manifest must be a JSON object"], file);
38168
+ if (!isObject3(raw)) throw new ExtensionManifestError(["the manifest must be a JSON object"], file);
37620
38169
  const problems = [];
37621
38170
  const m = JSON.parse(JSON.stringify(raw));
37622
38171
  const add = (path23, text5) => {
@@ -37634,10 +38183,10 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37634
38183
  const needDescription = (o, path23) => {
37635
38184
  if (!nonEmpty2(o.description)) add(`${path23}.description`, "is required");
37636
38185
  };
37637
- const tablesRaw = isObject2(m.tables) ? m.tables : {};
38186
+ const tablesRaw = isObject3(m.tables) ? m.tables : {};
37638
38187
  const roles = Object.keys(tablesRaw);
37639
- const basesRaw = isObject2(m.bases) ? m.bases : {};
37640
- const permWrite = isObject2(m.permissions) && Array.isArray(m.permissions.write) ? m.permissions.write : [];
38188
+ const basesRaw = isObject3(m.bases) ? m.bases : {};
38189
+ const permWrite = isObject3(m.permissions) && Array.isArray(m.permissions.write) ? m.permissions.write : [];
37641
38190
  const roleList = (v, path23, allowed, notText) => {
37642
38191
  if (!Array.isArray(v)) return add(path23, "must be a list of roles");
37643
38192
  v.forEach((r, i) => {
@@ -37645,7 +38194,7 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37645
38194
  });
37646
38195
  };
37647
38196
  const column = (c, path23, refs, inBase) => {
37648
- if (!isObject2(c)) return add(path23, "must be an object");
38197
+ if (!isObject3(c)) return add(path23, "must be an object");
37649
38198
  keys(c, COLUMN_KEYS, path23);
37650
38199
  if (!ident2(c.name)) add(`${path23}.name`, `must be ${IDENT_TEXT}`);
37651
38200
  else if (c.name === "id") add(path23, `"id" is the engine's own column: leave it out`);
@@ -37665,7 +38214,7 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37665
38214
  const seen = /* @__PURE__ */ new Set();
37666
38215
  v.forEach((c, i) => {
37667
38216
  column(c, `${path23}[${i}]`, refs, inBase);
37668
- const name = isObject2(c) ? c.name : void 0;
38217
+ const name = isObject3(c) ? c.name : void 0;
37669
38218
  if (typeof name !== "string") return;
37670
38219
  if (seen.has(name)) add(path23, `column "${name}" twice`);
37671
38220
  seen.add(name);
@@ -37690,7 +38239,7 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37690
38239
  const path23 = `tables.${role}`;
37691
38240
  const t = tablesRaw[role];
37692
38241
  if (!IDENT3.test(role)) add(path23, `a role is ${IDENT_TEXT}`);
37693
- if (!isObject2(t)) return add(path23, "must be an object");
38242
+ if (!isObject3(t)) return add(path23, "must be an object");
37694
38243
  keys(t, TABLE_KEYS, path23);
37695
38244
  if (typeof t.owned !== "boolean") add(`${path23}.owned`, "must be true or false");
37696
38245
  for (const k of ["name", "schema"]) if (t[k] !== void 0 && !ident2(t[k])) add(`${path23}.${k}`, `must be ${IDENT_TEXT}`);
@@ -37710,14 +38259,14 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37710
38259
  const seen = /* @__PURE__ */ new Set();
37711
38260
  t.constraints.forEach((c, i) => {
37712
38261
  const cp = `${path23}.constraints[${i}]`;
37713
- if (!isObject2(c)) return add(cp, "must be an object");
38262
+ if (!isObject3(c)) return add(cp, "must be an object");
37714
38263
  keys(c, CONSTRAINT_KEYS2, cp);
37715
38264
  if (!ident2(c.name)) add(`${cp}.name`, `must be ${IDENT_TEXT}`);
37716
38265
  else {
37717
38266
  if (seen.has(c.name)) add(`${path23}.constraints`, `constraint "${c.name}" twice`);
37718
38267
  seen.add(c.name);
37719
38268
  }
37720
- if (!isObject2(c.check)) add(`${cp}.check`, "must be an object");
38269
+ if (!isObject3(c.check)) add(`${cp}.check`, "must be an object");
37721
38270
  if (typeof c.hint !== "string") add(`${cp}.hint`, "must be a string");
37722
38271
  });
37723
38272
  }
@@ -37732,12 +38281,12 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37732
38281
  }
37733
38282
  }
37734
38283
  });
37735
- if (m.bases !== void 0 && !isObject2(m.bases)) add("bases", "must be an object");
38284
+ if (m.bases !== void 0 && !isObject3(m.bases)) add("bases", "must be an object");
37736
38285
  for (const b of Object.keys(basesRaw)) {
37737
38286
  const path23 = `bases.${b}`;
37738
38287
  const base = basesRaw[b];
37739
38288
  if (!IDENT3.test(b)) add(path23, `a base name is ${IDENT_TEXT}`);
37740
- if (!isObject2(base)) {
38289
+ if (!isObject3(base)) {
37741
38290
  add(path23, "must be an object");
37742
38291
  continue;
37743
38292
  }
@@ -37745,21 +38294,21 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37745
38294
  needDescription(base, path23);
37746
38295
  columns(base.columns, `${path23}.columns`, [], true);
37747
38296
  }
37748
- if (m.views !== void 0 && !isObject2(m.views)) add("views", "must be an object");
37749
- const viewsRaw = isObject2(m.views) ? m.views : {};
38297
+ if (m.views !== void 0 && !isObject3(m.views)) add("views", "must be an object");
38298
+ const viewsRaw = isObject3(m.views) ? m.views : {};
37750
38299
  for (const v of Object.keys(viewsRaw)) {
37751
38300
  const path23 = `views.${v}`;
37752
38301
  const view = viewsRaw[v];
37753
38302
  if (!/^vw_[a-z0-9_]+$/.test(v)) add(path23, `a view name starts with vw_ and is ${IDENT_TEXT}`);
37754
- if (!isObject2(view)) {
38303
+ if (!isObject3(view)) {
37755
38304
  add(path23, "must be an object");
37756
38305
  continue;
37757
38306
  }
37758
38307
  keys(view, VIEW_KEYS, path23);
37759
38308
  needDescription(view, path23);
37760
- if (!isObject2(view.query)) add(`${path23}.query`, "must be an object");
38309
+ if (!isObject3(view.query)) add(`${path23}.query`, "must be an object");
37761
38310
  }
37762
- if (!isObject2(m.permissions)) add("permissions", `must be an object with a "write" list of roles`);
38311
+ if (!isObject3(m.permissions)) add("permissions", `must be an object with a "write" list of roles`);
37763
38312
  else {
37764
38313
  keys(m.permissions, ["write"], "permissions");
37765
38314
  roleList(m.permissions.write, "permissions.write", roles, (r) => `"${r}" is not a role`);
@@ -37769,7 +38318,7 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37769
38318
  const seen = /* @__PURE__ */ new Set();
37770
38319
  m.tools.forEach((t, i) => {
37771
38320
  const path23 = `tools[${i}]`;
37772
- if (!isObject2(t)) return add(path23, "must be an object");
38321
+ if (!isObject3(t)) return add(path23, "must be an object");
37773
38322
  keys(t, TOOL_KEYS, path23);
37774
38323
  if (typeof t.name !== "string" || !TOOL_NAME.test(t.name)) add(`${path23}.name`, "must be snake_case, 1 to 64 characters");
37775
38324
  else {
@@ -37777,10 +38326,10 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37777
38326
  seen.add(t.name);
37778
38327
  }
37779
38328
  if (!nonEmpty2(t.description)) add(`${path23}.description`, "is required");
37780
- if (!isObject2(t.inputSchema) || t.inputSchema.type !== "object" || t.inputSchema.additionalProperties !== false) {
38329
+ if (!isObject3(t.inputSchema) || t.inputSchema.type !== "object" || t.inputSchema.additionalProperties !== false) {
37781
38330
  add(`${path23}.inputSchema`, `must be an object schema with "type": "object" and "additionalProperties": false`);
37782
38331
  }
37783
- const readOnly = isObject2(t.annotations) ? t.annotations.readOnlyHint : void 0;
38332
+ const readOnly = isObject3(t.annotations) ? t.annotations.readOnlyHint : void 0;
37784
38333
  if (readOnly !== void 0 && typeof readOnly !== "boolean") add(`${path23}.annotations.readOnlyHint`, "must be true or false");
37785
38334
  if (t.tables !== void 0) roleList(t.tables, `${path23}.tables`, roles, (r) => `"${r}" is not a role`);
37786
38335
  if (t.writes !== void 0) roleList(t.writes, `${path23}.writes`, permWrite, (r) => `"${r}" is not in permissions.write`);
@@ -37793,7 +38342,7 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37793
38342
  let previous = null;
37794
38343
  (Array.isArray(m.migrations) ? m.migrations : []).forEach((g, i) => {
37795
38344
  const path23 = `migrations[${i}]`;
37796
- if (!isObject2(g)) return add(path23, "must be an object");
38345
+ if (!isObject3(g)) return add(path23, "must be an object");
37797
38346
  keys(g, MIGRATION_KEYS, path23);
37798
38347
  const at = g.version;
37799
38348
  if (typeof at !== "string" || !parseVersion(at)) add(`${path23}.version`, `"${String(at)}" is not a version`);
@@ -37806,7 +38355,7 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37806
38355
  if (!Array.isArray(g.operations)) add(`${path23}.operations`, "must be a list");
37807
38356
  else {
37808
38357
  g.operations.forEach((o, j) => {
37809
- const op2 = isObject2(o) ? o.op : void 0;
38358
+ const op2 = isObject3(o) ? o.op : void 0;
37810
38359
  if (typeof op2 !== "string" || !BATCH_OPERATIONS2.includes(op2)) add(`${path23}.operations[${j}].op`, `"${String(op2)}" is not an operation a batch takes`);
37811
38360
  });
37812
38361
  }
@@ -37822,7 +38371,7 @@ function validateManifest(raw, file = MANIFEST_FILE) {
37822
38371
  const re = new RegExp(ROLE_TEXT.source, "g");
37823
38372
  for (let r = re.exec(v); r; r = re.exec(v)) if (!known.has(r[1])) add(path23, `"@{${r[1]}}" names no role`);
37824
38373
  } else if (Array.isArray(v)) v.forEach((x, i) => names2(x, `${path23}[${i}]`));
37825
- else if (isObject2(v)) for (const k of Object.keys(v)) names2(v[k], `${path23}.${k}`);
38374
+ else if (isObject3(v)) for (const k of Object.keys(v)) names2(v[k], `${path23}.${k}`);
37826
38375
  };
37827
38376
  for (const part of ["tables", "bases", "views", "migrations"]) if (m[part] !== void 0) names2(m[part], part);
37828
38377
  if (problems.length > 0) throw new ExtensionManifestError(problems, file);
@@ -38079,7 +38628,7 @@ async function loadExtensions(o) {
38079
38628
  let manifest;
38080
38629
  try {
38081
38630
  manifest = parseManifest(import_node_fs17.default.readFileSync(manifestFile, "utf8"));
38082
- const odd = manifest.tools.findIndex((t) => !isObject3(t.annotations));
38631
+ const odd = manifest.tools.findIndex((t) => !isObject4(t.annotations));
38083
38632
  if (odd >= 0) throw new Error(`tools[${odd}].annotations: must be an object`);
38084
38633
  } catch (e) {
38085
38634
  skip("entry", `the code of ${r.name} did not load: ${e.message}`);
@@ -38240,7 +38789,7 @@ Docs: read_docs(${JSON.stringify(`extensions/${name}`)})`);
38240
38789
  }
38241
38790
  return { name, version: r.version, servedBy, tools, instructions, has: (tool) => defs.has(tool), call };
38242
38791
  }
38243
- var import_node_fs17, import_node_path19, import_node_url2, noLog, ClientTokens, isObject3, matches2, cut;
38792
+ var import_node_fs17, import_node_path19, import_node_url2, noLog, ClientTokens, isObject4, matches2, cut;
38244
38793
  var init_extension_host = __esm({
38245
38794
  "src/api/extension-host.ts"() {
38246
38795
  "use strict";
@@ -38285,7 +38834,7 @@ var init_extension_host = __esm({
38285
38834
  return value;
38286
38835
  }
38287
38836
  };
38288
- isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
38837
+ isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
38289
38838
  matches2 = (v, rule) => typeof v === "string" && rule.test(v);
38290
38839
  cut = (s, n) => s.length > n ? s.slice(0, n) : s;
38291
38840
  }
@@ -38295,7 +38844,7 @@ var init_extension_host = __esm({
38295
38844
  function stable(v) {
38296
38845
  const walk = (x) => {
38297
38846
  if (Array.isArray(x)) return x.map(walk);
38298
- if (isObject4(x)) {
38847
+ if (isObject5(x)) {
38299
38848
  const out2 = {};
38300
38849
  for (const k of Object.keys(x).sort()) out2[k] = walk(x[k]);
38301
38850
  return out2;
@@ -38915,7 +39464,7 @@ function renderPlan(plan) {
38915
39464
  lines.push("", plan.agent_message, plan.summary);
38916
39465
  return lines.join("\n");
38917
39466
  }
38918
- var AGENT_MESSAGE, MAX_OPERATIONS, isObject4, obj3, list2, tableName, isLink;
39467
+ var AGENT_MESSAGE, MAX_OPERATIONS, isObject5, obj3, list2, tableName, isLink;
38919
39468
  var init_extension_plan = __esm({
38920
39469
  "src/api/extension-plan.ts"() {
38921
39470
  "use strict";
@@ -38923,8 +39472,8 @@ var init_extension_plan = __esm({
38923
39472
  init_guidance();
38924
39473
  AGENT_MESSAGE = `For the agent installing this extension: show this plan to the user before anything is written. Where a table says "proposed", the installer chose it: ask the user to confirm it, to pick another table (--map <role>=<catalog.schema.table>, --map <role>.<column>=<column>), or to create it (--create <role>). Add a missing column only after the user agrees (--add <role>.<column>). Solve each problem the same way. Then run the same command again with the user's choices and --yes, only once the user approved the tables it creates or changes, the tools and the token's permissions.`;
38925
39474
  MAX_OPERATIONS = 200;
38926
- isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
38927
- obj3 = (v) => isObject4(v) ? v : {};
39475
+ isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
39476
+ obj3 = (v) => isObject5(v) ? v : {};
38928
39477
  list2 = (v) => Array.isArray(v) ? v : [];
38929
39478
  tableName = (t, role) => t.name ?? role;
38930
39479
  isLink = (c) => c.type === "link" || c.type === "list" && c.items === "link";
@@ -40051,7 +40600,7 @@ __export(util_exports, {
40051
40600
  hexToUint8Array: () => hexToUint8Array,
40052
40601
  hide: () => hide,
40053
40602
  installLazyProp: () => installLazyProp,
40054
- isObject: () => isObject5,
40603
+ isObject: () => isObject6,
40055
40604
  isPlainObject: () => isPlainObject2,
40056
40605
  issue: () => issue,
40057
40606
  joinValues: () => joinValues,
@@ -40258,11 +40807,11 @@ function esc(str3) {
40258
40807
  function slugify(input) {
40259
40808
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
40260
40809
  }
40261
- function isObject5(data) {
40810
+ function isObject6(data) {
40262
40811
  return typeof data === "object" && data !== null && !Array.isArray(data);
40263
40812
  }
40264
40813
  function isPlainObject2(o) {
40265
- if (isObject5(o) === false)
40814
+ if (isObject6(o) === false)
40266
40815
  return false;
40267
40816
  const ctor = o.constructor;
40268
40817
  if (ctor === void 0)
@@ -40270,7 +40819,7 @@ function isPlainObject2(o) {
40270
40819
  if (typeof ctor !== "function")
40271
40820
  return true;
40272
40821
  const prot = ctor.prototype;
40273
- if (isObject5(prot) === false)
40822
+ if (isObject6(prot) === false)
40274
40823
  return false;
40275
40824
  if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
40276
40825
  return false;
@@ -42656,20 +43205,20 @@ var init_schemas = __esm({
42656
43205
  const shape = zod.def.shape;
42657
43206
  const propValues = {};
42658
43207
  for (const key2 in shape) {
42659
- const field2 = shape[key2]._zod;
42660
- if (field2.values) {
43208
+ const field3 = shape[key2]._zod;
43209
+ if (field3.values) {
42661
43210
  if (!Object.prototype.hasOwnProperty.call(propValues, key2)) {
42662
43211
  assignProp(propValues, key2, /* @__PURE__ */ new Set());
42663
43212
  }
42664
- for (const v of field2.values)
43213
+ for (const v of field3.values)
42665
43214
  propValues[key2].add(v);
42666
- if (field2.optin !== void 0)
43215
+ if (field3.optin !== void 0)
42667
43216
  propValues[key2].add(void 0);
42668
43217
  }
42669
43218
  }
42670
43219
  return propValues;
42671
43220
  });
42672
- const isObject6 = isObject5;
43221
+ const isObject7 = isObject6;
42673
43222
  const catchall = def.catchall;
42674
43223
  let value;
42675
43224
  const memo2 = globalConfig.memoizer;
@@ -42677,7 +43226,7 @@ var init_schemas = __esm({
42677
43226
  inst._zod.parse = (payload, ctx) => {
42678
43227
  value ?? (value = _normalized.value);
42679
43228
  const input = payload.value;
42680
- if (!isObject6(input)) {
43229
+ if (!isObject7(input)) {
42681
43230
  payload.issues.push({
42682
43231
  expected: "object",
42683
43232
  code: "invalid_type",
@@ -42813,7 +43362,7 @@ var init_schemas = __esm({
42813
43362
  return doc.compile();
42814
43363
  };
42815
43364
  let fastpass;
42816
- const isObject6 = isObject5;
43365
+ const isObject7 = isObject6;
42817
43366
  const jit = !globalConfig.jitless;
42818
43367
  const allowsEval2 = allowsEval;
42819
43368
  const fastEnabled = jit && allowsEval2.value;
@@ -42822,7 +43371,7 @@ var init_schemas = __esm({
42822
43371
  inst._zod.parse = (payload, ctx) => {
42823
43372
  value ?? (value = _normalized.value);
42824
43373
  const input = payload.value;
42825
- if (!isObject6(input)) {
43374
+ if (!isObject7(input)) {
42826
43375
  payload.issues.push({
42827
43376
  expected: "object",
42828
43377
  code: "invalid_type",
@@ -42922,7 +43471,7 @@ var init_schemas = __esm({
42922
43471
  const disc = cached(() => discriminatorMap(def));
42923
43472
  inst._zod.parse = (payload, ctx) => {
42924
43473
  const input = payload.value;
42925
- if (!isObject5(input)) {
43474
+ if (!isObject6(input)) {
42926
43475
  payload.issues.push({
42927
43476
  code: "invalid_type",
42928
43477
  expected: "object",
@@ -45285,8 +45834,8 @@ var init_json_schema_processors = __esm({
45285
45834
  }
45286
45835
  const requiredKeys = [];
45287
45836
  for (const key2 of Object.keys(shape)) {
45288
- const field2 = def.shape[key2];
45289
- if (ctx.io === "input" ? inputOptin(field2) === void 0 : field2._zod.optout === void 0) {
45837
+ const field3 = def.shape[key2];
45838
+ if (ctx.io === "input" ? inputOptin(field3) === void 0 : field3._zod.optout === void 0) {
45290
45839
  requiredKeys.push(key2);
45291
45840
  }
45292
45841
  }
@@ -61187,6 +61736,7 @@ init_metastore();
61187
61736
  init_table_registry();
61188
61737
  init_hints();
61189
61738
  init_tracing();
61739
+ init_read_scope();
61190
61740
  var HISTORY_OFF = (full) => guide(`Turn history on for it with alter_table("${full}", history: true), or for every table with configure_database(history: true); versions are kept from then on.`, "history");
61191
61741
  var ASOF_HINT = guide(`Pass "version" (an integer from describe_history) or "timestamp" (an ISO date-time), not both.`, "readAsOf");
61192
61742
  async function historyTableName(app, settings, table) {
@@ -61268,6 +61818,7 @@ async function newestHistory(app, settings, full, limit) {
61268
61818
  }
61269
61819
  async function describeHistory(app, settings, input) {
61270
61820
  const ref2 = await historyTableName(app, settings, input.table);
61821
+ assertTableReadable(ref2.full);
61271
61822
  const limit = input.limit ?? 50;
61272
61823
  if (input.since === void 0) return { table: ref2.full, history: await newestHistory(app, settings, ref2.full, limit) };
61273
61824
  const from = sinceMs(input.since);
@@ -61383,6 +61934,7 @@ function rowValues(content, settings) {
61383
61934
  }
61384
61935
  async function tableChanges(app, settings, input) {
61385
61936
  const ref2 = await historyTableName(app, settings, input.table);
61937
+ assertTableReadable(ref2.full);
61386
61938
  const versions = await listVersions(app, settings, ref2.full);
61387
61939
  if (versions.length === 0) throw new OperationInputError(`${ref2.full} has no history`, HISTORY_OFF(ref2.full));
61388
61940
  const latest = versions[versions.length - 1];
@@ -61449,6 +62001,8 @@ init_check_links();
61449
62001
  init_changeset_checks();
61450
62002
  init_computed_columns();
61451
62003
  init_path_templates();
62004
+ init_read_scope();
62005
+ init_query_tables();
61452
62006
  init_where();
61453
62007
  init_link_paths();
61454
62008
  init_validate();
@@ -61507,10 +62061,10 @@ async function addConstraintImpl(app, settings, input, opts = {}) {
61507
62061
  if (cat.entry.constraints?.[input.name]) throw new OperationInputError(`constraint "${input.name}" already exists; drop it first`, guide(`Choose another name, or drop_constraint "${input.name}" first to replace it; describe_table lists the constraints.`, "check"));
61508
62062
  const links = new CheckLinks(app, settings);
61509
62063
  await links.prepare(t, input.check);
61510
- const passes = compileCheck(input.check, t, settings, links);
62064
+ const passes2 = compileCheck(input.check, t, settings, links);
61511
62065
  const conflicts = [];
61512
62066
  for (const r of await loadRows(app, settings, t)) {
61513
- if (!passes(r)) conflicts.push({ path: r.file.path, message: `violates CHECK ${input.name}` });
62067
+ if (!passes2(r)) conflicts.push({ path: r.file.path, message: `violates CHECK ${input.name}` });
61514
62068
  }
61515
62069
  const hints = { ...constraintHints(cat.entry), ...hint ? { [input.name]: hint } : {} };
61516
62070
  const { constraint_hints: _h, ...withoutHints } = cat.entry;
@@ -61620,6 +62174,7 @@ async function resolveViewSelect(app, settings, input) {
61620
62174
  if (tableRef) return null;
61621
62175
  const ref2 = resolveCatalogRef(ms.model, input.table, "view");
61622
62176
  if (!ref2) return null;
62177
+ assertTableReadable(ref2.full);
61623
62178
  const view = ms.model.catalogs[ref2.catalog].schemas[ref2.schema].views[ref2.table];
61624
62179
  const base = viewQueryToSelect(input.host ? resolveThisTokens(view.query, input.host) : view.query);
61625
62180
  let where = { ...base.where ?? {} };
@@ -61719,13 +62274,17 @@ async function informationRows(app, settings, name) {
61719
62274
  const ms = await loadMetastore(app, settings);
61720
62275
  const model = ms?.model ?? parseMetastore("");
61721
62276
  const cfg = await readDatabaseConfig(app, settings);
61722
- const tables = catalogTables(model);
62277
+ const scope = currentReadScope();
62278
+ const tables = catalogTables(model).filter(({ ref: ref2 }) => tableReadable(ref2.full, scope));
62279
+ const views = catalogViews(model).filter(({ ref: ref2 }) => viewReadable2(model, ref2.full, scope));
62280
+ const visible = new Set([...tables, ...views].map(({ ref: ref2 }) => `${ref2.catalog}.${ref2.schema}`));
62281
+ const shown = (catalog, schema) => scope === null || (schema !== void 0 ? visible.has(`${catalog}.${schema}`) : [...visible].some((k) => k.startsWith(`${catalog}.`)));
61723
62282
  switch (name) {
61724
62283
  case "catalogs":
61725
- return Object.entries(model.catalogs).map(([c, cat]) => ({ catalog_name: c, comment: cat.description ?? null, is_default: c === cfg.default_catalog }));
62284
+ return Object.entries(model.catalogs).filter(([c]) => shown(c)).map(([c, cat]) => ({ catalog_name: c, comment: cat.description ?? null, is_default: c === cfg.default_catalog }));
61726
62285
  case "schemata":
61727
62286
  return Object.entries(model.catalogs).flatMap(
61728
- ([c, cat]) => Object.entries(cat.schemas).map(([s, sch]) => ({ catalog_name: c, schema_name: s, comment: sch.description ?? null }))
62287
+ ([c, cat]) => Object.entries(cat.schemas).filter(([s]) => shown(c, s)).map(([s, sch]) => ({ catalog_name: c, schema_name: s, comment: sch.description ?? null }))
61729
62288
  );
61730
62289
  case "tables": {
61731
62290
  const out2 = [];
@@ -61746,7 +62305,7 @@ async function informationRows(app, settings, name) {
61746
62305
  instructions: tableInstructions(entry) ?? null
61747
62306
  });
61748
62307
  }
61749
- for (const { ref: ref2, view } of catalogViews(model)) {
62308
+ for (const { ref: ref2, view } of views) {
61750
62309
  out2.push({
61751
62310
  table_catalog: ref2.catalog,
61752
62311
  table_schema: ref2.schema,
@@ -61784,7 +62343,7 @@ async function informationRows(app, settings, name) {
61784
62343
  }));
61785
62344
  });
61786
62345
  case "views":
61787
- return catalogViews(model).map(({ ref: ref2, view }) => ({
62346
+ return views.map(({ ref: ref2, view }) => ({
61788
62347
  table_catalog: ref2.catalog,
61789
62348
  table_schema: ref2.schema,
61790
62349
  table_name: ref2.table,
@@ -61807,6 +62366,12 @@ async function informationRows(app, settings, name) {
61807
62366
  });
61808
62367
  }
61809
62368
  }
62369
+ function viewReadable2(model, full, scope) {
62370
+ if (scope === null) return true;
62371
+ if (!tableReadable(full, scope)) return false;
62372
+ const r = tablesReadByView(model, full);
62373
+ return r.complete && r.tables.every((t) => tableReadable(t, scope));
62374
+ }
61810
62375
  async function informationSchemaTableData(app, settings, name) {
61811
62376
  const ref2 = refOf(SYSTEM_CATALOG, "information_schema", name);
61812
62377
  const entry = { columns: INFO_COLUMNS[name].map(([n, type]) => ({ name: n, type })) };
@@ -62069,6 +62634,8 @@ var setTableBases = instrumented("set_table_bases", setTableBasesImpl, (i) => ({
62069
62634
  // src/core/select.ts
62070
62635
  init_this_tokens();
62071
62636
  init_metastore();
62637
+ init_query_tables();
62638
+ init_read_scope();
62072
62639
  init_engine();
62073
62640
  init_file_cache();
62074
62641
  init_frontmatter();
@@ -62167,9 +62734,11 @@ async function loadJoin(app, settings, t, left, join, env, asOf2) {
62167
62734
  if (pairs.length !== 1) throw new OperationInputError(`join.on must contain exactly one "left": "right" pair`, JOIN_HINT);
62168
62735
  const [leftText, rightText] = pairs[0];
62169
62736
  if (typeof rightText !== "string") throw new OperationInputError(`join.on values must be strings`, JOIN_HINT);
62737
+ if (asOf2.timestamp !== void 0) await assertRefReadable(app, settings, join.table);
62170
62738
  const hist = asOf2.timestamp !== void 0 ? await loadTableAsOf(app, settings, join.table, { timestamp: asOf2.timestamp }) : null;
62171
62739
  if (hist) asOf2.read.set(hist.table.catalog.ref.full, hist.version);
62172
62740
  const rt = hist?.table ?? await loadTable(app, settings, join.table);
62741
+ assertTableReadable(rt.catalog.ref.full);
62173
62742
  const rKnown = readableKeys(rt, settings);
62174
62743
  const lGet = rowGetter(app, t, settings, env.tz);
62175
62744
  const rGet = rowGetter(app, rt, settings, env.tz);
@@ -62197,6 +62766,12 @@ async function loadJoin(app, settings, t, left, join, env, asOf2) {
62197
62766
  };
62198
62767
  }
62199
62768
  var MAX_VIEW_DEPTH = 8;
62769
+ async function assertRefReadable(app, settings, ref2) {
62770
+ if (!currentReadScope()) return;
62771
+ const model = (await loadMetastore(app, settings))?.model;
62772
+ const full = model ? refOf2(model, ref2, "table") : null;
62773
+ if (full) assertTableReadable(full);
62774
+ }
62200
62775
  var UNION_BRANCH_MAX = 1e3;
62201
62776
  function perTableVersion(what) {
62202
62777
  return new OperationInputError(
@@ -62214,7 +62789,7 @@ async function selectBase(app, settings, input, depth, read3) {
62214
62789
  const tables = catalogTables(model).filter(({ entry }) => {
62215
62790
  const ext = entry.bases ?? [];
62216
62791
  return ext.length > 0 && baseChain(model.bases, ext).includes(name);
62217
- }).map(({ ref: ref2 }) => ref2.full);
62792
+ }).map(({ ref: ref2 }) => ref2.full).filter((full) => tableReadable(full));
62218
62793
  const own2 = /* @__PURE__ */ new Set([SYSTEM_ID_COLUMN, ...baseChain(model.bases, [name]).flatMap((b) => model.bases[b].columns.map((c) => String(c.name)))]);
62219
62794
  const columns = input.columns ?? [...own2];
62220
62795
  for (const c of [...columns, ...Object.keys(input.where ?? {}).filter((k) => !["and", "or", "not"].includes(k))]) {
@@ -62299,10 +62874,12 @@ async function selectImpl(app, settings, input, depth = 0, read3 = /* @__PURE__
62299
62874
  }
62300
62875
  if (input.version === void 0 && input.timestamp === void 0) return selectOn(app, settings, input, null, read3);
62301
62876
  if (input.join && input.version !== void 0) throw perTableVersion("a join reads two tables");
62877
+ await assertRefReadable(app, settings, input.table);
62302
62878
  const hist = await loadTableAsOf(app, settings, input.table, {
62303
62879
  ...input.version !== void 0 ? { version: input.version } : {},
62304
62880
  ...input.timestamp !== void 0 ? { timestamp: input.timestamp } : {}
62305
62881
  });
62882
+ assertTableReadable(hist.table.catalog.ref.full);
62306
62883
  read3.set(hist.table.catalog.ref.full, hist.version);
62307
62884
  const r = await selectOn(app, settings, input, hist, read3);
62308
62885
  return { ...r, version: hist.version, timestamp: hist.timestamp, ...input.join ? { versions: sortedVersions(read3) } : {} };
@@ -62321,6 +62898,7 @@ async function selectOn(app, settings, input, hist, read3) {
62321
62898
  const t = hist?.table ?? await loadTable(app, settings, input.table).catch(async (e) => {
62322
62899
  throw e instanceof TableNotFoundError ? tableNotFound(input.table, (await loadMetastore(app, settings))?.model, "any") : e;
62323
62900
  });
62901
+ if (!hist) assertTableReadable(t.catalog.ref.full);
62324
62902
  const known = readableKeys(t, settings);
62325
62903
  const tz = (await readDatabaseConfig(app, settings)).timezone;
62326
62904
  const now = /* @__PURE__ */ new Date();
@@ -62352,7 +62930,7 @@ async function selectOn(app, settings, input, hist, read3) {
62352
62930
  const linkpath = parseWikilink(v);
62353
62931
  if (linkpath == null) return v;
62354
62932
  const dest = app.metadataCache.getFirstLinkpathDest(linkpath, sourcePath);
62355
- if (!dest || isArchivedPath(app, settings, dest.path)) return v;
62933
+ if (!dest || isArchivedPath(app, settings, dest.path) || !pathReadable(app, settings, dest.path)) return v;
62356
62934
  const fm = app.metadataCache.getFileCache(dest)?.frontmatter ?? {};
62357
62935
  return { path: dest.path, ...fm };
62358
62936
  };
@@ -62454,15 +63032,16 @@ function select(app, settings, input) {
62454
63032
  return traced(
62455
63033
  "datanotes select",
62456
63034
  { "datanotes.op": "select", table: input.table },
62457
- async () => selectImpl(app, settings, input.thisNote === void 0 ? input : { ...input, host: await noteHost(app, input.thisNote) }),
63035
+ async () => selectImpl(app, settings, input.thisNote === void 0 ? input : { ...input, host: await noteHost(app, settings, input.thisNote) }),
62458
63036
  (r) => ({ attributes: { total: r.total, returned: r.rows.length } })
62459
63037
  );
62460
63038
  }
62461
63039
  var THIS_NOTE_HINT = guide(`Give "this_note" the vault path of an existing note, with ".md" (list_folder or search_notes find it).`, "thisNote");
62462
- async function noteHost(app, path23) {
63040
+ async function noteHost(app, settings, path23) {
62463
63041
  const why = unsafeVaultPath(path23);
62464
63042
  if (why) throw new OperationInputError(`this_note: ${why}`, THIS_NOTE_HINT);
62465
63043
  const file = app.vault.getAbstractFileByPath(path23);
63044
+ assertPathReadable(app, settings, file?.path ?? path23);
62466
63045
  if (file == null || !("extension" in file)) throw new OperationInputError(`this_note: no note at "${path23}" (give its vault path, e.g. "Journal/2026-10-06.md")`, THIS_NOTE_HINT);
62467
63046
  const { ordered } = readOrderedFromMarkdown(await app.vault.read(file));
62468
63047
  return { path: file.path, frontmatter: Object.fromEntries(ordered) };
@@ -62551,6 +63130,7 @@ init_file_cache();
62551
63130
  init_inverse();
62552
63131
  init_metastore();
62553
63132
  init_tracing();
63133
+ init_read_scope();
62554
63134
  init_vault_index();
62555
63135
  var MIN_NAME_LENGTH = 3;
62556
63136
  var MAX_NAME_WORDS = 12;
@@ -62921,7 +63501,7 @@ function standsAlone(text5, start, end) {
62921
63501
  if (after === "." || after === ":") return !/[\p{L}\p{N}_/\\]/u.test(text5[end + 1] ?? "");
62922
63502
  return true;
62923
63503
  }
62924
- function matchesIn(text5, tokens, ix, self) {
63504
+ function matchesIn(text5, tokens, ix, self, visible) {
62925
63505
  const out2 = [];
62926
63506
  for (let i = 0; i < tokens.length; i++) {
62927
63507
  let key2 = tokens[i].norm;
@@ -62935,9 +63515,10 @@ function matchesIn(text5, tokens, ix, self) {
62935
63515
  let any = false;
62936
63516
  let isSelf = false;
62937
63517
  for (const p of paths.keys()) {
62938
- any = true;
62939
63518
  if (self.has(p)) isSelf = true;
62940
- else cands.push(p);
63519
+ else if (visible(p)) cands.push(p);
63520
+ else continue;
63521
+ any = true;
62941
63522
  }
62942
63523
  if (any) out2.push({ ti: i, tj: j, start: tokens[i].start, end: tokens[j].end, key: key2, cands: isSelf ? [] : cands.sort(), self: isSelf });
62943
63524
  }
@@ -62950,7 +63531,7 @@ function matchesIn(text5, tokens, ix, self) {
62950
63531
  }
62951
63532
  return out2.sort((a, b) => a.start - b.start || b.end - a.end);
62952
63533
  }
62953
- function mentionsIn(text5, frag, ix, self) {
63534
+ function mentionsIn(text5, frag, ix, self, visible) {
62954
63535
  const out2 = [];
62955
63536
  for (const [a, b] of freeSegments(text5)) {
62956
63537
  const tokens = [...text5.slice(a, b).matchAll(WORD_RE)].map((m) => ({
@@ -62958,7 +63539,7 @@ function mentionsIn(text5, frag, ix, self) {
62958
63539
  end: a + m.index + m[0].length,
62959
63540
  norm: m[0].normalize("NFC").toLowerCase()
62960
63541
  }));
62961
- const matches3 = matchesIn(text5, tokens, ix, self);
63542
+ const matches3 = matchesIn(text5, tokens, ix, self, visible);
62962
63543
  if (matches3.length === 0) continue;
62963
63544
  const inMatch = /* @__PURE__ */ new Set();
62964
63545
  for (const m of matches3) for (let t = m.ti; t <= m.tj; t++) inMatch.add(t);
@@ -63064,7 +63645,15 @@ async function autolinkFragmentsImpl(app, settings, input) {
63064
63645
  if (input.fragments.every((f) => f.trim() === "")) return { fragments: [...input.fragments], report };
63065
63646
  const ix = await nameIndex(app, settings);
63066
63647
  const self = /* @__PURE__ */ new Set([input.path, ...input.self ?? []]);
63067
- const mentions = input.fragments.flatMap((text5, i) => mentionsIn(text5, i, ix, self));
63648
+ const scope = currentReadScope();
63649
+ const seenVisible = /* @__PURE__ */ new Map();
63650
+ const visible = (p) => {
63651
+ if (!scope) return true;
63652
+ let v = seenVisible.get(p);
63653
+ if (v === void 0) seenVisible.set(p, v = pathReadable(app, settings, p, scope));
63654
+ return v;
63655
+ };
63656
+ const mentions = input.fragments.flatMap((text5, i) => mentionsIn(text5, i, ix, self, visible));
63068
63657
  const doubtful = new Set(mentions.filter((m) => m.kind !== "clear" && m.kind !== "self" && m.kind !== "sentence_start").flatMap((m) => m.keys));
63069
63658
  const linked = linkedTargets(app, input.body, input.path);
63070
63659
  const edits = [];
@@ -63708,6 +64297,13 @@ function clearLinksTo(app, sourcePath, ordered, targets) {
63708
64297
  return pointsAt(v) ? [k, null] : [k, v];
63709
64298
  });
63710
64299
  }
64300
+ function referencedBy(app, settings, sources) {
64301
+ const shown = sources.filter((p) => !pathHidden(app, settings, p));
64302
+ const hidden2 = sources.length - shown.length;
64303
+ if (hidden2 === 0) return `referenced by ${sources.join(", ")}`;
64304
+ const rest = `${hidden2} ${hidden2 === 1 ? "row" : "rows"} this token may not read`;
64305
+ return `referenced by ${shown.length > 0 ? `${shown.join(", ")} and ${rest}` : rest}`;
64306
+ }
63711
64307
  async function deleteRowsImpl(app, settings, input, opts = {}) {
63712
64308
  const t = await loadTable(app, settings, input.table);
63713
64309
  const pred = compileWhere(await resolveWithin(app, settings, t, input.where), t, settings);
@@ -63722,7 +64318,7 @@ async function deleteRowsImpl(app, settings, input, opts = {}) {
63722
64318
  for (const r of rows) {
63723
64319
  const sources = inbound.get(r.file.path) ?? [];
63724
64320
  if (sources.length > 0 && policy === "restrict") {
63725
- conflicts.push({ path: r.file.path, message: `referenced by ${sources.join(", ")}` });
64321
+ conflicts.push({ path: r.file.path, message: referencedBy(app, settings, sources) });
63726
64322
  }
63727
64323
  sources.forEach((s) => referrers.add(s));
63728
64324
  }
@@ -63795,6 +64391,7 @@ init_where();
63795
64391
  init_row_checks();
63796
64392
  init_changeset_checks();
63797
64393
  init_message();
64394
+ init_read_scope();
63798
64395
  init_link_paths();
63799
64396
  function checkRetarget(dst, settings, r) {
63800
64397
  const dstKnown = knownKeys(dst, settings);
@@ -63970,7 +64567,7 @@ async function adoptRowsImpl(app, settings, input, opts = {}) {
63970
64567
  /* @__PURE__ */ new Set([settings.tableKey])
63971
64568
  );
63972
64569
  const owner = model ? rowTableFull(model, fmGet(ordered, settings.tableKey)) : null;
63973
- if (owner) conflicts.push({ path: p, message: `already a row of ${owner}: use move_rows` });
64570
+ if (owner) conflicts.push({ path: p, message: `already a row of ${tableReadable(owner) ? owner : "another table"}: use move_rows` });
63974
64571
  notes.push({ path: p, ordered });
63975
64572
  }
63976
64573
  const cs = emptyChangeSet("adopt_rows", `adopt ${notes.length} notes into ${dst.def.name}`, dst.def.schemaPath);
@@ -64593,13 +65190,36 @@ init_engine();
64593
65190
  init_hints();
64594
65191
  init_journal();
64595
65192
  init_metastore();
65193
+ init_read_scope();
64596
65194
  async function describeDatabaseHistory(app, settings, input = {}) {
64597
65195
  const from = input.since === void 0 ? void 0 : sinceMs(input.since);
64598
65196
  const tables = await loggedTables(app, settings);
64599
65197
  const limit = Math.max(input.limit ?? 50, 0);
65198
+ const batchOf = await batchMembership(app, settings);
65199
+ const scope = currentReadScope();
65200
+ if (!scope) return databaseCommits(tables, await newestOf(app, settings, tables, limit, from), batchOf, limit);
65201
+ const readable = tables.filter((t) => tableReadable(t, scope));
65202
+ if (!(limit > 0)) return { tables: readable, history: [] };
65203
+ for (let depth = limit; ; depth *= 2) {
65204
+ const mine = await newestOf(app, settings, readable, depth, from);
65205
+ const oldest = mine.reduce((m, c) => Math.min(m, Date.parse(c.timestamp)), Infinity);
65206
+ const others = oldest === Infinity ? [] : await newestOf(app, settings, tables.filter((t) => !readable.includes(t)), Infinity, Math.max(from ?? -Infinity, oldest - 6e4));
65207
+ const all = databaseCommits(tables, [...mine, ...others], batchOf, Infinity);
65208
+ const shown = all.history.filter((c) => tableReadable(c.table, scope) && (c.tables ?? []).every((t) => tableReadable(t, scope)));
65209
+ const last = shown.length >= limit ? shown[limit - 1].timestamp : null;
65210
+ const complete = readable.every((t) => {
65211
+ const read3 = mine.filter((c) => c.table === t);
65212
+ return read3.length < depth || last !== null && read3[read3.length - 1].timestamp < last;
65213
+ });
65214
+ if (complete) return { tables: readable, history: shown.slice(0, limit) };
65215
+ }
65216
+ }
65217
+ async function newestOf(app, settings, tables, limit, from) {
64600
65218
  const all = [];
64601
65219
  for (const table of tables) for (const c of await newestCommitInfos(app, settings, table, limit, from)) all.push({ table, ...c });
64602
- const batchOf = await batchMembership(app, settings);
65220
+ return all;
65221
+ }
65222
+ function databaseCommits(tables, all, batchOf, limit) {
64603
65223
  const groupOf = (c) => c.migrationId === void 0 ? void 0 : batchOf.get(c.migrationId) ?? c.migrationId;
64604
65224
  const groups = /* @__PURE__ */ new Map();
64605
65225
  for (const c of all) {
@@ -64757,6 +65377,8 @@ init_metastore();
64757
65377
  init_descriptions();
64758
65378
  init_row_files();
64759
65379
  init_table_registry();
65380
+ init_read_scope();
65381
+ init_query_tables();
64760
65382
 
64761
65383
  // src/core/graph.ts
64762
65384
  init_engine();
@@ -64812,6 +65434,7 @@ init_table_registry();
64812
65434
  init_validate();
64813
65435
  init_hints();
64814
65436
  init_tracing();
65437
+ init_read_scope();
64815
65438
  var LINK_LABEL = "link";
64816
65439
  var MAX_GRAPH_DEPTH = 10;
64817
65440
  var MAX_GRAPH_NODES = 5e3;
@@ -64833,7 +65456,10 @@ function addEdge(g, keys, from, to, label) {
64833
65456
  back.push({ to: from, label });
64834
65457
  g.in.set(to, back);
64835
65458
  }
64836
- async function graphIndex(app, settings) {
65459
+ function graphIndex(app, settings) {
65460
+ return runWithReadScope(null, () => sharedGraph(app, settings));
65461
+ }
65462
+ async function sharedGraph(app, settings) {
64837
65463
  const generation2 = vaultGeneration(app);
64838
65464
  let metastore = cachedMetastore(app, settings);
64839
65465
  const attempt = `${generation2}\0${metastorePath(settings)}`;
@@ -65110,7 +65736,7 @@ function tablesOf(app, settings, names2, what) {
65110
65736
  for (const n of names2) {
65111
65737
  if (TABLE_PATTERN.test(n)) {
65112
65738
  const prefix = n.slice(0, -1);
65113
- for (const t of all) if (t.qualified.startsWith(prefix)) out2.add(t.qualified);
65739
+ for (const t of all) if (t.qualified.startsWith(prefix) && tableReadable(t.qualified)) out2.add(t.qualified);
65114
65740
  continue;
65115
65741
  }
65116
65742
  if (n.includes("*")) throw new OperationInputError(`${what}: "${n}" is not a table pattern`, NODE_SET_HINT);
@@ -65124,10 +65750,12 @@ async function resolveNodes(app, settings, set, what) {
65124
65750
  for (const p of set.paths ?? []) {
65125
65751
  const path23 = p.endsWith(".md") ? p : `${p}.md`;
65126
65752
  if (app.vault.getAbstractFileByPath(path23) == null) throw new OperationInputError(`${what}: note not found: ${path23}`);
65753
+ assertPathReadable(app, settings, path23);
65127
65754
  out2.add(path23);
65128
65755
  }
65129
65756
  if (set.table !== void 0) {
65130
65757
  const t = await loadTable(app, settings, set.table);
65758
+ assertTableReadable(t.catalog.ref.full);
65131
65759
  const pred = compileWhere(await resolveWithin(app, settings, t, set.where), t, settings);
65132
65760
  for (const r of await loadRows(app, settings, t)) if (pred(r)) out2.add(r.file.path);
65133
65761
  } else if (set.where !== void 0) {
@@ -65141,13 +65769,24 @@ async function resolveNodes(app, settings, set, what) {
65141
65769
  if (!set.paths && set.table === void 0 && set.tables === void 0) throw new OperationInputError(`"${what}" needs "paths", "table" or "tables"`, NODE_SET_HINT);
65142
65770
  return [...out2].sort();
65143
65771
  }
65144
- function stepsFrom(g, node2, direction, labels) {
65772
+ function visibleNodes(app, settings) {
65773
+ const scope = currentReadScope();
65774
+ if (!scope) return null;
65775
+ const known = /* @__PURE__ */ new Map();
65776
+ return (path23) => {
65777
+ let ok2 = known.get(path23);
65778
+ if (ok2 === void 0) known.set(path23, ok2 = pathReadable(app, settings, path23, scope));
65779
+ return ok2;
65780
+ };
65781
+ }
65782
+ function stepsFrom(g, node2, direction, labels, visible) {
65145
65783
  const steps = [];
65784
+ const follow = (e) => (!labels || labels.has(e.label)) && (!visible || visible(e.to));
65146
65785
  if (direction !== "in") {
65147
- for (const e of g.out.get(node2) ?? []) if (!labels || labels.has(e.label)) steps.push({ ...e, dir: "out" });
65786
+ for (const e of g.out.get(node2) ?? []) if (follow(e)) steps.push({ ...e, dir: "out" });
65148
65787
  }
65149
65788
  if (direction !== "out") {
65150
- for (const e of g.in.get(node2) ?? []) if (!labels || labels.has(e.label)) steps.push({ ...e, dir: "in" });
65789
+ for (const e of g.in.get(node2) ?? []) if (follow(e)) steps.push({ ...e, dir: "in" });
65151
65790
  }
65152
65791
  return steps;
65153
65792
  }
@@ -65163,11 +65802,12 @@ async function graphNeighbors(app, settings, input) {
65163
65802
  const direction = checkDirection(input.direction);
65164
65803
  const g = await graphIndex(app, settings);
65165
65804
  const labels = labelSet(input.edges);
65805
+ const visible = visibleNodes(app, settings);
65166
65806
  const nodes = await resolveNodes(app, settings, input.from, "from");
65167
65807
  if (nodes.length > MAX_GRAPH_NODES) throw new OperationInputError(`"from" matches ${nodes.length} notes (max ${MAX_GRAPH_NODES}): narrow it`);
65168
65808
  return {
65169
65809
  nodes: nodes.map((path23) => {
65170
- const edges = stepsFrom(g, path23, direction, labels).map((s) => ({
65810
+ const edges = stepsFrom(g, path23, direction, labels, visible).map((s) => ({
65171
65811
  path: s.to,
65172
65812
  ...g.tableOf.has(s.to) ? { table: g.tableOf.get(s.to) } : {},
65173
65813
  label: s.label,
@@ -65182,11 +65822,18 @@ async function graphNeighbors(app, settings, input) {
65182
65822
  function qualifiedTables(app, settings, names2, what) {
65183
65823
  if (!names2 || names2.length === 0) return null;
65184
65824
  const all = listTables(app, settings.databaseFolder);
65825
+ const scope = currentReadScope();
65826
+ const known = scope ? all.filter((t) => tableReadable(t.qualified, scope)) : all;
65185
65827
  const out2 = /* @__PURE__ */ new Set();
65186
65828
  for (const n of names2) {
65187
- const hits = all.filter((t) => t.qualified === n || t.qualified.endsWith(`.${n}`));
65829
+ let hits = all.filter((t) => t.qualified === n || t.qualified.endsWith(`.${n}`));
65830
+ if (scope) {
65831
+ const readable = hits.filter((t) => tableReadable(t.qualified, scope));
65832
+ if (readable.length === 0 && hits.length > 0) throw tableReadDenied(scope.token, hits[0].qualified);
65833
+ hits = readable;
65834
+ }
65188
65835
  if (hits.length === 0) {
65189
- throw new OperationInputError(`${what}: unknown table "${n}"`, unknownName(n.split(".").pop(), all.map((t) => t.name), "tables", "list_tables lists them.", "graph"));
65836
+ throw new OperationInputError(`${what}: unknown table "${n}"`, unknownName(n.split(".").pop(), known.map((t) => t.name), "tables", "list_tables lists them.", "graph"));
65190
65837
  }
65191
65838
  if (hits.length > 1) {
65192
65839
  throw new OperationInputError(`${what}: "${n}" matches ${hits.map((h) => h.qualified).join(", ")}`, guide(`Name the table in full: ${hits.map((h) => h.qualified).join(" or ")}.`, "graph"));
@@ -65207,6 +65854,7 @@ async function graphTraverse(app, settings, input) {
65207
65854
  const labels = labelSet(input.edges);
65208
65855
  const want = qualifiedTables(app, settings, input.tables, "tables");
65209
65856
  const through = qualifiedTables(app, settings, input.through, "through");
65857
+ const visible = visibleNodes(app, settings);
65210
65858
  const starts = await resolveNodes(app, settings, input.from, "from");
65211
65859
  const via = new Map(starts.map((s) => [s, []]));
65212
65860
  let frontier = starts;
@@ -65227,7 +65875,7 @@ async function graphTraverse(app, settings, input) {
65227
65875
  const next = [];
65228
65876
  for (const node2 of frontier) {
65229
65877
  if (depth > 1 && through && !through.has(g.tableOf.get(node2) ?? "")) continue;
65230
- for (const s of stepsFrom(g, node2, direction, labels)) {
65878
+ for (const s of stepsFrom(g, node2, direction, labels, visible)) {
65231
65879
  if (via.has(s.to)) continue;
65232
65880
  via.set(s.to, [...via.get(node2), { from: node2, label: s.label, direction: s.dir, to: s.to }]);
65233
65881
  next.push(s.to);
@@ -65247,6 +65895,7 @@ async function graphPath(app, settings, input) {
65247
65895
  }
65248
65896
  const g = await graphIndex(app, settings);
65249
65897
  const labels = labelSet(input.edges);
65898
+ const visible = visibleNodes(app, settings);
65250
65899
  const sources = await resolveNodes(app, settings, input.from, "from");
65251
65900
  const targets = new Set(await resolveNodes(app, settings, input.to, "to"));
65252
65901
  const via = new Map(sources.map((s) => [s, []]));
@@ -65256,7 +65905,7 @@ async function graphPath(app, settings, input) {
65256
65905
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
65257
65906
  const next = [];
65258
65907
  for (const node2 of frontier) {
65259
- for (const s of stepsFrom(g, node2, direction, labels)) {
65908
+ for (const s of stepsFrom(g, node2, direction, labels, visible)) {
65260
65909
  if (via.has(s.to)) continue;
65261
65910
  const steps = [...via.get(node2), { from: node2, label: s.label, direction: s.dir, to: s.to }];
65262
65911
  if (targets.has(s.to)) return { found: true, length: depth, via: steps, from: steps[0].from, to: s.to };
@@ -65273,6 +65922,7 @@ async function graphPath(app, settings, input) {
65273
65922
  init_engine();
65274
65923
  init_metastore();
65275
65924
  init_table_registry();
65925
+ init_read_scope();
65276
65926
  var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
65277
65927
  var DATETIME_RE2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/;
65278
65928
  var LINK_RE2 = /^\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]$/;
@@ -65310,14 +65960,16 @@ function tableNameFor(folder) {
65310
65960
  }
65311
65961
  function filesOf(app, settings, input) {
65312
65962
  const out2 = /* @__PURE__ */ new Map();
65963
+ const scope = currentReadScope();
65313
65964
  if (input.folder !== void 0) {
65314
65965
  const prefix = input.folder.replace(/^\/+|\/+$/g, "");
65315
65966
  for (const f of app.vault.getMarkdownFiles()) {
65316
- if (prefix === "" || f.path.startsWith(`${prefix}/`)) out2.set(f.path, f);
65967
+ if ((prefix === "" || f.path.startsWith(`${prefix}/`)) && pathReadable(app, settings, f.path, scope)) out2.set(f.path, f);
65317
65968
  }
65318
65969
  }
65319
65970
  for (const p of input.paths ?? []) {
65320
65971
  const path23 = p.endsWith(".md") ? p : `${p}.md`;
65972
+ assertPathReadable(app, settings, path23);
65321
65973
  const f = app.vault.getAbstractFileByPath(path23);
65322
65974
  if (f && "extension" in f) out2.set(path23, f);
65323
65975
  }
@@ -65355,7 +66007,7 @@ function inferColumns(app, settings, observed, files, maxEnum) {
65355
66007
  col.items = itemKind === "list" ? "string" : itemKind;
65356
66008
  }
65357
66009
  if (col.items === "link") {
65358
- const ref2 = refOf2(app, settings, filled.flatMap((x) => Array.isArray(x.value) ? x.value.map((v) => ({ path: x.path, value: v })) : [x]));
66010
+ const ref2 = refOf3(app, settings, filled.flatMap((x) => Array.isArray(x.value) ? x.value.map((v) => ({ path: x.path, value: v })) : [x]));
65359
66011
  if (ref2) col.ref = ref2;
65360
66012
  }
65361
66013
  } else if (kind === "string") {
@@ -65370,7 +66022,7 @@ function inferColumns(app, settings, observed, files, maxEnum) {
65370
66022
  } else {
65371
66023
  col.type = kind;
65372
66024
  if (kind === "link") {
65373
- const ref2 = refOf2(app, settings, filled);
66025
+ const ref2 = refOf3(app, settings, filled);
65374
66026
  if (ref2) col.ref = ref2;
65375
66027
  }
65376
66028
  }
@@ -65386,7 +66038,7 @@ function frequencyOrder(values) {
65386
66038
  for (const v of values) n.set(v, (n.get(v) ?? 0) + 1);
65387
66039
  return [...n.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([v]) => v);
65388
66040
  }
65389
- function refOf2(app, settings, links) {
66041
+ function refOf3(app, settings, links) {
65390
66042
  const counts = /* @__PURE__ */ new Map();
65391
66043
  let resolved = 0;
65392
66044
  for (const { path: path23, value } of links) {
@@ -65400,7 +66052,7 @@ function refOf2(app, settings, links) {
65400
66052
  if (owner) counts.set(owner, (counts.get(owner) ?? 0) + 1);
65401
66053
  }
65402
66054
  const best = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
65403
- return best && resolved > 0 && best[1] >= resolved * 0.8 ? best[0] : void 0;
66055
+ return best && resolved > 0 && best[1] >= resolved * 0.8 && tableReadable(best[0]) ? best[0] : void 0;
65404
66056
  }
65405
66057
  function frontmatter(app, f) {
65406
66058
  const fm = app.metadataCache.getFileCache(f)?.frontmatter;
@@ -65412,6 +66064,7 @@ async function inferTable(app, settings, input) {
65412
66064
  const notes = [];
65413
66065
  if (input.table !== void 0) {
65414
66066
  const t = await loadTable(app, settings, input.table);
66067
+ assertTableReadable(t.catalog.ref.full);
65415
66068
  const rows = await loadRows(app, settings, t);
65416
66069
  const declared = /* @__PURE__ */ new Set([...t.def.columns.map((c) => c.name), ...t.def.untypedFields]);
65417
66070
  const observed2 = /* @__PURE__ */ new Map();
@@ -65575,7 +66228,8 @@ init_hints();
65575
66228
  var FOLLOW_HINT = (topic) => guide("They run while the engine follows the vault: start it with datanotes serve --vault <folder>.", topic);
65576
66229
  async function listTablesAndViews(app, settings) {
65577
66230
  const ms = await loadMetastore(app, settings);
65578
- const views = ms ? catalogViews(ms.model).map(({ ref: ref2, view }) => ({
66231
+ const scope = currentReadScope();
66232
+ const views = ms ? catalogViews(ms.model).filter(({ ref: ref2 }) => viewReadable(ms.model, ref2.full, scope)).map(({ ref: ref2, view }) => ({
65579
66233
  name: ref2.table,
65580
66234
  catalog: ref2.catalog,
65581
66235
  schema: ref2.schema,
@@ -65583,10 +66237,17 @@ async function listTablesAndViews(app, settings) {
65583
66237
  ...view.description ? { description: view.description } : {},
65584
66238
  ...view.aliases && view.aliases.length > 0 ? { aliases: view.aliases } : {}
65585
66239
  })) : [];
65586
- return { tables: listTables(app, settings.databaseFolder), views };
66240
+ return { tables: listTables(app, settings.databaseFolder).filter((t) => tableReadable(t.qualified, scope)), views };
65587
66241
  }
65588
66242
  async function describeTable(app, settings, table) {
66243
+ const scope = currentReadScope();
66244
+ if (scope) {
66245
+ const model = (await loadMetastore(app, settings))?.model;
66246
+ const view = model && typeof table === "string" && refOf2(model, table, "table") === null ? refOf2(model, table, "view") : null;
66247
+ if (model && view && !viewReadable(model, view, scope)) throw tableReadDenied(scope.token, view);
66248
+ }
65589
66249
  const t = await loadTable(app, settings, table);
66250
+ assertTableReadable(t.catalog.ref.full);
65590
66251
  const { def } = t;
65591
66252
  const o = def.options;
65592
66253
  const e = t.catalog.entry;
@@ -65831,6 +66492,7 @@ init_metastore();
65831
66492
  init_store();
65832
66493
  init_engine();
65833
66494
  init_message();
66495
+ init_read_scope();
65834
66496
  var SHARDS = 16;
65835
66497
  var FORMAT = 1;
65836
66498
  function quantizeValues(v) {
@@ -66151,13 +66813,23 @@ var SemanticIndex = class {
66151
66813
  const q = await this.queryVector(input.query.trim());
66152
66814
  const limit = Math.min(Math.max(1, input.limit ?? 10), 100);
66153
66815
  const folder = input.folder?.replace(/^\/+|\/+$/g, "");
66154
- const wantTable = (t) => !input.tables || input.tables.length === 0 || t !== void 0 && input.tables.some((x) => t === x || t.endsWith(`.${x}`));
66816
+ const scope = currentReadScope();
66817
+ const settings = this.getSettings();
66818
+ if (scope) {
66819
+ const model = cachedMetastore(this.app, settings);
66820
+ for (const name of input.tables ?? []) {
66821
+ const full = model ? rowTableFull(model, name) : null;
66822
+ if (full !== null) assertTableReadable(full);
66823
+ }
66824
+ }
66825
+ const wantTable2 = (t) => !input.tables || input.tables.length === 0 || t !== void 0 && input.tables.some((x) => t === x || t.endsWith(`.${x}`));
66155
66826
  const min = input.minScore;
66156
66827
  const passing = (f) => f.passages.map((p) => ({ p, estimate: estimateOf(q, p) })).filter(({ p, estimate }) => min === void 0 || estimate >= min + SCORE_SLACK || estimate >= min - SCORE_SLACK && scoreOf(q, p) >= min);
66157
66828
  const notes = [];
66158
66829
  for (const [path23, f] of this.files) {
66159
66830
  if (folder && !path23.startsWith(`${folder}/`)) continue;
66160
- if (!wantTable(f.table)) continue;
66831
+ if (!wantTable2(f.table)) continue;
66832
+ if (scope && !pathReadable(this.app, settings, path23, scope)) continue;
66161
66833
  let estimate = -Infinity;
66162
66834
  for (const x of passing(f)) estimate = Math.max(estimate, x.estimate);
66163
66835
  if (estimate > -Infinity) notes.push({ path: path23, f, estimate });
@@ -66613,6 +67285,536 @@ init_actor();
66613
67285
  init_changeset();
66614
67286
  init_engine();
66615
67287
  init_vault_settle();
67288
+
67289
+ // src/core/note-search.ts
67290
+ init_file_cache();
67291
+ init_hints();
67292
+ init_engine();
67293
+ init_metastore();
67294
+ init_read_scope();
67295
+
67296
+ // src/core/vault-read.ts
67297
+ init_file_cache();
67298
+ init_database();
67299
+ init_datetime();
67300
+ init_hints();
67301
+ init_read_scope();
67302
+ var VaultPathNotFoundError = class extends GuidedError {
67303
+ constructor() {
67304
+ super(...arguments);
67305
+ this.name = "VaultPathNotFoundError";
67306
+ }
67307
+ };
67308
+ var VaultPathError = class extends GuidedError {
67309
+ constructor() {
67310
+ super(...arguments);
67311
+ this.name = "VaultPathError";
67312
+ }
67313
+ };
67314
+ var FIND_NOTE = guide("Give the vault path of the note, with .md: list_folder lists a folder, search_notes finds notes by their words.", "readNote");
67315
+ function clean(path23) {
67316
+ return path23.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
67317
+ }
67318
+ function readableFilter(app, settings) {
67319
+ const scope = currentReadScope();
67320
+ if (scope === null) return null;
67321
+ if (!settings) return () => false;
67322
+ return (path23) => pathReadable(app, settings, path23, scope);
67323
+ }
67324
+ async function readNote(app, settings, path23) {
67325
+ const rel = clean(path23);
67326
+ if (rel === "" || /\/$/.test(path23.trim())) {
67327
+ throw new VaultPathError("path must name a file, not a folder", guide('Pass the vault path of a note, e.g. "Films/Metropolis.md"; list_folder lists a folder.', "readNote"));
67328
+ }
67329
+ const f = app.vault.getAbstractFileByPath(rel);
67330
+ if (!f || !("extension" in f)) {
67331
+ if (f) throw new VaultPathError(`${rel} is a folder: list_folder lists it`, FIND_NOTE);
67332
+ throw new VaultPathNotFoundError(`no file ${rel} in the vault`, FIND_NOTE);
67333
+ }
67334
+ const file = f;
67335
+ assertPathReadable(app, settings, file.path);
67336
+ const { content } = await readHashed(app, file);
67337
+ const cache = app.metadataCache.getFileCache(file);
67338
+ const { position: _position, ...frontMatter } = cache?.frontmatter ?? {};
67339
+ const fmTags = frontMatter.tags;
67340
+ const tags = [
67341
+ ...(cache?.tags ?? []).map((t) => t.tag.replace(/^#/, "")),
67342
+ ...Array.isArray(fmTags) ? fmTags.map(String) : typeof fmTags === "string" ? [fmTags] : []
67343
+ ];
67344
+ const stat = file.stat;
67345
+ const tz = (await readDatabaseConfig(app, settings)).timezone;
67346
+ const at = (ms) => typeof ms === "number" && ms > 0 ? isoAt(ms, tz) : null;
67347
+ return {
67348
+ path: file.path,
67349
+ content,
67350
+ front_matter: frontMatter,
67351
+ tags: [...new Set(tags)],
67352
+ modified_at: at(stat?.mtime),
67353
+ created_at: at(stat?.ctime),
67354
+ size: typeof stat?.size === "number" ? stat.size : null
67355
+ };
67356
+ }
67357
+ function listFolder(app, folder, settings) {
67358
+ const rel = clean(folder);
67359
+ const node2 = rel === "" ? app.vault.getRoot?.() ?? app.vault.getAbstractFileByPath("/") : app.vault.getAbstractFileByPath(rel);
67360
+ if (!node2 || !("children" in node2)) {
67361
+ if (node2) throw new VaultPathError(`${rel} is a file: read_note reads it`, guide('Pass a folder of the vault ("" is the vault root); read_note reads a file.', "readNote"));
67362
+ throw new VaultPathNotFoundError(`no folder ${rel} in the vault`, guide('list_folder with folder "" lists the vault root; pass one of its folders.', "readNote"));
67363
+ }
67364
+ const readable = readableFilter(app, settings);
67365
+ let shown = null;
67366
+ if (readable) {
67367
+ shown = /* @__PURE__ */ new Set();
67368
+ const prefix = rel === "" ? "" : `${rel}/`;
67369
+ for (const f of app.vault.getMarkdownFiles()) {
67370
+ if (!f.path.startsWith(prefix) || !readable(f.path)) continue;
67371
+ shown.add(f.path);
67372
+ for (let i = f.path.indexOf("/", prefix.length); i >= 0; i = f.path.indexOf("/", i + 1)) shown.add(f.path.slice(0, i));
67373
+ }
67374
+ }
67375
+ const folders = [];
67376
+ const files = [];
67377
+ for (const child of node2.children) {
67378
+ const name = child.name ?? child.path.split("/").pop();
67379
+ if (name.startsWith(".")) continue;
67380
+ const path23 = rel === "" ? name : `${rel}/${name}`;
67381
+ if (shown && !shown.has(path23)) continue;
67382
+ ("children" in child ? folders : files).push(path23);
67383
+ }
67384
+ return { folder: rel, folders: folders.sort(), files: files.sort() };
67385
+ }
67386
+ function fold5(s) {
67387
+ return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
67388
+ }
67389
+ var searchFold = fold5;
67390
+ var ONE_TO_ONE = /^[\u0000-˿]*$/;
67391
+ var BMP_FOLDED = new Uint8Array(65536).fill(255);
67392
+ function foldedLength(cp, ch) {
67393
+ if (cp > 65535) return fold5(ch).length;
67394
+ let n = BMP_FOLDED[cp];
67395
+ if (n === 255) BMP_FOLDED[cp] = n = fold5(ch).length;
67396
+ return n;
67397
+ }
67398
+ function foldedOffsets(content) {
67399
+ if (ONE_TO_ONE.test(content)) return null;
67400
+ const starts = [];
67401
+ const ends = [];
67402
+ let aligned = true;
67403
+ for (let o = 0; o < content.length; ) {
67404
+ const cp = content.codePointAt(o);
67405
+ const width = cp > 65535 ? 2 : 1;
67406
+ const n = cp < 768 ? 1 : foldedLength(cp, content.slice(o, o + width));
67407
+ if (n !== width) aligned = false;
67408
+ if (n === 0 && ends.length > 0) {
67409
+ for (let k = ends.length - 1; k >= 0 && ends[k] === o; k--) ends[k] = o + width;
67410
+ }
67411
+ for (let k = 0; k < n; k++) {
67412
+ starts.push(o);
67413
+ ends.push(o + width);
67414
+ }
67415
+ o += width;
67416
+ }
67417
+ if (aligned) return null;
67418
+ return {
67419
+ // a match covers at least one folded character: its start and its last character are in the text
67420
+ start: (i) => starts[i],
67421
+ end: (i) => ends[i - 1]
67422
+ };
67423
+ }
67424
+ var SEARCH_CACHE = { maxChars: 64 * 1024 * 1024 };
67425
+ var FOLDED = /* @__PURE__ */ new WeakMap();
67426
+ function foldedCache(app) {
67427
+ let cache = FOLDED.get(app);
67428
+ if (!cache) FOLDED.set(app, cache = { notes: /* @__PURE__ */ new Map(), chars: 0 });
67429
+ return cache;
67430
+ }
67431
+ function keptFolded(app, path23, hash) {
67432
+ const kept2 = foldedCache(app).notes.get(path23);
67433
+ return kept2 && kept2.hash === hash ? kept2 : void 0;
67434
+ }
67435
+ function foldedNote(app, path23, hash, content, keep) {
67436
+ const note = { hash, text: fold5(content), name: fold5(path23) };
67437
+ if (!keep) return note;
67438
+ const cache = foldedCache(app);
67439
+ const kept2 = cache.notes.get(path23);
67440
+ const size = note.text.length + note.name.length;
67441
+ if (kept2) {
67442
+ cache.chars += size - kept2.text.length - kept2.name.length;
67443
+ cache.notes.set(path23, note);
67444
+ } else if (cache.chars + size <= SEARCH_CACHE.maxChars) {
67445
+ cache.chars += size;
67446
+ cache.notes.set(path23, note);
67447
+ }
67448
+ return note;
67449
+ }
67450
+ function forgetFolded(app, present2) {
67451
+ const cache = foldedCache(app);
67452
+ for (const [path23, note] of cache.notes) {
67453
+ if (present2.has(path23)) continue;
67454
+ cache.notes.delete(path23);
67455
+ cache.chars -= note.text.length + note.name.length;
67456
+ }
67457
+ }
67458
+ var SEARCH_SOURCES = /* @__PURE__ */ new WeakMap();
67459
+ function setSearchSource(app, source) {
67460
+ if (source) SEARCH_SOURCES.set(app, source);
67461
+ else SEARCH_SOURCES.delete(app);
67462
+ }
67463
+ function searchCandidates(app, words) {
67464
+ return SEARCH_SOURCES.get(app)?.candidates(words) ?? null;
67465
+ }
67466
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
67467
+ function indexableWord(w) {
67468
+ if (w.indexOf("\0") >= 0 || LONE_SURROGATE.test(w)) return false;
67469
+ let n = 0;
67470
+ for (const _ of w) if (++n >= 3) return true;
67471
+ return false;
67472
+ }
67473
+ var HIDDEN = /(^|\/)\./;
67474
+ var MAX_RESULTS = 200;
67475
+ function scoreOf2(n, words) {
67476
+ let hits = 0;
67477
+ for (const w of words) {
67478
+ for (let i = n.text.indexOf(w); i >= 0 && hits < 20; i = n.text.indexOf(w, i + w.length)) hits++;
67479
+ if (hits >= 20) break;
67480
+ }
67481
+ return hits + words.filter((w) => n.name.includes(w)).length * 5;
67482
+ }
67483
+ function matchesOf(text5, content, words, ctx) {
67484
+ const spans = [];
67485
+ for (const w of words) {
67486
+ for (let i = text5.indexOf(w); i >= 0; i = text5.indexOf(w, i + w.length)) spans.push({ start: i, end: i + w.length });
67487
+ }
67488
+ spans.sort((x, y) => x.start - y.start || y.end - x.end);
67489
+ const merged = [];
67490
+ for (const sp of spans) {
67491
+ const last = merged[merged.length - 1];
67492
+ if (last && (sp.start <= last.end || !/[\p{L}\p{N}]/u.test(text5.slice(last.end, sp.start)))) last.end = Math.max(last.end, sp.end);
67493
+ else merged.push({ ...sp });
67494
+ }
67495
+ const at = merged.length > 0 ? foldedOffsets(content) : null;
67496
+ return merged.slice(0, 20).map((m) => {
67497
+ const start = at ? at.start(m.start) : m.start;
67498
+ const end = at ? at.end(m.end) : m.end;
67499
+ return { start, end, context: content.slice(Math.max(0, start - ctx), end + ctx) };
67500
+ });
67501
+ }
67502
+ function candidateFiles(app, c) {
67503
+ const out2 = [];
67504
+ const seen = /* @__PURE__ */ new Set();
67505
+ for (const list4 of [c.hits, c.unsure]) {
67506
+ for (const p of list4) {
67507
+ if (seen.has(p)) continue;
67508
+ seen.add(p);
67509
+ const f = app.vault.getAbstractFileByPath(p);
67510
+ if (f && "extension" in f && f.extension === "md") out2.push(f);
67511
+ }
67512
+ }
67513
+ return out2;
67514
+ }
67515
+ async function searchNotes(app, query, contextLength = 100, settings) {
67516
+ const words = [...new Set(fold5(query).split(/\s+/).filter(Boolean))];
67517
+ if (words.length === 0) return { query, results: [], total: 0 };
67518
+ const ctx = Number.isFinite(contextLength) && contextLength >= 0 ? Math.min(Math.floor(contextLength), 1e3) : 100;
67519
+ const indexable = words.filter(indexableWord);
67520
+ const narrowed = indexable.length > 0 ? searchCandidates(app, indexable) : null;
67521
+ const files = narrowed ? candidateFiles(app, narrowed) : app.vault.getMarkdownFiles();
67522
+ const keep = narrowed === null;
67523
+ const present2 = keep ? /* @__PURE__ */ new Set() : null;
67524
+ const has2 = (n) => words.every((w) => n.text.includes(w) || n.name.includes(w));
67525
+ const readable = readableFilter(app, settings);
67526
+ const found = [];
67527
+ for (const f of files) {
67528
+ if (HIDDEN.test(f.path)) continue;
67529
+ present2?.add(f.path);
67530
+ if (readable && !readable(f.path)) continue;
67531
+ let content;
67532
+ let folded = keptFolded(app, f.path, await readHash(app, f));
67533
+ if (!folded) {
67534
+ const r = await readHashed(app, f);
67535
+ content = r.content;
67536
+ folded = foldedNote(app, f.path, r.hash, content, keep);
67537
+ }
67538
+ if (!has2(folded)) continue;
67539
+ if (content === void 0) {
67540
+ const r = await readHashed(app, f);
67541
+ content = r.content;
67542
+ if (r.hash !== folded.hash && !has2(folded = foldedNote(app, f.path, r.hash, content, keep))) continue;
67543
+ }
67544
+ found.push({ path: f.path, score: scoreOf2(folded, words), folded, content });
67545
+ }
67546
+ if (present2) forgetFolded(app, present2);
67547
+ found.sort((x, y) => y.score - x.score || x.path.localeCompare(y.path));
67548
+ const results = found.slice(0, MAX_RESULTS).map((r) => ({ path: r.path, score: r.score, matches: matchesOf(r.folded.text, r.content, words, ctx) }));
67549
+ return { query, results, total: found.length };
67550
+ }
67551
+
67552
+ // src/core/note-search.ts
67553
+ init_message();
67554
+ var K12 = 1.2;
67555
+ var B2 = 0.75;
67556
+ var NAME = 3;
67557
+ var FRONT_MATTER = 1;
67558
+ var PAIR = 0.75;
67559
+ var RRF_K2 = 60;
67560
+ var FUSION_DEPTH2 = 50;
67561
+ var MEANING_DEPTH = 100;
67562
+ var SNIPPET = 200;
67563
+ var SEARCH_GUIDE = (hint) => guide(hint, "search");
67564
+ function searchTerms(text5) {
67565
+ return (searchFold(text5).match(/[\p{L}\p{N}]+/gu) ?? []).filter((w) => w.length > 1 || new RegExp("\\p{N}", "u").test(w));
67566
+ }
67567
+ function parseSearchQuery(query) {
67568
+ const words = [];
67569
+ const required2 = [];
67570
+ const excluded = [];
67571
+ const quoted3 = query.replace(/[“”„«»]/g, '"');
67572
+ for (const m of quoted3.matchAll(/([+-]?)(?:"([^"]*)"?|([^\s"]+))/g)) {
67573
+ const [, sign, phrase, word] = m;
67574
+ if (word !== void 0 && (sign === "" || word.startsWith("-"))) {
67575
+ words.push(`${sign}${word}`);
67576
+ continue;
67577
+ }
67578
+ const content = phrase ?? word;
67579
+ const terms = searchTerms(content);
67580
+ if (sign === "-") {
67581
+ if (terms.length > 0) excluded.push(terms);
67582
+ continue;
67583
+ }
67584
+ words.push(content);
67585
+ if (terms.length > 0) required2.push(terms);
67586
+ }
67587
+ return { text: words.join(" "), required: required2, excluded };
67588
+ }
67589
+ function field2(text5) {
67590
+ const terms = searchTerms(text5);
67591
+ const counts = /* @__PURE__ */ new Map();
67592
+ for (const t of terms) counts.set(t, (counts.get(t) ?? 0) + 1);
67593
+ return { terms, counts };
67594
+ }
67595
+ function occurrences2(f, terms) {
67596
+ if (terms.length === 1) return f.counts.get(terms[0]) ?? 0;
67597
+ let n = 0;
67598
+ for (let i = 0; i + terms.length <= f.terms.length; i++) if (terms.every((s, j) => f.terms[i + j] === s)) n++;
67599
+ return n;
67600
+ }
67601
+ function adjacent2(f, a, b) {
67602
+ for (let i = 0; i + 1 < f.terms.length; i++) if (f.terms[i] === a && f.terms[i + 1] === b) return true;
67603
+ return false;
67604
+ }
67605
+ function linkText(text5) {
67606
+ return text5.replace(/\[\[([^\]|#]*)(?:#[^\]|]*)?(?:\|([^\]]*))?\]\]/g, (_, target, alias) => alias ?? target.split("/").pop());
67607
+ }
67608
+ function splitNote(content) {
67609
+ const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(content);
67610
+ return m ? { frontMatter: m[1], body: content.slice(m[0].length) } : { frontMatter: null, body: content };
67611
+ }
67612
+ function frontMatterValues(yaml, tableKey) {
67613
+ const skip = /* @__PURE__ */ new Set(["id", tableKey]);
67614
+ const out2 = [];
67615
+ let key2 = null;
67616
+ for (const line of yaml.split(/\r?\n/)) {
67617
+ const entry = /^([^\s:#-][^:]*):\s*(.*)$/.exec(line);
67618
+ if (entry) {
67619
+ key2 = entry[1].trim().replace(/^["']|["']$/g, "");
67620
+ if (!skip.has(key2) && entry[2].trim() !== "") out2.push(linkText(entry[2].trim()));
67621
+ continue;
67622
+ }
67623
+ if (key2 !== null && !skip.has(key2) && line.trim() !== "") out2.push(linkText(line.trim().replace(/^-\s*/, "")));
67624
+ }
67625
+ return out2;
67626
+ }
67627
+ function fieldsOf(path23, hash, content, tableKey) {
67628
+ const { frontMatter, body } = splitNote(content);
67629
+ const lines = frontMatter === null ? [] : frontMatterValues(frontMatter, tableKey);
67630
+ const name = path23.split("/").pop().replace(/\.md$/i, "");
67631
+ return { hash, name: field2(name), frontMatter: field2(lines.join("\n")), body: field2(linkText(body)), frontMatterLines: lines, bodyText: body };
67632
+ }
67633
+ var SEARCH_TERMS_CACHE = { maxTerms: 8e6 };
67634
+ var KEPT2 = /* @__PURE__ */ new WeakMap();
67635
+ var sizeOf = (n) => n.name.terms.length + n.frontMatter.terms.length + n.body.terms.length;
67636
+ function keptNotes(app) {
67637
+ let kept2 = KEPT2.get(app);
67638
+ if (!kept2) KEPT2.set(app, kept2 = { notes: /* @__PURE__ */ new Map(), terms: 0 });
67639
+ return kept2;
67640
+ }
67641
+ async function noteFields(app, f, tableKey) {
67642
+ const kept2 = keptNotes(app);
67643
+ const old = kept2.notes.get(f.path);
67644
+ if (old && old.hash === await readHash(app, f)) return old;
67645
+ const { content, hash } = await readHashed(app, f);
67646
+ const n = fieldsOf(f.path, hash, content, tableKey);
67647
+ if (old) {
67648
+ kept2.terms += sizeOf(n) - sizeOf(old);
67649
+ kept2.notes.set(f.path, n);
67650
+ } else if (kept2.terms + sizeOf(n) <= SEARCH_TERMS_CACHE.maxTerms) {
67651
+ kept2.terms += sizeOf(n);
67652
+ kept2.notes.set(f.path, n);
67653
+ }
67654
+ return n;
67655
+ }
67656
+ function forgetGone(app, present2) {
67657
+ const kept2 = keptNotes(app);
67658
+ for (const [path23, n] of kept2.notes) {
67659
+ if (present2.has(path23)) continue;
67660
+ kept2.notes.delete(path23);
67661
+ kept2.terms -= sizeOf(n);
67662
+ }
67663
+ }
67664
+ function allows2(n, q) {
67665
+ const holds = (terms) => occurrences2(n.name, terms) + occurrences2(n.frontMatter, terms) + occurrences2(n.body, terms) > 0;
67666
+ return q.required.every(holds) && !q.excluded.some(holds);
67667
+ }
67668
+ var wantTable = (tables, table) => !tables || tables.length === 0 || table !== null && tables.some((x) => table === x || table.endsWith(`.${x}`));
67669
+ function headingAt(body, at) {
67670
+ let heading = "";
67671
+ for (const m of body.slice(0, at).matchAll(/^#{1,6}[ \t]+(.+?)[ \t#]*$/gm)) heading = m[1];
67672
+ return heading;
67673
+ }
67674
+ function around(text5, at) {
67675
+ const start = Math.max(0, at - 60);
67676
+ const cut2 = text5.slice(start, start + SNIPPET).replace(/\s+/g, " ").trim();
67677
+ return `${start > 0 ? "\u2026" : ""}${cut2}${start + SNIPPET < text5.length ? "\u2026" : ""}`;
67678
+ }
67679
+ function firstMatch(text5, terms) {
67680
+ for (const m of text5.matchAll(/[\p{L}\p{M}\p{N}]+/gu)) if (searchTerms(m[0]).some((t) => terms.has(t))) return m.index;
67681
+ return -1;
67682
+ }
67683
+ function excerptOf(n, terms) {
67684
+ const prose = n.bodyText.split("\n").filter((l) => !/^(#{1,6}\s|\s*```|\s*~~~)/.test(l)).join(" ").replace(/\s+/g, " ").trim();
67685
+ const at = firstMatch(n.bodyText, terms);
67686
+ if (at >= 0) return { heading: headingAt(n.bodyText, at), snippet: around(prose, Math.max(0, firstMatch(prose, terms))) };
67687
+ const line = n.frontMatterLines.find((l) => firstMatch(l, terms) >= 0);
67688
+ if (line !== void 0) return { heading: "", snippet: around(line, 0) };
67689
+ return { heading: "", snippet: around(prose, 0) };
67690
+ }
67691
+ function rankByWords(notes, corpus, q, terms) {
67692
+ const df = terms.map((t) => notes.filter((n) => occurrences2(n.fields.name, [t]) + occurrences2(n.fields.frontMatter, [t]) + occurrences2(n.fields.body, [t]) > 0).length);
67693
+ const idf = df.map((d) => Math.log(1 + (corpus - d + 0.5) / (d + 0.5)));
67694
+ const avg = notes.length === 0 ? 1 : Math.max(1, notes.reduce((s, n) => s + n.fields.body.terms.length, 0) / notes.length);
67695
+ const out2 = [];
67696
+ for (const n of notes) {
67697
+ if (!allows2(n.fields, q)) continue;
67698
+ const { name, frontMatter, body } = n.fields;
67699
+ let score = 0;
67700
+ let matched = 0;
67701
+ terms.forEach((t, i) => {
67702
+ const w = idf[i];
67703
+ const count2 = body.counts.get(t) ?? 0;
67704
+ const s = (count2 > 0 ? w * count2 * (K12 + 1) / (count2 + K12 * (1 - B2 + B2 * body.terms.length / avg)) : 0) + (name.counts.has(t) ? NAME * w : 0) + (frontMatter.counts.has(t) ? FRONT_MATTER * w : 0);
67705
+ if (s > 0) matched++;
67706
+ score += s;
67707
+ });
67708
+ if (matched === 0) continue;
67709
+ for (let i = 0; i + 1 < terms.length; i++) {
67710
+ const [a, b] = [terms[i], terms[i + 1]];
67711
+ if (adjacent2(name, a, b) || adjacent2(frontMatter, a, b) || adjacent2(body, a, b)) score += PAIR * Math.min(idf[i], idf[i + 1]);
67712
+ }
67713
+ score *= (matched / terms.length) ** 2;
67714
+ out2.push({ path: n.path, score, fields: n.fields });
67715
+ }
67716
+ return out2.sort((x, y) => y.score - x.score || x.path.localeCompare(y.path));
67717
+ }
67718
+ var HIDDEN2 = /(^|\/)\./;
67719
+ async function searchVault(app, settings, input, semantic) {
67720
+ if (typeof input.query !== "string" || input.query.trim() === "") throw new OperationInputError(`"query" is required`, SEARCH_GUIDE(`Give a few words, for example search({query: "pasta al pomodoro"}).`));
67721
+ await loadMetastore(app, settings);
67722
+ const q = parseSearchQuery(input.query);
67723
+ const terms = [...new Set(searchTerms(q.text))];
67724
+ if (terms.length === 0) {
67725
+ throw new OperationInputError(
67726
+ "search: the query has no words to look for",
67727
+ SEARCH_GUIDE(`Give at least one word to rank by: "-word" only leaves notes out, and single letters are not searched. Example: search({query: "uovo -latte"}).`)
67728
+ );
67729
+ }
67730
+ const limit = Math.min(Math.max(1, Math.floor(input.limit ?? 10)), 100);
67731
+ const folder = input.folder?.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
67732
+ const scope = currentReadScope();
67733
+ if (scope) {
67734
+ const model = cachedMetastore(app, settings);
67735
+ for (const name of input.tables ?? []) {
67736
+ const full = model ? rowTableFull(model, name) : null;
67737
+ if (full !== null) assertTableReadable(full);
67738
+ }
67739
+ }
67740
+ const files = app.vault.getMarkdownFiles();
67741
+ const corpus = /* @__PURE__ */ new Map();
67742
+ for (const f of files) {
67743
+ if (HIDDEN2.test(f.path)) continue;
67744
+ if (folder && !f.path.startsWith(`${folder}/`)) continue;
67745
+ if (scope && !pathReadable(app, settings, f.path, scope)) continue;
67746
+ const table = input.tables && input.tables.length > 0 ? tableOfPath(app, settings, f.path) : null;
67747
+ if (!wantTable(input.tables, table)) continue;
67748
+ corpus.set(f.path, { file: f, table });
67749
+ }
67750
+ let read3 = corpus.values();
67751
+ if (terms.every(indexableWord)) {
67752
+ const named = /* @__PURE__ */ new Set();
67753
+ let all = false;
67754
+ for (const t of terms) {
67755
+ const c = searchCandidates(app, [t]);
67756
+ if (c === null) {
67757
+ all = true;
67758
+ break;
67759
+ }
67760
+ for (const list4 of [c.hits, c.unsure]) for (const p of list4) named.add(p);
67761
+ }
67762
+ if (!all) read3 = [...named].map((p) => corpus.get(p)).filter((c) => c !== void 0);
67763
+ }
67764
+ const notes = [];
67765
+ for (const c of read3) notes.push({ path: c.file.path, fields: await noteFields(app, c.file, settings.tableKey) });
67766
+ forgetGone(app, new Set(files.map((f) => f.path)));
67767
+ const byWords = rankByWords(notes, corpus.size, q, terms);
67768
+ const termSet = new Set(terms);
67769
+ const tableOf = (path23) => corpus.get(path23)?.table ?? tableOfPath(app, settings, path23);
67770
+ const words = (r, score, matchedBy) => {
67771
+ const table = tableOf(r.path);
67772
+ return { path: r.path, ...table ? { table } : {}, score, ...excerptOf(r.fields, termSet), matched_by: matchedBy };
67773
+ };
67774
+ if (!semantic || !settings.semanticEnabled) {
67775
+ return { query: input.query, results: byWords.slice(0, limit).map((r) => words(r, Math.round(r.score * 100) / 100, ["words"])), meaning: "off" };
67776
+ }
67777
+ let meaningHits;
67778
+ try {
67779
+ const r = await semantic.search({
67780
+ query: q.text,
67781
+ limit: MEANING_DEPTH,
67782
+ ...input.tables && input.tables.length > 0 ? { tables: input.tables } : {},
67783
+ ...folder ? { folder } : {},
67784
+ ...input.minScore !== void 0 ? { minScore: input.minScore } : {}
67785
+ });
67786
+ meaningHits = r.results;
67787
+ } catch (e) {
67788
+ if (e instanceof ReadDeniedError || e instanceof OperationInputError) throw e;
67789
+ return { query: input.query, results: byWords.slice(0, limit).map((r) => words(r, Math.round(r.score * 100) / 100, ["words"])), meaning: "failed", meaning_error: messageOf(e) };
67790
+ }
67791
+ const meaning = [];
67792
+ for (const h of meaningHits) {
67793
+ const c = corpus.get(h.path);
67794
+ if (!c) continue;
67795
+ const fields = await noteFields(app, c.file, settings.tableKey);
67796
+ if (!allows2(fields, q)) continue;
67797
+ meaning.push({ path: h.path, heading: h.heading, snippet: h.snippet, fields });
67798
+ if (meaning.length === FUSION_DEPTH2) break;
67799
+ }
67800
+ const fused = /* @__PURE__ */ new Map();
67801
+ byWords.slice(0, FUSION_DEPTH2).forEach((r, i) => fused.set(r.path, { score: 1 / (RRF_K2 + i + 1), wordsRank: i + 1, ranked: r }));
67802
+ meaning.forEach((m, i) => {
67803
+ const f = fused.get(m.path) ?? { score: 0, wordsRank: Infinity };
67804
+ f.score += 1 / (RRF_K2 + i + 1);
67805
+ f.meaning = m;
67806
+ fused.set(m.path, f);
67807
+ });
67808
+ const results = [...fused.entries()].sort(([pa, a], [pb, b]) => b.score - a.score || a.wordsRank - b.wordsRank || pa.localeCompare(pb)).slice(0, limit).map(([path23, f]) => {
67809
+ const score = Math.round(f.score * 1e4) / 1e4;
67810
+ if (f.ranked) return words(f.ranked, score, f.meaning ? ["words", "meaning"] : ["words"]);
67811
+ const table = tableOf(path23);
67812
+ return { path: path23, ...table ? { table } : {}, score, heading: f.meaning.heading, snippet: f.meaning.snippet, matched_by: ["meaning"] };
67813
+ });
67814
+ return { query: input.query, results, meaning: "on" };
67815
+ }
67816
+
67817
+ // src/server/rest.ts
66616
67818
  init_write_check();
66617
67819
  init_query();
66618
67820
  init_journal();
@@ -66890,6 +68092,7 @@ async function rowMovesSince(app, rows) {
66890
68092
  // src/core/change-feed.ts
66891
68093
  init_table_loader();
66892
68094
  init_types_table();
68095
+ init_read_scope();
66893
68096
  init_store();
66894
68097
  init_vault_index();
66895
68098
  init_telemetry();
@@ -67046,6 +68249,24 @@ function matches(ev, q) {
67046
68249
  if (q.excludeActor && ev.actor?.harness === q.excludeActor) return false;
67047
68250
  return true;
67048
68251
  }
68252
+ function eventTable(app, settings, table) {
68253
+ if (table === null) return null;
68254
+ const model = cachedMetastore(app, settings);
68255
+ return (model ? rowTableFull(model, table) : null) ?? table;
68256
+ }
68257
+ function eventForScope(ev, scope, app, settings) {
68258
+ if (scope === null) return ev;
68259
+ if (ev.op === "tool_call") return ev.actor?.auth === scope.token ? ev : null;
68260
+ const table = eventTable(app, settings, ev.table);
68261
+ if (table === null || !tableReadable(table, scope)) return null;
68262
+ const shown = (path23) => path23 === ev.path || path23 === ev.oldPath || pathReadable(app, settings, path23, scope);
68263
+ const violations = ev.violations?.filter((v) => shown(v.path));
68264
+ const cause = ev.cause?.filter((c) => pathReadable(app, settings, c.path, scope));
68265
+ const cut2 = violations !== void 0 && violations.length < ev.violations.length;
68266
+ if (!cut2 && cause?.length === ev.cause?.length) return ev;
68267
+ const { violationsTotal, ...rest } = ev;
68268
+ return { ...rest, ...violations !== void 0 ? { violations } : {}, ...!cut2 && violationsTotal !== void 0 ? { violationsTotal } : {}, ...cause !== void 0 ? { cause } : {} };
68269
+ }
67049
68270
  var STATE_SLICE = 2e3;
67050
68271
  var ChangeFeed = class {
67051
68272
  constructor(app, getSettings, retentionDays = 30) {
@@ -67715,9 +68936,14 @@ var ChangeFeed = class {
67715
68936
  latest: broken.slice(0, limit).map((e) => ({ path: e.path, table: e.table, at: e.at, seq: e.seq, violations: count2(e), message: e.violations[0].message }))
67716
68937
  };
67717
68938
  }
67718
- /** Events after `since` that match the query. */
67719
- async read(q = {}) {
68939
+ /**
68940
+ * Events after `since` that match the query, as the read scope sees them ({@link eventForScope}:
68941
+ * by default the scope of the request that reads; delivery that runs on its own passes its own).
68942
+ */
68943
+ async read(q = {}, scope = currentReadScope()) {
67720
68944
  const since = q.since ?? 0;
68945
+ const s = this.getSettings();
68946
+ const visible = (ev) => eventForScope(ev, scope, this.app, s);
67721
68947
  const limit = Math.min(Math.max(q.limit ?? 500, 1), 5e3);
67722
68948
  let source;
67723
68949
  if (this.buffer.length > 0 && since >= this.buffer[0].seq - 1) source = this.buffer.slice(firstAfter(this.buffer, since));
@@ -67735,7 +68961,7 @@ var ChangeFeed = class {
67735
68961
  if (ev.seq > max) max = ev.seq;
67736
68962
  if (ev.seq <= since) continue;
67737
68963
  source.push(ev);
67738
- if (plain && matches(ev, q)) matched++;
68964
+ if (plain && matches(ev, q) && (scope === null || visible(ev) !== null)) matched++;
67739
68965
  }
67740
68966
  if (events2.length > 0) this.fileMaxSeq.set(f, max);
67741
68967
  if (plain && matched >= limit) break;
@@ -67751,7 +68977,10 @@ var ChangeFeed = class {
67751
68977
  for (let i = 0; i < source.length; i++) {
67752
68978
  const ev = source[i];
67753
68979
  last = ev.seq;
67754
- if (kept2[i] && rowMatches(ev)) events.push(ev);
68980
+ if (kept2[i] && rowMatches(ev)) {
68981
+ const shown = visible(ev);
68982
+ if (shown) events.push(shown);
68983
+ }
67755
68984
  if (events.length >= limit) break;
67756
68985
  }
67757
68986
  if (events.length < limit) last = Math.max(last, Math.min(this.seq, source.length ? source[source.length - 1].seq : since));
@@ -67795,10 +69024,10 @@ var ChangeFeed = class {
67795
69024
  for (const w of [...this.waiters]) w();
67796
69025
  }
67797
69026
  /** Like `read`, but waits up to `waitMs` for a matching event when there is none yet (long poll). */
67798
- async wait(q, waitMs) {
69027
+ async wait(q, waitMs, scope = currentReadScope()) {
67799
69028
  const started = Date.now();
67800
69029
  const deadline = started + Math.min(Math.max(waitMs, 0), 6e4);
67801
- let page2 = await this.read(q);
69030
+ let page2 = await this.read(q, scope);
67802
69031
  let since = page2.last;
67803
69032
  while (page2.events.length === 0 && Date.now() < deadline && this.waitsEnded < started) {
67804
69033
  await new Promise((resolve) => {
@@ -67811,7 +69040,7 @@ var ChangeFeed = class {
67811
69040
  }
67812
69041
  this.waiters.add(done);
67813
69042
  });
67814
- page2 = await this.read({ ...q, since });
69043
+ page2 = await this.read({ ...q, since }, scope);
67815
69044
  since = page2.last;
67816
69045
  }
67817
69046
  return page2;
@@ -67842,246 +69071,18 @@ function changedTables2(prev, next) {
67842
69071
  return catalogTables(after).filter(({ ref: ref2, entry }) => was.get(ref2.full) !== JSON.stringify(entry)).map(({ ref: ref2 }) => ref2.full).sort();
67843
69072
  }
67844
69073
 
67845
- // src/core/vault-read.ts
67846
- init_file_cache();
67847
- init_database();
67848
- init_datetime();
67849
- init_hints();
67850
- var VaultPathNotFoundError = class extends GuidedError {
67851
- constructor() {
67852
- super(...arguments);
67853
- this.name = "VaultPathNotFoundError";
67854
- }
67855
- };
67856
- var VaultPathError = class extends GuidedError {
67857
- constructor() {
67858
- super(...arguments);
67859
- this.name = "VaultPathError";
67860
- }
67861
- };
67862
- var FIND_NOTE = guide("Give the vault path of the note, with .md: list_folder lists a folder, search_notes finds notes by their words.", "readNote");
67863
- function clean(path23) {
67864
- return path23.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
67865
- }
67866
- async function readNote(app, settings, path23) {
67867
- const rel = clean(path23);
67868
- if (rel === "" || /\/$/.test(path23.trim())) {
67869
- throw new VaultPathError("path must name a file, not a folder", guide('Pass the vault path of a note, e.g. "Films/Metropolis.md"; list_folder lists a folder.', "readNote"));
67870
- }
67871
- const f = app.vault.getAbstractFileByPath(rel);
67872
- if (!f || !("extension" in f)) {
67873
- if (f) throw new VaultPathError(`${rel} is a folder: list_folder lists it`, FIND_NOTE);
67874
- throw new VaultPathNotFoundError(`no file ${rel} in the vault`, FIND_NOTE);
67875
- }
67876
- const file = f;
67877
- const { content } = await readHashed(app, file);
67878
- const cache = app.metadataCache.getFileCache(file);
67879
- const { position: _position, ...frontMatter } = cache?.frontmatter ?? {};
67880
- const fmTags = frontMatter.tags;
67881
- const tags = [
67882
- ...(cache?.tags ?? []).map((t) => t.tag.replace(/^#/, "")),
67883
- ...Array.isArray(fmTags) ? fmTags.map(String) : typeof fmTags === "string" ? [fmTags] : []
67884
- ];
67885
- const stat = file.stat;
67886
- const tz = (await readDatabaseConfig(app, settings)).timezone;
67887
- const at = (ms) => typeof ms === "number" && ms > 0 ? isoAt(ms, tz) : null;
67888
- return {
67889
- path: file.path,
67890
- content,
67891
- front_matter: frontMatter,
67892
- tags: [...new Set(tags)],
67893
- modified_at: at(stat?.mtime),
67894
- created_at: at(stat?.ctime),
67895
- size: typeof stat?.size === "number" ? stat.size : null
67896
- };
67897
- }
67898
- function listFolder(app, folder) {
67899
- const rel = clean(folder);
67900
- const node2 = rel === "" ? app.vault.getRoot?.() ?? app.vault.getAbstractFileByPath("/") : app.vault.getAbstractFileByPath(rel);
67901
- if (!node2 || !("children" in node2)) {
67902
- if (node2) throw new VaultPathError(`${rel} is a file: read_note reads it`, guide('Pass a folder of the vault ("" is the vault root); read_note reads a file.', "readNote"));
67903
- throw new VaultPathNotFoundError(`no folder ${rel} in the vault`, guide('list_folder with folder "" lists the vault root; pass one of its folders.', "readNote"));
67904
- }
67905
- const folders = [];
67906
- const files = [];
67907
- for (const child of node2.children) {
67908
- const name = child.name ?? child.path.split("/").pop();
67909
- if (name.startsWith(".")) continue;
67910
- const path23 = rel === "" ? name : `${rel}/${name}`;
67911
- ("children" in child ? folders : files).push(path23);
67912
- }
67913
- return { folder: rel, folders: folders.sort(), files: files.sort() };
67914
- }
67915
- function fold5(s) {
67916
- return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
67917
- }
67918
- var searchFold = fold5;
67919
- var ONE_TO_ONE = /^[\u0000-˿]*$/;
67920
- var BMP_FOLDED = new Uint8Array(65536).fill(255);
67921
- function foldedLength(cp, ch) {
67922
- if (cp > 65535) return fold5(ch).length;
67923
- let n = BMP_FOLDED[cp];
67924
- if (n === 255) BMP_FOLDED[cp] = n = fold5(ch).length;
67925
- return n;
67926
- }
67927
- function foldedOffsets(content) {
67928
- if (ONE_TO_ONE.test(content)) return null;
67929
- const starts = [];
67930
- const ends = [];
67931
- let aligned = true;
67932
- for (let o = 0; o < content.length; ) {
67933
- const cp = content.codePointAt(o);
67934
- const width = cp > 65535 ? 2 : 1;
67935
- const n = cp < 768 ? 1 : foldedLength(cp, content.slice(o, o + width));
67936
- if (n !== width) aligned = false;
67937
- if (n === 0 && ends.length > 0) {
67938
- for (let k = ends.length - 1; k >= 0 && ends[k] === o; k--) ends[k] = o + width;
67939
- }
67940
- for (let k = 0; k < n; k++) {
67941
- starts.push(o);
67942
- ends.push(o + width);
67943
- }
67944
- o += width;
67945
- }
67946
- if (aligned) return null;
67947
- return {
67948
- // a match covers at least one folded character: its start and its last character are in the text
67949
- start: (i) => starts[i],
67950
- end: (i) => ends[i - 1]
67951
- };
67952
- }
67953
- var SEARCH_CACHE = { maxChars: 64 * 1024 * 1024 };
67954
- var FOLDED = /* @__PURE__ */ new WeakMap();
67955
- function foldedCache(app) {
67956
- let cache = FOLDED.get(app);
67957
- if (!cache) FOLDED.set(app, cache = { notes: /* @__PURE__ */ new Map(), chars: 0 });
67958
- return cache;
67959
- }
67960
- function keptFolded(app, path23, hash) {
67961
- const kept2 = foldedCache(app).notes.get(path23);
67962
- return kept2 && kept2.hash === hash ? kept2 : void 0;
67963
- }
67964
- function foldedNote(app, path23, hash, content, keep) {
67965
- const note = { hash, text: fold5(content), name: fold5(path23) };
67966
- if (!keep) return note;
67967
- const cache = foldedCache(app);
67968
- const kept2 = cache.notes.get(path23);
67969
- const size = note.text.length + note.name.length;
67970
- if (kept2) {
67971
- cache.chars += size - kept2.text.length - kept2.name.length;
67972
- cache.notes.set(path23, note);
67973
- } else if (cache.chars + size <= SEARCH_CACHE.maxChars) {
67974
- cache.chars += size;
67975
- cache.notes.set(path23, note);
67976
- }
67977
- return note;
67978
- }
67979
- function forgetFolded(app, present2) {
67980
- const cache = foldedCache(app);
67981
- for (const [path23, note] of cache.notes) {
67982
- if (present2.has(path23)) continue;
67983
- cache.notes.delete(path23);
67984
- cache.chars -= note.text.length + note.name.length;
67985
- }
67986
- }
67987
- var SEARCH_SOURCES = /* @__PURE__ */ new WeakMap();
67988
- function setSearchSource(app, source) {
67989
- if (source) SEARCH_SOURCES.set(app, source);
67990
- else SEARCH_SOURCES.delete(app);
67991
- }
67992
- var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
67993
- function indexableWord(w) {
67994
- if (w.indexOf("\0") >= 0 || LONE_SURROGATE.test(w)) return false;
67995
- let n = 0;
67996
- for (const _ of w) if (++n >= 3) return true;
67997
- return false;
67998
- }
67999
- var HIDDEN = /(^|\/)\./;
68000
- var MAX_RESULTS = 200;
68001
- function scoreOf2(n, words) {
68002
- let hits = 0;
68003
- for (const w of words) {
68004
- for (let i = n.text.indexOf(w); i >= 0 && hits < 20; i = n.text.indexOf(w, i + w.length)) hits++;
68005
- if (hits >= 20) break;
68006
- }
68007
- return hits + words.filter((w) => n.name.includes(w)).length * 5;
68008
- }
68009
- function matchesOf(text5, content, words, ctx) {
68010
- const spans = [];
68011
- for (const w of words) {
68012
- for (let i = text5.indexOf(w); i >= 0; i = text5.indexOf(w, i + w.length)) spans.push({ start: i, end: i + w.length });
68013
- }
68014
- spans.sort((x, y) => x.start - y.start || y.end - x.end);
68015
- const merged = [];
68016
- for (const sp of spans) {
68017
- const last = merged[merged.length - 1];
68018
- if (last && (sp.start <= last.end || !/[\p{L}\p{N}]/u.test(text5.slice(last.end, sp.start)))) last.end = Math.max(last.end, sp.end);
68019
- else merged.push({ ...sp });
68020
- }
68021
- const at = merged.length > 0 ? foldedOffsets(content) : null;
68022
- return merged.slice(0, 20).map((m) => {
68023
- const start = at ? at.start(m.start) : m.start;
68024
- const end = at ? at.end(m.end) : m.end;
68025
- return { start, end, context: content.slice(Math.max(0, start - ctx), end + ctx) };
68026
- });
68027
- }
68028
- function candidateFiles(app, c) {
68029
- const out2 = [];
68030
- const seen = /* @__PURE__ */ new Set();
68031
- for (const list4 of [c.hits, c.unsure]) {
68032
- for (const p of list4) {
68033
- if (seen.has(p)) continue;
68034
- seen.add(p);
68035
- const f = app.vault.getAbstractFileByPath(p);
68036
- if (f && "extension" in f && f.extension === "md") out2.push(f);
68037
- }
68038
- }
68039
- return out2;
68040
- }
68041
- async function searchNotes(app, query, contextLength = 100) {
68042
- const words = [...new Set(fold5(query).split(/\s+/).filter(Boolean))];
68043
- if (words.length === 0) return { query, results: [], total: 0 };
68044
- const ctx = Number.isFinite(contextLength) && contextLength >= 0 ? Math.min(Math.floor(contextLength), 1e3) : 100;
68045
- const indexable = words.filter(indexableWord);
68046
- const narrowed = indexable.length > 0 ? SEARCH_SOURCES.get(app)?.candidates(indexable) ?? null : null;
68047
- const files = narrowed ? candidateFiles(app, narrowed) : app.vault.getMarkdownFiles();
68048
- const keep = narrowed === null;
68049
- const present2 = keep ? /* @__PURE__ */ new Set() : null;
68050
- const has2 = (n) => words.every((w) => n.text.includes(w) || n.name.includes(w));
68051
- const found = [];
68052
- for (const f of files) {
68053
- if (HIDDEN.test(f.path)) continue;
68054
- present2?.add(f.path);
68055
- let content;
68056
- let folded = keptFolded(app, f.path, await readHash(app, f));
68057
- if (!folded) {
68058
- const r = await readHashed(app, f);
68059
- content = r.content;
68060
- folded = foldedNote(app, f.path, r.hash, content, keep);
68061
- }
68062
- if (!has2(folded)) continue;
68063
- if (content === void 0) {
68064
- const r = await readHashed(app, f);
68065
- content = r.content;
68066
- if (r.hash !== folded.hash && !has2(folded = foldedNote(app, f.path, r.hash, content, keep))) continue;
68067
- }
68068
- found.push({ path: f.path, score: scoreOf2(folded, words), folded, content });
68069
- }
68070
- if (present2) forgetFolded(app, present2);
68071
- found.sort((x, y) => y.score - x.score || x.path.localeCompare(y.path));
68072
- const results = found.slice(0, MAX_RESULTS).map((r) => ({ path: r.path, score: r.score, matches: matchesOf(r.folded.text, r.content, words, ctx) }));
68073
- return { query, results, total: found.length };
68074
- }
68075
-
68076
69074
  // src/server/rest.ts
68077
69075
  init_message();
68078
69076
  init_hints();
68079
69077
  init_guidance();
68080
69078
  init_requests();
68081
69079
  init_actor();
69080
+ init_read_scope();
69081
+ init_changeset_checks();
68082
69082
 
68083
69083
  // src/server/token-routes.ts
68084
69084
  init_catalog();
69085
+ init_hints();
68085
69086
  var READ_OPERATIONS = new Set(OPERATIONS.filter((o) => o.annotations.readOnlyHint === true).map((o) => o.name));
68086
69087
  var SCHEMA_OPERATIONS = new Set(Object.keys(OPERATION_KIND).filter((op2) => OPERATION_KIND[op2] === "schema"));
68087
69088
  var ROW_WRITES = {
@@ -68095,14 +69096,25 @@ var ROW_WRITES = {
68095
69096
  recompute_columns: ["table"],
68096
69097
  align_relations: ["table"]
68097
69098
  };
69099
+ var READ_TABLE_ARGS = {
69100
+ select_rows: ["table"],
69101
+ describe_table: ["table"],
69102
+ check_table: ["table"],
69103
+ infer_table: ["table"],
69104
+ list_link_targets: ["table"],
69105
+ table_changes: ["table"],
69106
+ describe_history: ["table"]
69107
+ };
68098
69108
  var OWNER_OPERATIONS = /* @__PURE__ */ new Set(["create_webhook", "delete_webhook"]);
68099
69109
  var READ_ROUTES = /* @__PURE__ */ new Set(["/manual/search/", "/tokens/me/", "/tokens/update/", "/extensions/", "/openapi/", "/openapi.json/", "/sessions/identify/", "/tool-calls/record/"]);
68100
69110
  var OWNER_ADMIN_ROUTES = /* @__PURE__ */ new Set(["/tokens/", "/tokens/create/", "/tokens/update/", "/tokens/delete/", "/tokens/rotate/", "/extensions/record/", "/extensions/delete/", "/status/", "/stop/"]);
69111
+ var READ_SCOPE_UNSUPPORTED = "read permissions need Node's AsyncLocalStorage";
69112
+ var READ_SCOPE_UNSUPPORTED_HINT = guide(`This token has "read" patterns, which this runtime cannot enforce: run the engine on Node (datanotes serve), or have the owner pass "read": null to tokens/update/ for this token.`, "tokenRead");
68101
69113
 
68102
69114
  // src/server/scheduler.ts
68103
69115
  init_telemetry();
68104
69116
  var FREE_WRITES = /* @__PURE__ */ new Set(["sync_semantic_index"]);
68105
- var FREE_READS = /* @__PURE__ */ new Set(["semantic_search"]);
69117
+ var FREE_READS = /* @__PURE__ */ new Set(["semantic_search", "search"]);
68106
69118
  var MAX_WRITES_AHEAD = 8;
68107
69119
  var WRITE_SLOT = { maxMs: 6e4 };
68108
69120
  function operationClass(op2, body = {}) {
@@ -68222,7 +69234,7 @@ var Forbidden = class extends GuidedError {
68222
69234
  }
68223
69235
  };
68224
69236
  var FORBIDDEN_HINT = guide(
68225
- 'Named tokens read everything; writes need a matching "write" pattern, schema changes "ddl": true, maintenance "maintain": true. Ask the owner to update the token (tokens/update/), or use a token that has the permission.',
69237
+ 'Named tokens read the tables their "read" patterns name (every table without "read"); writes need a matching "write" pattern, schema changes "ddl": true, maintenance "maintain": true. Ask the owner to update the token (tokens/update/), or use a token that has the permission.',
68226
69238
  "tokens"
68227
69239
  );
68228
69240
  function operationOf(route) {
@@ -68232,7 +69244,7 @@ function operationOf(route) {
68232
69244
  function argumentsGuidance(op2, route) {
68233
69245
  if (op2) return { hint: `Check the arguments of ${op2}: ${argumentsOf(op2)}.`, docs: toolDocs(op2) };
68234
69246
  if (route?.startsWith("/tokens/")) {
68235
- return guide(`The token routes take {"name"} and, to create or update one, "description" (text), "write" (table patterns), "ddl" and "maintain" (true or false), "autolink" (true or false: the default of its writes for automatic links).`, "manageTokens");
69247
+ return guide(`The token routes take {"name"} and, to create or update one, "description" (text), "read" (table patterns it may read; on update, null for every table), "write" (table patterns it may write, each one readable), "ddl" and "maintain" (true or false), "autolink" (true or false: the default of its writes for automatic links).`, "manageTokens");
68236
69248
  }
68237
69249
  return guide("Check the request's fields against the route in GET /datanotes/openapi.json.", "routes");
68238
69250
  }
@@ -68286,9 +69298,54 @@ function runOpts(body) {
68286
69298
  function kindsArg(body) {
68287
69299
  return strings(body, "kinds")?.map(opOfKind);
68288
69300
  }
68289
- function opResponse(r) {
68290
- if (r.ok) return { status: 200, json: r };
68291
- return { status: writeStatus(r.failed.length > 0, r.rolled_back), json: { ...r, ...refusalGuidance(r) } };
69301
+ function writeResponse(r, app, settings) {
69302
+ const answer = redactWriteAnswer(r, app, settings);
69303
+ if (r.ok) return { status: 200, json: answer };
69304
+ return { status: writeStatus(r.failed.length > 0, r.rolled_back), json: { ...answer, ...refusalGuidance(answer) } };
69305
+ }
69306
+ function redactWriteAnswer(r, app, settings) {
69307
+ const scope = currentReadScope();
69308
+ if (!scope) return r;
69309
+ const hidden2 = (path23) => pathHidden(app, settings, path23, scope);
69310
+ const blocked = /* @__PURE__ */ new Set();
69311
+ const keep = (list4, pathOf) => list4.filter((x) => {
69312
+ const p = pathOf(x);
69313
+ if (!hidden2(p)) return true;
69314
+ blocked.add(p);
69315
+ return false;
69316
+ });
69317
+ const conflicts = keep(r.conflicts, (c) => c.path).map((c) => ({ path: c.path, message: c.message }));
69318
+ const violations = keep(r.violations, (v) => v.path).map((v) => ({ path: v.path, violations: v.violations }));
69319
+ const failed2 = keep(r.failed, (f) => f.path).map((f) => ({ path: f.path, error: f.error }));
69320
+ if (blocked.size > 0) {
69321
+ const n = blocked.size;
69322
+ conflicts.push({ path: "", message: `${n} ${n === 1 ? "row" : "rows"} this token may not read ${n === 1 ? "blocks" : "block"} this write` });
69323
+ }
69324
+ const links = r.links_added?.filter((l) => !hidden2(l.path) && !hidden2(l.target)).map((l) => ({ path: l.path, text: l.text, target: l.target, link: l.link }));
69325
+ const mentions = r.ambiguous_mentions?.filter((m) => !hidden2(m.path)).map((m) => ({ path: m.path, text: m.text, reason: m.reason, candidates: m.candidates.filter((c) => !hidden2(c)), context: m.context, hint: m.hint })).filter((m) => m.candidates.length > 0);
69326
+ return {
69327
+ ok: r.ok,
69328
+ op: r.op,
69329
+ dry_run: r.dry_run,
69330
+ ...r.migration_id !== void 0 ? { migration_id: r.migration_id } : {},
69331
+ notes_changed: r.notes_changed,
69332
+ files_created: r.files_created,
69333
+ files_moved: r.files_moved,
69334
+ files_deleted: r.files_deleted,
69335
+ references_updated: r.references_updated,
69336
+ conflicts,
69337
+ violations,
69338
+ failed: failed2,
69339
+ ...r.rolled_back !== void 0 ? { rolled_back: r.rolled_back } : {},
69340
+ planned_edits: r.planned_edits.filter((e) => !hidden2(e.path)).map((e) => ({ path: e.path, set: e.set, remove: e.remove, ...e.body !== void 0 ? { body: e.body } : {} })),
69341
+ planned_creates: r.planned_creates.filter((p) => !hidden2(p)),
69342
+ planned_moves: r.planned_moves.filter((m) => !hidden2(m.from) && !hidden2(m.to)).map((m) => ({ from: m.from, to: m.to })),
69343
+ planned_deletes: r.planned_deletes.filter((p) => !hidden2(p)),
69344
+ planned_texts: [...r.planned_texts],
69345
+ ...r.commits ? { commits: r.commits.filter((c) => tableReadable(c.table, scope)).map((c) => ({ table: c.table, version: c.version, operation: c.operation })) } : {},
69346
+ ...links && links.length > 0 ? { links_added: links } : {},
69347
+ ...mentions && mentions.length > 0 ? { ambiguous_mentions: mentions } : {}
69348
+ };
68292
69349
  }
68293
69350
  function writeStatus(failed2, rolledBack) {
68294
69351
  return !failed2 ? 422 : rolledBack === true ? 503 : 500;
@@ -68300,6 +69357,7 @@ function errorResponse(e, op2, route) {
68300
69357
  if (e instanceof TableNotFoundError) return { status: 404, json: body(guide("list_tables lists the tables and views, with their descriptions and aliases.", "findTable")) };
68301
69358
  if (e instanceof MigrationNotFoundError) return { status: 404, json: body(guide("list_migrations lists the journal ids (migration_id) of the writes, newest first.", "undo")) };
68302
69359
  if (e instanceof VaultPathNotFoundError) return { status: 404, json: body(guide("list_folder lists a folder, search_notes finds notes by their words.", "readNote")) };
69360
+ if (e instanceof ReadDeniedError) return { status: 403, json: body(READ_DENIED_GUIDANCE) };
68303
69361
  if (e instanceof Forbidden) return { status: 403, json: body(FORBIDDEN_HINT) };
68304
69362
  if (e instanceof AmbiguousCatalogRefError) {
68305
69363
  return { status: 409, json: { ...body(guide(`Name the table in full (catalog.schema.table): ${e.candidates.join(" or ")}.`, "catalogs")), tables: e.candidates } };
@@ -68312,6 +69370,26 @@ function errorResponse(e, op2, route) {
68312
69370
  function stepFailure(json3) {
68313
69371
  return Object.assign(new Error(json3.error), guidanceOf(json3));
68314
69372
  }
69373
+ function redactBatchRollback(r, app, settings) {
69374
+ const scope = currentReadScope();
69375
+ if (!scope || !r.rollback) return r;
69376
+ const shown = (path23) => !pathHidden(app, settings, path23, scope);
69377
+ return {
69378
+ ok: r.ok,
69379
+ op: r.op,
69380
+ dry_run: r.dry_run,
69381
+ ...r.migration_id !== void 0 ? { migration_id: r.migration_id } : {},
69382
+ steps: r.steps,
69383
+ ...r.failed_at !== void 0 ? { failed_at: r.failed_at } : {},
69384
+ ...r.rolled_back !== void 0 ? { rolled_back: r.rolled_back } : {},
69385
+ rollback: r.rollback.map((u) => ({
69386
+ migration_id: u.migration_id,
69387
+ ok: u.ok,
69388
+ conflicts: u.conflicts.filter((c) => shown(c.path)).map((c) => ({ path: c.path, message: c.message })),
69389
+ failed: u.failed.filter((f) => shown(f.path)).map((f) => ({ path: f.path, error: f.error }))
69390
+ }))
69391
+ };
69392
+ }
68315
69393
  function batchGuidance(r) {
68316
69394
  if (r.dry_run) return guide("Fix the operations marked refused, then send the batch again (dry_run checks each one against the vault as it is now).", "batch");
68317
69395
  if (r.rolled_back === false) return guide('Some operations could not be undone: "rollback" lists them; undo them with undo_migration, or repair the rows, before you retry.', "batch");
@@ -68356,6 +69434,7 @@ function checkArgs(op2, args) {
68356
69434
  function createHandlers(app, getSettings, saveSettings = async () => {
68357
69435
  }, semantic, api, options = {}) {
68358
69436
  const s = getSettings;
69437
+ const opResponse = (r) => writeResponse(r, app, s());
68359
69438
  const pub = api ?? createPublicApi(app, getSettings, saveSettings, semantic);
68360
69439
  const needSemantic = () => {
68361
69440
  if (!semantic) throw new BadRequest("semantic search is not available", SEMANTIC_OFF);
@@ -68713,7 +69792,8 @@ function createHandlers(app, getSettings, saveSettings = async () => {
68713
69792
  });
68714
69793
  const failedStep = r.failed_at !== void 0 && (r.steps[r.failed_at]?.result?.failed.length ?? 0) > 0;
68715
69794
  const status = r.ok ? 200 : r.rolled_back === false ? 500 : writeStatus(failedStep, true);
68716
- return { status, json: r.ok ? r : { ...r, ...batchGuidance(r) } };
69795
+ const answer = redactBatchRollback(r, app, s());
69796
+ return { status, json: answer.ok ? answer : { ...answer, ...batchGuidance(answer) } };
68717
69797
  },
68718
69798
  // --- queries ---
68719
69799
  select_rows: async (b) => {
@@ -68758,10 +69838,10 @@ function createHandlers(app, getSettings, saveSettings = async () => {
68758
69838
  };
68759
69839
  },
68760
69840
  read_note: async (b) => ({ status: 200, json: await readNote(app, s(), text3(b, "path")) }),
68761
- list_folder: async (b) => ({ status: 200, json: listFolder(app, text3(b, "folder") ?? "") }),
69841
+ list_folder: async (b) => ({ status: 200, json: listFolder(app, text3(b, "folder") ?? "", s()) }),
68762
69842
  search_notes: async (b) => {
68763
69843
  const ctx = optInt(b, "context_length");
68764
- return { status: 200, json: await searchNotes(app, str(b, "query"), ctx ?? 100) };
69844
+ return { status: 200, json: await searchNotes(app, str(b, "query"), ctx ?? 100, s()) };
68765
69845
  },
68766
69846
  semantic_search: async (b) => {
68767
69847
  const sem = needSemantic();
@@ -68778,6 +69858,20 @@ function createHandlers(app, getSettings, saveSettings = async () => {
68778
69858
  })
68779
69859
  };
68780
69860
  },
69861
+ search: async (b) => {
69862
+ const limit = optInt(b, "limit");
69863
+ const minScore = b.min_score;
69864
+ return {
69865
+ status: 200,
69866
+ json: await searchVault(app, s(), {
69867
+ query: str(b, "query"),
69868
+ ...limit !== void 0 ? { limit } : {},
69869
+ ...strings(b, "tables") ? { tables: strings(b, "tables") } : {},
69870
+ ...str(b, "folder") !== void 0 ? { folder: str(b, "folder") } : {},
69871
+ ...typeof minScore === "number" ? { minScore } : {}
69872
+ }, semantic)
69873
+ };
69874
+ },
68781
69875
  graph_neighbors: async (b) => ({
68782
69876
  status: 200,
68783
69877
  json: await graphNeighbors(app, s(), {
@@ -69161,10 +70255,12 @@ var STATUS_TEXT = {
69161
70255
  404: "Unknown table, view, migration, note or folder.",
69162
70256
  409: "Ambiguous table name: it matches several tables (`paths` or `tables` lists them)."
69163
70257
  };
70258
+ var READ_DENIED_TEXT = "A named token whose `read` patterns do not cover the table, view or note the request names (such as `token cook may not read personal.health.weight`). Sets it did not name (listings, bases, search, the graph, the change feed) are filtered instead.";
69164
70259
  var ERRORS = {
69165
70260
  400: { description: STATUS_TEXT[400], content: json(ref("Error")) },
69166
70261
  401: { $ref: "#/components/responses/Unauthorized" },
69167
70262
  403: { description: STATUS_TEXT[403], content: json(ref("Error")) },
70263
+ readDenied: { description: READ_DENIED_TEXT, content: json(ref("Error")) },
69168
70264
  404: { description: STATUS_TEXT[404], content: json(ref("Error")) },
69169
70265
  409: { description: STATUS_TEXT[409], content: json(ref("AmbiguousName")) },
69170
70266
  500: { $ref: "#/components/responses/ServerError" }
@@ -69174,6 +70270,7 @@ function postRoute(d) {
69174
70270
  const op2 = operationOf(d.route);
69175
70271
  const read3 = op2 !== void 0 ? READ_OPERATIONS.has(op2) : READ_ROUTES.has(d.route) && d.route !== "/tokens/update/";
69176
70272
  if (!read3) responses["403"] = ERRORS[403];
70273
+ else if (op2 !== void 0) responses["403"] = ERRORS.readDenied;
69177
70274
  if (d.namesTable) responses["404"] = ERRORS[404];
69178
70275
  const refused2 = op2 !== void 0 ? refusedStatus(op2) : void 0;
69179
70276
  if (refused2 === 409) {
@@ -69213,6 +70310,7 @@ var TOKEN_ROUTES = {
69213
70310
  properties: {
69214
70311
  name: { type: "string", pattern: "^[a-z0-9][a-z0-9-]*$", description: "Kebab-case; the journal records it as `auth`. `owner` is reserved." },
69215
70312
  description: { type: "string" },
70313
+ read: { type: "array", items: { type: "string" }, description: "Tables the token may read, as patterns like `write`'s. Absent (or with `*`): every table. It must cover every `write` pattern, and stay absent with `ddl` or `maintain` (400 otherwise)." },
69216
70314
  write: { type: "array", items: { type: "string" }, description: "Tables the token may write: catalog.schema.table, catalog.schema.*, catalog.* or *." },
69217
70315
  ddl: { type: "boolean", description: "May the token change the schema?" },
69218
70316
  maintain: { type: "boolean", description: "May the token also undo, restore, vacuum, configure and rebuild the semantic index? (For an agent's own token.)" },
@@ -69223,12 +70321,13 @@ var TOKEN_ROUTES = {
69223
70321
  writes: true
69224
70322
  },
69225
70323
  "/tokens/update/": {
69226
- summary: "Change a named token. A token may change its own description and autolink default; permissions are the owner's.",
70324
+ summary: "Change a named token. A token may change its own description and autolink default; permissions (`read`, `write`, `ddl`, `maintain`) are the owner's.",
69227
70325
  body: {
69228
70326
  type: "object",
69229
70327
  properties: {
69230
70328
  name: { type: "string", description: "Owner only (a named token updates itself)." },
69231
70329
  description: { type: "string", description: "An empty string removes it." },
70330
+ read: { type: ["array", "null"], items: { type: "string" }, description: "The tables the token may read; null reads every table again. Checked against `write`, `ddl` and `maintain` as on create." },
69232
70331
  write: { type: "array", items: { type: "string" } },
69233
70332
  ddl: { type: "boolean" },
69234
70333
  maintain: { type: "boolean" },
@@ -69402,11 +70501,12 @@ var SCHEMAS = {
69402
70501
  Token: {
69403
70502
  type: "object",
69404
70503
  description: "A named token as the registry shows it (never its value).",
69405
- required: ["name", "write", "ddl", "created_at", "updated_at"],
70504
+ required: ["name", "write", "read", "ddl", "created_at", "updated_at"],
69406
70505
  properties: {
69407
70506
  name: { type: "string" },
69408
70507
  description: { type: "string" },
69409
70508
  write: { type: "array", items: { type: "string" } },
70509
+ read: { type: "array", items: { type: "string" }, description: 'The tables it may read (patterns like `write`\'s); `["*"]` when it reads every table.' },
69410
70510
  ddl: { type: "boolean" },
69411
70511
  maintain: { type: "boolean" },
69412
70512
  autolink: { type: "boolean", description: "The default of its writes for automatic links (absent: the database's setting)." },
@@ -69657,7 +70757,7 @@ function openApiDocument(version2) {
69657
70757
  info: {
69658
70758
  title: "Datanotes",
69659
70759
  version: version2,
69660
- description: "Database of Markdown notes: tables of notes, rows, queries, history, a change feed and the vault's notes.\n\n**Operations**: every operation is `POST /datanotes/operations/<name>/`, its body the operation's arguments (the same names as the MCP tool of the same name). An unknown argument is refused (400) with the closest real one.\n\n**Authentication**: every route but `GET /`, this document and `/datanotes/docs` needs `Authorization: Bearer <token>`: the owner's token (`httpServerToken` in the settings: everything) or a named token (`/datanotes/tokens/create/`: reads everything; writes the tables in its `write` patterns, changes the schema with `ddl`; the engine's administration stays the owner's: 403 otherwise).\n\n**Actor**: send `X-Datanotes-Actor: {\"harness\": \u2026, \"model\": \u2026, \"session\": \u2026}` (JSON) to record on whose behalf a write is made; the journal adds the credential (`auth`: `owner`, or the named token's name), which the client cannot claim.\n\n**Errors**: `{error}` with 400 (bad input), 401 (missing or wrong token), 403 (a named token beyond its permissions), 404 (unknown table, view, migration or note), 409 (ambiguous table name), 500. A refused write answers 422 (409 for undo) with the operation's result, `ok: false`: nothing was written."
70760
+ description: "Database of Markdown notes: tables of notes, rows, queries, history, a change feed and the vault's notes.\n\n**Operations**: every operation is `POST /datanotes/operations/<name>/`, its body the operation's arguments (the same names as the MCP tool of the same name). An unknown argument is refused (400) with the closest real one.\n\n**Authentication**: every route but `GET /`, this document and `/datanotes/docs` needs `Authorization: Bearer <token>`: the owner's token (`httpServerToken` in the settings: everything) or a named token (`/datanotes/tokens/create/`: reads the tables in its `read` patterns, every table without them; writes the tables in its `write` patterns, changes the schema with `ddl`; the engine's administration stays the owner's: 403 otherwise).\n\n**Actor**: send `X-Datanotes-Actor: {\"harness\": \u2026, \"model\": \u2026, \"session\": \u2026}` (JSON) to record on whose behalf a write is made; the journal adds the credential (`auth`: `owner`, or the named token's name), which the client cannot claim.\n\n**Errors**: `{error}` with 400 (bad input), 401 (missing or wrong token), 403 (a named token beyond its permissions, or a read of a table outside its `read` patterns), 404 (unknown table, view, migration or note), 409 (ambiguous table name), 500. A refused write answers 422 (409 for undo) with the operation's result, `ok: false`: nothing was written."
69661
70761
  },
69662
70762
  servers: [{ url: "/" }],
69663
70763
  tags: [
@@ -69691,6 +70791,7 @@ function openApiDocument(version2) {
69691
70791
  init_message();
69692
70792
  init_hints();
69693
70793
  init_docs();
70794
+ init_read_scope();
69694
70795
  var ListenError = class extends GuidedError {
69695
70796
  constructor(message, code, g) {
69696
70797
  super(message, g);
@@ -70000,7 +71101,7 @@ var HttpApiServer = class {
70000
71101
  "gen_ai.tool.name": actor.tool
70001
71102
  });
70002
71103
  if (method === "GET" && (path23 === `${ROUTE_PREFIX2}/changes/stream` || path23 === `${ROUTE_PREFIX2}/changes/stream/`)) {
70003
- await this.stream(req, res, url);
71104
+ await this.stream(req, res, url, named ? this.opts.readScopeOf?.(named.name) ?? null : null);
70004
71105
  return;
70005
71106
  }
70006
71107
  if (method === "GET" && (path23 === MANUAL_PATH || path23.startsWith(`${MANUAL_PATH}/`))) {
@@ -70061,7 +71162,11 @@ var HttpApiServer = class {
70061
71162
  * `where`: JSON).
70062
71163
  * A comment line every 25 s keeps proxies from closing the connection.
70063
71164
  */
70064
- async stream(req, res, url) {
71165
+ async stream(req, res, url, scope) {
71166
+ if (scope && !(this.opts.readScopeSupported ?? READ_SCOPE_SUPPORTED)) {
71167
+ send2(res, 503, { error: READ_SCOPE_UNSUPPORTED, ...READ_SCOPE_UNSUPPORTED_HINT });
71168
+ return;
71169
+ }
70065
71170
  const feed = this.opts.changeFeed?.();
70066
71171
  if (!feed || !res.write) {
70067
71172
  send2(res, 503, { error: "the change feed is not available", ...guide("The change feed runs while the engine follows the vault (datanotes serve); list_changes reads it.", "changeFeed") });
@@ -70099,7 +71204,7 @@ var HttpApiServer = class {
70099
71204
  `);
70100
71205
  try {
70101
71206
  while (open2 && this.server && !this.draining) {
70102
- const page2 = await feed.wait({ ...q, since: cursor, limit: 500 }, 2e4);
71207
+ const page2 = await feed.wait({ ...q, since: cursor, limit: 500 }, 2e4, scope);
70103
71208
  if (!open2) break;
70104
71209
  if (page2.gap) res.write(`event: gap
70105
71210
  data: {"since":${cursor}}
@@ -70265,6 +71370,8 @@ init_actor();
70265
71370
  init_redact();
70266
71371
 
70267
71372
  // src/server/webhooks.ts
71373
+ init_metastore();
71374
+ init_read_scope();
70268
71375
  init_store();
70269
71376
  init_telemetry();
70270
71377
  init_message();
@@ -70360,8 +71467,16 @@ var WebhookDispatcher = class {
70360
71467
  if (this.saveTimer) await this.save();
70361
71468
  await this.saving;
70362
71469
  }
71470
+ /**
71471
+ * The webhooks, as the read scope of the request sees them: a restricted token (one with `read`
71472
+ * patterns) sees only the webhooks on tables it may all read, never one on every table.
71473
+ */
70363
71474
  list() {
70364
- return this.hooks.map(publicView);
71475
+ const scope = currentReadScope();
71476
+ if (!scope) return this.hooks.map(publicView);
71477
+ const model = cachedMetastore(this.app, this.getSettings());
71478
+ const full = (t) => (model ? rowTableFull(model, t) : null) ?? t;
71479
+ return this.hooks.filter((h) => (h.tables ?? []).length > 0 && h.tables.every((t) => tableReadable(full(t), scope))).map(publicView);
70365
71480
  }
70366
71481
  async create(input) {
70367
71482
  if (!/^https?:\/\//i.test(input.url)) throw new Error(`"url" must be an http(s) URL`);
@@ -70451,7 +71566,7 @@ var WebhookDispatcher = class {
70451
71566
  continue;
70452
71567
  }
70453
71568
  const q = { since: h.cursor, limit: BATCH, ...h.tables ? { tables: h.tables } : {}, ...h.ops ? { ops: h.ops } : {}, ...h.columns ? { columns: h.columns } : {}, ...h.excludeActor ? { excludeActor: h.excludeActor } : {}, ...h.where ? { where: h.where } : {} };
70454
- const page2 = await feed.wait(q, WAIT_MS);
71569
+ const page2 = await feed.wait(q, WAIT_MS, null);
70455
71570
  if (!this.alive(h)) return;
70456
71571
  if (page2.events.length === 0 && !page2.gap) {
70457
71572
  if (page2.last !== h.cursor) {
@@ -70621,7 +71736,7 @@ async function engineStatus(i) {
70621
71736
  cursor: w.cursor
70622
71737
  }))
70623
71738
  },
70624
- tokens: i.tokens.map((t) => ({ name: t.name, write: t.write, ddl: t.ddl, maintain: t.maintain === true, last_seen: t.last_seen ?? null })),
71739
+ tokens: i.tokens.map((t) => ({ name: t.name, ...t.read.includes("*") ? {} : { read: t.read }, write: t.write, ddl: t.ddl, maintain: t.maintain === true, last_seen: t.last_seen ?? null })),
70625
71740
  hand_edits: handEdits,
70626
71741
  logs: {
70627
71742
  folder: s.fileLogEnabled ? i.host?.store ? `${i.host.store}${i.host.store.includes("\\") ? "\\" : "/"}logs` : storePath("logs") : null,
@@ -70633,13 +71748,14 @@ async function engineStatus(i) {
70633
71748
 
70634
71749
  // src/server/tokens.ts
70635
71750
  init_actor();
71751
+ init_read_scope();
70636
71752
  init_metastore();
70637
71753
  init_store();
70638
71754
  init_telemetry();
70639
71755
  init_message();
70640
71756
  init_hints();
70641
71757
  var FILE = "tokens.json";
70642
- var NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
71758
+ var NAME2 = /^[a-z0-9][a-z0-9-]{0,62}$/;
70643
71759
  var RESERVED_NAMES = /* @__PURE__ */ new Set(["owner"]);
70644
71760
  var SEEN_SAVE_MS = 6e4;
70645
71761
  async function sha256Hex(text5) {
@@ -70647,11 +71763,13 @@ async function sha256Hex(text5) {
70647
71763
  return [...d].map((b) => b.toString(16).padStart(2, "0")).join("");
70648
71764
  }
70649
71765
  function tableAllowed(full, patterns) {
70650
- return patterns.some((p) => {
70651
- if (p === "*") return true;
70652
- if (p.endsWith(".*")) return full.startsWith(p.slice(0, -1));
70653
- return p === full;
70654
- });
71766
+ return matchesTablePattern(full, patterns);
71767
+ }
71768
+ function scopeOf(t) {
71769
+ return t.read === void 0 || t.read.includes("*") ? null : { token: t.name, patterns: t.read };
71770
+ }
71771
+ function readCovers(r, w) {
71772
+ return r === "*" || r === w || r.endsWith(".*") && w.startsWith(r.slice(0, -1));
70655
71773
  }
70656
71774
  function tokenNameOf(actor) {
70657
71775
  const auth = actor?.auth;
@@ -70711,6 +71829,7 @@ var TokenRegistry = class {
70711
71829
  return {
70712
71830
  name: t.name,
70713
71831
  ...t.description !== void 0 ? { description: t.description } : {},
71832
+ read: t.read ?? ["*"],
70714
71833
  write: t.write,
70715
71834
  ddl: t.ddl,
70716
71835
  ...t.maintain !== void 0 ? { maintain: t.maintain } : {},
@@ -70741,7 +71860,7 @@ var TokenRegistry = class {
70741
71860
  }
70742
71861
  async create(input) {
70743
71862
  await this.load();
70744
- if (!NAME.test(input.name)) throw new BadRequest(`"name" must be kebab-case (a-z, 0-9, -)`);
71863
+ if (!NAME2.test(input.name)) throw new BadRequest(`"name" must be kebab-case (a-z, 0-9, -)`);
70745
71864
  if (RESERVED_NAMES.has(input.name)) throw new BadRequest(`"${input.name}" is reserved (the owner's token)`);
70746
71865
  if (this.get(input.name)) throw new BadRequest(`token ${input.name} already exists (update it, or delete it first)`);
70747
71866
  const token = newToken();
@@ -70750,13 +71869,15 @@ var TokenRegistry = class {
70750
71869
  name: input.name,
70751
71870
  ...input.description ? { description: input.description } : {},
70752
71871
  tokenHash: await sha256Hex(token),
70753
- write: checkPatterns(input.write ?? []),
71872
+ ...input.read !== void 0 ? { read: readPatterns(input.read) } : {},
71873
+ write: checkPatterns(input.write ?? [], "write"),
70754
71874
  ddl: input.ddl === true,
70755
71875
  ...input.maintain === true ? { maintain: true } : {},
70756
71876
  ...input.autolink !== void 0 ? { autolink: input.autolink } : {},
70757
71877
  createdAt: now,
70758
71878
  updatedAt: now
70759
71879
  };
71880
+ checkReadCoversPermissions(t);
70760
71881
  this.tokens.push(t);
70761
71882
  this.reindex();
70762
71883
  await this.save();
@@ -70765,24 +71886,31 @@ var TokenRegistry = class {
70765
71886
  }
70766
71887
  /**
70767
71888
  * Change a token. `self` (a token updating its own entry) may change its description and its
70768
- * `autolink` default; permissions are the owner's. `autolink: null` follows the database again.
71889
+ * `autolink` default; permissions are the owner's. `autolink: null` follows the database again,
71890
+ * `read: null` reads every table again. Nothing changes when the token as changed would be refused.
70769
71891
  */
70770
71892
  async update(name, patch, self) {
70771
71893
  await this.load();
70772
71894
  const t = this.get(name);
70773
71895
  if (!t) throw new BadRequest(`no token ${name}`);
70774
- if (self && (patch.write !== void 0 || patch.ddl !== void 0 || patch.maintain !== void 0)) throw new Forbidden("a token cannot change its own permissions");
71896
+ if (self && (patch.read !== void 0 || patch.write !== void 0 || patch.ddl !== void 0 || patch.maintain !== void 0)) throw new Forbidden("a token cannot change its own permissions");
71897
+ const next = { ...t };
70775
71898
  if (patch.description !== void 0) {
70776
- if (patch.description === "") delete t.description;
70777
- else t.description = patch.description;
70778
- }
70779
- if (patch.write !== void 0) t.write = checkPatterns(patch.write);
70780
- if (patch.ddl !== void 0) t.ddl = patch.ddl;
70781
- if (patch.maintain === true) t.maintain = true;
70782
- else if (patch.maintain === false) delete t.maintain;
70783
- if (patch.autolink === null) delete t.autolink;
70784
- else if (patch.autolink !== void 0) t.autolink = patch.autolink;
70785
- t.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
71899
+ if (patch.description === "") delete next.description;
71900
+ else next.description = patch.description;
71901
+ }
71902
+ if (patch.read === null) delete next.read;
71903
+ else if (patch.read !== void 0) next.read = readPatterns(patch.read);
71904
+ if (patch.write !== void 0) next.write = checkPatterns(patch.write, "write");
71905
+ if (patch.ddl !== void 0) next.ddl = patch.ddl;
71906
+ if (patch.maintain === true) next.maintain = true;
71907
+ else if (patch.maintain === false) delete next.maintain;
71908
+ if (patch.autolink === null) delete next.autolink;
71909
+ else if (patch.autolink !== void 0) next.autolink = patch.autolink;
71910
+ checkReadCoversPermissions(next);
71911
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
71912
+ for (const k of Object.keys(t)) if (!(k in next)) delete t[k];
71913
+ Object.assign(t, next);
70786
71914
  await this.save();
70787
71915
  return this.publicView(t);
70788
71916
  }
@@ -70807,7 +71935,10 @@ var TokenRegistry = class {
70807
71935
  return token;
70808
71936
  }
70809
71937
  // --- permissions ---------------------------------------------------------------------------
70810
- /** Why this token may not run this route with this body, or null when it may. */
71938
+ /**
71939
+ * Why this token may not run this route with this body, or null when it may: the reason of a
71940
+ * refused write or route, or the refusal of a read (the same error the engine's read sites throw).
71941
+ */
70811
71942
  async check(name, route, body) {
70812
71943
  const t = this.get(name);
70813
71944
  if (!t) return "unknown token";
@@ -70818,8 +71949,8 @@ var TokenRegistry = class {
70818
71949
  }
70819
71950
  /** Why this token may not run this operation with these arguments, or null when it may. */
70820
71951
  async checkOperation(t, op2, body) {
70821
- if (READ_OPERATIONS.has(op2)) return null;
70822
71952
  const b = body && typeof body === "object" ? body : {};
71953
+ if (READ_OPERATIONS.has(op2)) return this.checkRead(t, op2, b);
70823
71954
  if (op2 === "batch") {
70824
71955
  const steps = Array.isArray(b.operations) ? b.operations : [];
70825
71956
  for (const [i, raw] of steps.entries()) {
@@ -70842,20 +71973,44 @@ var TokenRegistry = class {
70842
71973
  }
70843
71974
  return null;
70844
71975
  }
70845
- /** Wrap a dispatcher: calls made with a named token are checked first. */
71976
+ /**
71977
+ * The refusal of a read whose arguments name a table the token may not read, or null. Only names
71978
+ * that resolve to a table are checked here (a view, `base:x`, the system catalog or an unknown
71979
+ * name pass): the engine's read sites decide the rest while the request runs in the token's scope.
71980
+ */
71981
+ async checkRead(t, op2, b) {
71982
+ const scope = scopeOf(t);
71983
+ if (!scope) return null;
71984
+ const names2 = (Object.prototype.hasOwnProperty.call(READ_TABLE_ARGS, op2) ? READ_TABLE_ARGS[op2] : []).map((k) => b[k]);
71985
+ if ((op2 === "semantic_search" || op2 === "search") && Array.isArray(b.tables)) names2.push(...b.tables.filter((x) => typeof x === "string" && !x.includes("*")));
71986
+ if (!names2.some((n) => typeof n === "string")) return null;
71987
+ const model = (await loadMetastore(this.deps.app, this.deps.getSettings()).catch(() => null))?.model ?? null;
71988
+ if (!model) return null;
71989
+ for (const ref2 of names2) {
71990
+ const full = rowTableFull(model, ref2);
71991
+ if (full !== null && !matchesTablePattern(full, scope.patterns)) return tableReadDenied(t.name, full);
71992
+ }
71993
+ return null;
71994
+ }
71995
+ /** Wrap a dispatcher: calls made with a named token are checked first, and run in its read scope. */
70846
71996
  guard(dispatch2) {
70847
71997
  return async (route, handler, body) => {
70848
71998
  const name = tokenNameOf(currentActor());
70849
- if (name) {
70850
- const why = await this.check(name, route, body);
70851
- if (why) {
70852
- telemetry().log("WARN", "datanotes token refused", { token_name: name, "http.route": route, reason: why });
70853
- return { status: 403, json: { error: why, ...FORBIDDEN_HINT } };
70854
- }
70855
- const autolink = this.get(name)?.autolink;
70856
- if (autolink !== void 0) body = withAutolinkDefault(route, body, autolink);
70857
- }
70858
- return dispatch2(route, handler, body);
71999
+ if (!name) return dispatch2(route, handler, body);
72000
+ const why = await this.check(name, route, body);
72001
+ if (why) {
72002
+ const reason = typeof why === "string" ? why : why.message;
72003
+ telemetry().log("WARN", "datanotes token refused", { token_name: name, "http.route": route, reason });
72004
+ return typeof why === "string" ? { status: 403, json: { error: why, ...FORBIDDEN_HINT } } : errorResponse(why);
72005
+ }
72006
+ const token = this.get(name);
72007
+ if (token.autolink !== void 0) body = withAutolinkDefault(route, body, token.autolink);
72008
+ const scope = scopeOf(token);
72009
+ if (scope && !(this.deps.readScopeSupported ?? READ_SCOPE_SUPPORTED)) {
72010
+ telemetry().log("WARN", "datanotes token refused", { token_name: name, "http.route": route, reason: READ_SCOPE_UNSUPPORTED });
72011
+ return { status: 503, json: { error: READ_SCOPE_UNSUPPORTED, ...READ_SCOPE_UNSUPPORTED_HINT } };
72012
+ }
72013
+ return runWithReadScope(scope, () => dispatch2(route, handler, body));
70859
72014
  };
70860
72015
  }
70861
72016
  // --- routes --------------------------------------------------------------------------------
@@ -70871,6 +72026,7 @@ var TokenRegistry = class {
70871
72026
  if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) throw new BadRequest(`"${k}" must be a list of strings`);
70872
72027
  return v;
70873
72028
  };
72029
+ const strListOrNull = (b, k) => b[k] === null ? null : strList(b, k);
70874
72030
  const optStr = (b, k) => {
70875
72031
  const v = b[k];
70876
72032
  if (v === void 0 || v === null) return void 0;
@@ -70900,10 +72056,12 @@ var TokenRegistry = class {
70900
72056
  ownerOnly();
70901
72057
  const autolink = optAutolink(b, false);
70902
72058
  const description = optStr(b, "description");
72059
+ const read3 = strList(b, "read");
70903
72060
  const write = strList(b, "write");
70904
72061
  const r = await this.create({
70905
72062
  name: optStr(b, "name") ?? "",
70906
72063
  ...description !== void 0 ? { description } : {},
72064
+ ...read3 ? { read: read3 } : {},
70907
72065
  ...write ? { write } : {},
70908
72066
  ...b.ddl === true ? { ddl: true } : {},
70909
72067
  ...b.maintain === true ? { maintain: true } : {},
@@ -70915,10 +72073,12 @@ var TokenRegistry = class {
70915
72073
  const self = !!caller();
70916
72074
  for (const k of ["ddl", "maintain"]) if (b[k] !== void 0 && typeof b[k] !== "boolean") throw new BadRequest(`"${k}" must be true or false`);
70917
72075
  const description = optStr(b, "description");
72076
+ const read3 = strListOrNull(b, "read");
70918
72077
  const write = strList(b, "write");
70919
72078
  const autolink = optAutolink(b, true);
70920
72079
  const patch = {
70921
72080
  ...description !== void 0 ? { description } : {},
72081
+ ...read3 !== void 0 ? { read: read3 } : {},
70922
72082
  ...write !== void 0 ? { write } : {},
70923
72083
  ...typeof b.ddl === "boolean" ? { ddl: b.ddl } : {},
70924
72084
  ...typeof b.maintain === "boolean" ? { maintain: b.maintain } : {},
@@ -70966,12 +72126,28 @@ function optAutolink(b, nullable2) {
70966
72126
  )
70967
72127
  );
70968
72128
  }
70969
- function checkPatterns(patterns) {
72129
+ function checkPatterns(patterns, field3) {
70970
72130
  for (const p of patterns) {
70971
- if (!/^(\*|[\w-]+(\.[\w-]+){0,2}(\.\*)?)$/.test(p)) throw new BadRequest(`write pattern "${p}" must be a table name, catalog.schema.*, catalog.* or *`);
72131
+ if (!/^(\*|[\w-]+(\.[\w-]+){0,2}(\.\*)?)$/.test(p)) {
72132
+ throw new BadRequest(`${field3} pattern "${p}" must be a table name, catalog.schema.*, catalog.* or *`, field3 === "read" ? guide(`Write each read pattern as catalog.schema.table, catalog.schema.*, catalog.* or *; list_tables gives the full names.`, "tokenRead") : void 0);
72133
+ }
70972
72134
  }
70973
72135
  return [...new Set(patterns)];
70974
72136
  }
72137
+ function readPatterns(patterns) {
72138
+ const read3 = checkPatterns(patterns, "read");
72139
+ return read3.includes("*") ? ["*"] : read3;
72140
+ }
72141
+ function checkReadCoversPermissions(t) {
72142
+ const read3 = t.read;
72143
+ if (read3 === void 0 || read3.includes("*")) return;
72144
+ if (t.ddl || t.maintain === true) {
72145
+ throw new BadRequest(`token ${t.name}: "ddl" and "maintain" need read on every table`, guide(`Leave "read" out (or pass ["*"]) for a token with ddl or maintain, or turn them off.`, "tokenRead"));
72146
+ }
72147
+ for (const w of t.write) {
72148
+ if (!read3.some((r) => readCovers(r, w))) throw new BadRequest(`token ${t.name} writes ${w} but may not read it`, guide(`Add "${w}" (or a wider pattern) to "read": a write answers with the rows it touches.`, "tokenRead"));
72149
+ }
72150
+ }
70975
72151
 
70976
72152
  // src/server/extensions.ts
70977
72153
  init_actor();
@@ -71076,6 +72252,7 @@ var ExtensionRegistry = class {
71076
72252
 
71077
72253
  // src/server/read-cache.ts
71078
72254
  init_metastore();
72255
+ init_query_tables();
71079
72256
  init_file_cache();
71080
72257
  init_vault_index();
71081
72258
  var CACHED_READS = /* @__PURE__ */ new Set([
@@ -71094,7 +72271,6 @@ var CACHED_READS = /* @__PURE__ */ new Set([
71094
72271
  ]);
71095
72272
  var GRAPH_READS = /* @__PURE__ */ new Set(["graph_neighbors", "graph_traverse", "graph_path"]);
71096
72273
  var SECRET_SETTINGS = /* @__PURE__ */ new Set(["httpServerToken", "telemetryHeaders", "storeBackupRemote"]);
71097
- var PLAIN_SELECT_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where", "order_by", "limit", "offset", "group_by", "aggregates", "include_body", "join", "union"]);
71098
72274
  var TODAY_BUCKET_MS = 15 * 6e4;
71099
72275
  function pathKey(path23) {
71100
72276
  return path23.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").normalize("NFC").toLowerCase();
@@ -71376,8 +72552,8 @@ function readDeps(op2, body, model) {
71376
72552
  return { tables: "vault", today: today || r.today || views.today };
71377
72553
  }
71378
72554
  if (op2 === "describe_table" && model && typeof body.table === "string") {
71379
- const ref2 = refOf3(model, body.table, "table");
71380
- if (ref2 && !refOf3(model, body.table, "view")) return { tables: [ref2], today };
72555
+ const ref2 = refOf2(model, body.table, "table");
72556
+ if (ref2 && !refOf2(model, body.table, "view")) return { tables: [ref2], today };
71381
72557
  }
71382
72558
  if (op2 === "list_tables") return { tables: [], today };
71383
72559
  if (op2 === "describe_database" && model) return { tables: catalogTables(model).map(({ ref: ref2 }) => ref2.full), today, loose: true };
@@ -71386,111 +72562,6 @@ function readDeps(op2, body, model) {
71386
72562
  if (GRAPH_READS.has(op2) && model) return { tables: catalogTables(model).map(({ ref: ref2 }) => ref2.full).sort(), today, links: true };
71387
72563
  return { tables: "vault", today };
71388
72564
  }
71389
- function refOf3(model, name, kind) {
71390
- try {
71391
- return resolveCatalogRef(model, name, kind)?.full ?? null;
71392
- } catch {
71393
- return null;
71394
- }
71395
- }
71396
- function isSystemTable(name) {
71397
- const t = name.trim();
71398
- if (t.startsWith(`${SYSTEM_CATALOG}.`)) return true;
71399
- const parts = t.split(".");
71400
- return parts.length === 2 && parts[0] === "information_schema";
71401
- }
71402
- var DOTTED_KEY = /"[^"]*\.[^"]*"\s*:/;
71403
- function queryTables(q, model, out2, depth) {
71404
- const text5 = JSON.stringify(q);
71405
- const r = { vault: false, today: text5.includes("{today"), now: text5.includes("{now") };
71406
- if (depth > 8) return { ...r, vault: true };
71407
- if (text5.includes("within") || text5.includes("{this")) r.vault = true;
71408
- if (DOTTED_KEY.test(text5) && !linkedTables(q, model, out2)) r.vault = true;
71409
- if (text5.includes("file_backlink_count")) r.links = true;
71410
- if (Object.keys(q).some((k) => !PLAIN_SELECT_KEYS.has(k))) r.vault = true;
71411
- const sources = [q.table];
71412
- if (q.join && typeof q.join === "object") sources.push(q.join.table);
71413
- if (Array.isArray(q.union)) for (const b of q.union) sources.push(b && typeof b === "object" ? b.table : void 0);
71414
- for (const name of sources) {
71415
- if (typeof name === "string" && isSystemTable(name)) {
71416
- r.store = true;
71417
- continue;
71418
- }
71419
- if (typeof name !== "string" || name.startsWith("base:")) {
71420
- r.vault = true;
71421
- continue;
71422
- }
71423
- const table = refOf3(model, name, "table");
71424
- const view = refOf3(model, name, "view");
71425
- if (!table && !view) r.vault = true;
71426
- if (table) out2.add(table);
71427
- const v = view ? viewQuery(model, view) : null;
71428
- if (view && !v) r.vault = true;
71429
- if (v) {
71430
- const sub = queryTables(v, model, out2, depth + 1);
71431
- r.vault ||= sub.vault;
71432
- r.links ||= sub.links;
71433
- r.store ||= sub.store;
71434
- r.today ||= sub.today;
71435
- r.now ||= sub.now;
71436
- }
71437
- }
71438
- return r;
71439
- }
71440
- var isRecord2 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
71441
- function plainTable(model, name) {
71442
- return typeof name === "string" && !refOf3(model, name, "view") ? refOf3(model, name, "table") : null;
71443
- }
71444
- function linkedTables(q, model, out2) {
71445
- const join = isRecord2(q.join) ? q.join : null;
71446
- const branches = Array.isArray(q.union) ? q.union : null;
71447
- const rest = {
71448
- ...q,
71449
- where: void 0,
71450
- join: join ? { ...join, where: void 0 } : q.join,
71451
- union: branches ? branches.map((b) => isRecord2(b) ? { ...b, where: void 0 } : b) : q.union
71452
- };
71453
- if (DOTTED_KEY.test(JSON.stringify(rest))) return false;
71454
- const roots = branches ? branches.map((b) => isRecord2(b) ? plainTable(model, b.table) : null) : [plainTable(model, q.table)];
71455
- if (roots.length === 0) return false;
71456
- for (const table of roots) if (!followKeys(q.where, table, model, out2)) return false;
71457
- if (join && !followKeys(join.where, plainTable(model, join.table), model, out2)) return false;
71458
- for (const b of branches ?? []) if (isRecord2(b) && !followKeys(b.where, plainTable(model, b.table), model, out2)) return false;
71459
- return true;
71460
- }
71461
- function followKeys(where, table, model, out2) {
71462
- if (where === null || typeof where !== "object") return true;
71463
- if (Array.isArray(where)) return where.every((w) => followKeys(w, table, model, out2));
71464
- for (const [key2, cond] of Object.entries(where)) {
71465
- if (key2 === "and" || key2 === "or" || key2 === "not") {
71466
- if (!followKeys(cond, table, model, out2)) return false;
71467
- } else if (key2.indexOf(".") > 0 && (table === null || !followPath(key2, table, model, out2))) return false;
71468
- }
71469
- return true;
71470
- }
71471
- function followPath(key2, table, model, out2) {
71472
- let current2 = table;
71473
- for (const column of key2.split(".").slice(0, -1)) {
71474
- const next = linkTarget(model, current2, column);
71475
- if (!next) return false;
71476
- out2.add(next);
71477
- current2 = next;
71478
- }
71479
- return true;
71480
- }
71481
- function linkTarget(model, full, column) {
71482
- const t = catalogTables(model).find((x) => x.ref.full === full);
71483
- if (!t) return null;
71484
- let columns;
71485
- try {
71486
- columns = effectiveColumns(model, t.ref, t.entry);
71487
- } catch {
71488
- return null;
71489
- }
71490
- const c = columns.find((x) => x.name === column);
71491
- const link = c !== void 0 && (c.type === "link" || c.type === "list" && c.items === "link");
71492
- return link && typeof c.ref === "string" ? refOf3(model, c.ref, "table") : null;
71493
- }
71494
72565
  var VIEW_TIMES = /* @__PURE__ */ new WeakMap();
71495
72566
  function viewTimes(model) {
71496
72567
  let t = VIEW_TIMES.get(model);
@@ -71500,11 +72571,6 @@ function viewTimes(model) {
71500
72571
  }
71501
72572
  return t;
71502
72573
  }
71503
- function viewQuery(model, full) {
71504
- const [catalog, schema, name] = full.split(".");
71505
- const q = model.catalogs[catalog]?.schemas[schema]?.views[name]?.query;
71506
- return q && typeof q === "object" ? q : null;
71507
- }
71508
72574
 
71509
72575
  // src/server/runtime.ts
71510
72576
  init_message();
@@ -71848,6 +72914,11 @@ var EngineRuntime = class {
71848
72914
  ...this.o.httpHost ? { host: this.o.httpHost } : {},
71849
72915
  changeFeed: () => this.feed,
71850
72916
  tokens: this.tokens,
72917
+ // the change stream's events, as the token may read them (a token gone since: nothing)
72918
+ readScopeOf: (name) => {
72919
+ const t = this.tokens.get(name);
72920
+ return t ? scopeOf(t) : { token: name, patterns: [] };
72921
+ },
71851
72922
  dispatch: guarded,
71852
72923
  ...this.o.docs ? { docs: this.o.docs } : {},
71853
72924
  compression: () => this.o.getSettings().httpCompression
@@ -71899,7 +72970,7 @@ var EngineRuntime = class {
71899
72970
  const name = tokenNameOf(currentActor());
71900
72971
  if (!name) return "owner";
71901
72972
  const t = this.tokens.get(name);
71902
- return JSON.stringify([name, t?.write ?? null, t?.ddl ?? null, t?.maintain ?? null, t?.autolink ?? null]);
72973
+ return JSON.stringify([name, t?.write ?? null, t?.ddl ?? null, t?.maintain ?? null, t?.autolink ?? null, t?.read ?? null]);
71903
72974
  }
71904
72975
  /** Follow the vault (call once the vault is indexed: Obsidian's layout-ready, or after opening). */
71905
72976
  async start(opts = {}) {
@@ -72987,7 +74058,7 @@ function relativePath(fromDir2, to) {
72987
74058
  while (i < a.length && i < b.length - 1 && a[i] === b[i]) i++;
72988
74059
  return [...a.slice(i).map(() => ".."), ...b.slice(i)].join("/");
72989
74060
  }
72990
- function linkText(resolver, path23, sourcePath, format, omitMd = true) {
74061
+ function linkText2(resolver, path23, sourcePath, format, omitMd = true) {
72991
74062
  const target = omitMd && /\.md$/i.test(path23) ? stripMd(path23) : path23;
72992
74063
  if (format === "absolute") return target;
72993
74064
  if (format === "relative") return relativePath(dirname2(sourcePath), target);
@@ -75149,7 +76220,7 @@ var NodeMetadataCache = class {
75149
76220
  return f && "extension" in f ? f : null;
75150
76221
  }
75151
76222
  fileToLinktext(file, sourcePath, omitMdExtension = true) {
75152
- return linkText(this.resolver, file.path, sourcePath, this.vault.config.newLinkFormat, omitMdExtension);
76223
+ return linkText2(this.resolver, file.path, sourcePath, this.vault.config.newLinkFormat, omitMdExtension);
75153
76224
  }
75154
76225
  on(name, cb) {
75155
76226
  this.events.on(name, cb);
@@ -75251,7 +76322,7 @@ var NodeFileManager = class {
75251
76322
  src,
75252
76323
  resolveOld,
75253
76324
  moved,
75254
- (dest, s) => linkText(newResolver, dest, s, format, true),
76325
+ (dest, s) => linkText2(newResolver, dest, s, format, true),
75255
76326
  oldSrc,
75256
76327
  (lp, s) => newResolver.resolve(lp, s)
75257
76328
  ).text;
@@ -76145,7 +77216,7 @@ function setUp(o, vault, home, explicit, self) {
76145
77216
  // src/node/service.ts
76146
77217
  init_local();
76147
77218
  init_hints();
76148
- var VERSION = true ? "3.3.1" : "dev";
77219
+ var VERSION = true ? "3.4.0" : "dev";
76149
77220
  function parsePort(raw, from = "--port") {
76150
77221
  const n = /^\s*\d+\s*$/.test(raw) ? Number(raw) : NaN;
76151
77222
  if (n === 0 || Number.isInteger(n) && n >= 1024 && n <= 65535) return n;
@@ -76660,7 +77731,8 @@ ${"error".padEnd(11)}${r.error}`;
76660
77731
  line("changes", f.running ? `feed running, last seq ${f.head}` : `feed NOT RUNNING: ${f.reason}`);
76661
77732
  line("webhooks", `${r.webhooks.count}${r.webhooks.failing.length ? ` (${r.webhooks.failing.length} failing)` : ""}`);
76662
77733
  for (const w of r.webhooks.failing) out2.push(` ! ${w.id} ${w.url_host}: ${plural(w.failures, "failed attempt")}, retried with backoff${w.last_error ? `; last error: ${w.last_error}` : ""}`);
76663
- const perms = (t) => [t.write.length ? `write ${t.write.join(", ")}` : "read only", ...t.ddl ? ["ddl"] : [], ...t.maintain ? ["maintain"] : []].join(", ");
77734
+ const reads = (t) => t.read ? [`read ${t.read.length ? t.read.join(", ") : "nothing"}`] : [];
77735
+ const perms = (t) => [...reads(t), t.write.length ? `write ${t.write.join(", ")}` : t.read ? "no write" : "read only", ...t.ddl ? ["ddl"] : [], ...t.maintain ? ["maintain"] : []].join(", ");
76664
77736
  line("tokens", r.tokens.length ? r.tokens.map((t) => `${t.name} (${perms(t)})`).join("; ") : "owner only");
76665
77737
  const h = r.hand_edits;
76666
77738
  line(
@@ -77209,7 +78281,7 @@ Docs: https://datanotes.dev`;
77209
78281
 
77210
78282
  // src/node/cli.ts
77211
78283
  init_message();
77212
- var VERSION2 = true ? "3.3.1" : "dev";
78284
+ var VERSION2 = true ? "3.4.0" : "dev";
77213
78285
  var consoleError = console.error;
77214
78286
  async function main5(argv = process.argv.slice(2)) {
77215
78287
  const [command, ...rest] = argv;