datanotes 3.1.0 → 3.3.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 (72) hide show
  1. package/README.md +1 -0
  2. package/SECURITY.md +12 -0
  3. package/dist/api/client.d.cts +2 -0
  4. package/dist/api/client.d.ts +2 -0
  5. package/dist/api/docs.d.cts +14 -4
  6. package/dist/api/docs.d.ts +14 -4
  7. package/dist/api/extension-files.d.cts +23 -0
  8. package/dist/api/extension-files.d.ts +23 -0
  9. package/dist/api/extension-host.d.cts +108 -0
  10. package/dist/api/extension-host.d.ts +108 -0
  11. package/dist/api/extension-manifest.d.cts +154 -0
  12. package/dist/api/extension-manifest.d.ts +154 -0
  13. package/dist/api/extension-plan.d.cts +174 -0
  14. package/dist/api/extension-plan.d.ts +174 -0
  15. package/dist/api/extension-record.d.cts +65 -0
  16. package/dist/api/extension-record.d.ts +65 -0
  17. package/dist/api/extension-sdk.d.cts +65 -0
  18. package/dist/api/extension-sdk.d.ts +65 -0
  19. package/dist/api/extensions.cjs +18472 -0
  20. package/dist/api/extensions.d.cts +11 -0
  21. package/dist/api/extensions.d.ts +11 -0
  22. package/dist/api/extensions.js +18409 -0
  23. package/dist/api/index.cjs +675 -503
  24. package/dist/api/index.js +675 -503
  25. package/dist/api/mcp.cjs +203 -13
  26. package/dist/api/mcp.js +203 -13
  27. package/dist/datanotes.cjs +7903 -4667
  28. package/dist/docs/administration/extensions.md +213 -0
  29. package/dist/docs/api/operations/add_column.md +1 -0
  30. package/dist/docs/api/operations/add_constraint.md +2 -0
  31. package/dist/docs/api/operations/alter_column.md +1 -1
  32. package/dist/docs/api/operations/alter_table.md +3 -2
  33. package/dist/docs/api/operations/check_table.md +1 -1
  34. package/dist/docs/api/operations/create_table.md +3 -1
  35. package/dist/docs/api/operations/describe_table.md +1 -1
  36. package/dist/docs/api/operations/extensions_delete.md +29 -0
  37. package/dist/docs/api/operations/extensions_list.md +20 -0
  38. package/dist/docs/api/operations/extensions_record.md +44 -0
  39. package/dist/docs/api/operations/insert_row.md +2 -2
  40. package/dist/docs/api/operations/tags/extensions.md +9 -0
  41. package/dist/docs/api/operations/tags/rows-dml.md +1 -1
  42. package/dist/docs/api.md +10 -2
  43. package/dist/docs/develop/write-an-extension.md +369 -0
  44. package/dist/docs/guides/data-quality/check-constraints.md +14 -13
  45. package/dist/docs/guides/data-quality/hand-edits.md +10 -10
  46. package/dist/docs/guides/database-objects/bases.md +11 -11
  47. package/dist/docs/guides/database-objects/catalogs-and-schemas.md +16 -10
  48. package/dist/docs/guides/database-objects/columns.md +38 -38
  49. package/dist/docs/guides/database-objects/relations.md +173 -36
  50. package/dist/docs/guides/database-objects/tables.md +80 -35
  51. package/dist/docs/guides/database-objects/views.md +44 -44
  52. package/dist/docs/guides/history/change-data-feed.md +34 -34
  53. package/dist/docs/guides/history/journal-and-undo.md +12 -12
  54. package/dist/docs/guides/history/restore.md +20 -20
  55. package/dist/docs/guides/history/table-history.md +31 -31
  56. package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
  57. package/dist/docs/guides/read-and-write/import-notes.md +21 -21
  58. package/dist/docs/guides/read-and-write/joins-and-aggregates.md +7 -7
  59. package/dist/docs/guides/read-and-write/read-and-filter.md +10 -10
  60. package/dist/docs/guides/read-and-write/write-rows.md +10 -10
  61. package/dist/docs/guides/search/text.md +1 -1
  62. package/dist/docs/guides/security/security-model.md +12 -0
  63. package/dist/docs/reference/cli.md +32 -1
  64. package/dist/docs/reference/errors.md +1 -1
  65. package/dist/docs/reference/glossary.md +8 -0
  66. package/dist/docs/reference/operations/auxiliary.md +2 -2
  67. package/dist/docs/reference/operations/rows.md +1 -1
  68. package/dist/docs/reference/operations/schema.md +15 -11
  69. package/dist/docs/reference/operations.md +28 -28
  70. package/dist/docs/release-notes.md +57 -0
  71. package/dist/docs/start/connect-agents.md +11 -0
  72. package/package.json +11 -1
@@ -0,0 +1,369 @@
1
+ # Write an extension
2
+
3
+ > An extension adds tables and MCP tools to any Datanotes database: a git repository with a manifest, a built entry file and docs.
4
+
5
+ An extension is a git repository that adds tables, views, bases and MCP tools to a Datanotes database. You install it with `datanotes extension install`. [Install extensions](https://datanotes.dev/administration/extensions/) covers the user’s side.
6
+
7
+ This page builds a small extension, `acme/datanotes-ext-reading`. It keeps a reading list: books, their authors and what you read now.
8
+
9
+ ## Lay out the repository
10
+
11
+ The repository holds the manifest at its root, the built entry file the manifest names, and a `docs` folder.
12
+
13
+ ```text
14
+ datanotes-ext-reading/
15
+ datanotes-extension.json
16
+ dist/extension.mjs
17
+ docs/
18
+ index.md
19
+ tools.md
20
+ src/extension.ts
21
+ package.json
22
+ README.md
23
+ LICENSE
24
+ ```
25
+
26
+ * `dist/extension.mjs` is committed. The installer never runs `npm install`, a build or a script. It copies the files and checks their hashes.
27
+ * The entry file is one bundled ES module that imports only Node built-ins. Bundle your code and its libraries into it. The installer reads its imports, minified or not, and refuses a package or another file.
28
+ * `docs/index.md` is required. `docs/tools.md` has one `##` heading per tool, named like the tool.
29
+ * `README.md` and a licence file travel with the code when present.
30
+
31
+ Bundle the entry file with esbuild:
32
+
33
+ ```bash
34
+ npx esbuild src/extension.ts --bundle --platform=node \
35
+ --format=esm --target=node18 --outfile=dist/extension.mjs
36
+ ```
37
+
38
+ Your repository must not ignore `dist/`. Commit the bundle with each release.
39
+
40
+ ## Write the manifest
41
+
42
+ `datanotes-extension.json` declares the extension’s name, version, tables, tools and permissions. Before you confirm, the installer reads only the manifest. So the plan shows what the extension may do, without running its code.
43
+
44
+ ```json
45
+ {
46
+ "manifest_version": 1,
47
+ "name": "reading",
48
+ "version": "1.0.0",
49
+ "description": "A reading list: books and who wrote them",
50
+ "datanotes": ">=3.3.0",
51
+ "api": 1,
52
+ "entry": "dist/extension.mjs",
53
+ "default_schema": "reading",
54
+ "instructions": "Call list_reading, then finish_book once a book is read.",
55
+ "tables": {
56
+ "person": {
57
+ "owned": false,
58
+ "names": ["person", "people", "author"],
59
+ "description": "A person: here, the author of a book",
60
+ "filename": "{name}",
61
+ "columns": [
62
+ { "name": "name", "type": "string", "required": true,
63
+ "description": "The person's full name" }
64
+ ]
65
+ },
66
+ "book": {
67
+ "owned": true,
68
+ "description": "A book on the reading list",
69
+ "filename": "{title}",
70
+ "columns": [
71
+ { "name": "title", "type": "string", "required": true,
72
+ "description": "The book's title, as printed on the cover" },
73
+ { "name": "author", "type": "link", "ref": "@person",
74
+ "description": "Who wrote it" },
75
+ { "name": "status", "type": "enum",
76
+ "values": ["to_read", "reading", "read"], "default": "to_read",
77
+ "description": "Where the reader is with it" },
78
+ { "name": "rating", "type": "integer", "min": 1, "max": 5,
79
+ "description": "From 1 to 5, once read" }
80
+ ],
81
+ "constraints": [
82
+ {
83
+ "name": "rating_only_when_read",
84
+ "check": {
85
+ "or": [{ "status": "read" }, { "rating": { "exists": false } }]
86
+ },
87
+ "hint": "Rate a book only once it is read: set status read too."
88
+ }
89
+ ],
90
+ "instructions": "Link author to a row of @{person}.",
91
+ "bases": ["shelved"]
92
+ }
93
+ },
94
+ "bases": {
95
+ "shelved": {
96
+ "description": "Where a thing sits on a shelf",
97
+ "columns": [
98
+ { "name": "shelf", "type": "string",
99
+ "description": "The shelf it sits on" }
100
+ ]
101
+ }
102
+ },
103
+ "views": {
104
+ "vw_reading": {
105
+ "description": "Books being read now",
106
+ "query": { "table": "@book", "where": { "status": "reading" } }
107
+ }
108
+ },
109
+ "permissions": { "write": ["book"] },
110
+ "tools": [
111
+ {
112
+ "name": "list_reading",
113
+ "description": "List the books being read now: title and author.",
114
+ "inputSchema": {
115
+ "type": "object", "properties": {}, "additionalProperties": false
116
+ },
117
+ "annotations": { "title": "List Reading", "readOnlyHint": true },
118
+ "tables": ["book", "person"],
119
+ "writes": []
120
+ },
121
+ {
122
+ "name": "finish_book",
123
+ "description": "Mark a book read, with an optional rating from 1 to 5.",
124
+ "inputSchema": {
125
+ "type": "object",
126
+ "properties": {
127
+ "title": { "type": "string", "description": "The book's title" },
128
+ "rating": { "type": "integer", "minimum": 1, "maximum": 5 }
129
+ },
130
+ "required": ["title"],
131
+ "additionalProperties": false
132
+ },
133
+ "annotations": { "title": "Finish Book", "readOnlyHint": false },
134
+ "tables": ["book"],
135
+ "writes": ["book"]
136
+ }
137
+ ]
138
+ }
139
+ ```
140
+
141
+ The `book` table has a CHECK with its hint, and the base `shelved`. The view `vw_reading` lists the books being read.
142
+
143
+ Every key at the top level follows a rule:
144
+
145
+ | Key | Rule |
146
+ | ------------------ | ------------------------------------------------------------------------------------------------------ |
147
+ | `manifest_version` | Required. Exactly `1`. |
148
+ | `name` | Required. 2 to 40 lowercase letters, digits or `-`, starting with a letter. The token is `ext-<name>`. |
149
+ | `version` | Required. `MAJOR.MINOR.PATCH`, with an optional `-pre` suffix. |
150
+ | `description` | Required. 1 to 300 characters. |
151
+ | `datanotes` | Required. Exactly `">=X.Y.Z"`: the oldest engine version it works with. |
152
+ | `api` | Required. Exactly `1`: the module API version. |
153
+ | `entry` | Required. A relative path to a `.mjs` file inside the repository, with `/` separators. |
154
+ | `default_schema` | Required. Lowercase letters, digits and `_`: the schema of created owned tables and views. |
155
+ | `instructions` | Optional. 1 to 400 characters, added to the `datanotes mcp` instructions. |
156
+ | `tables` | Required. At least one role. Order matters. |
157
+ | `bases` | Optional. The extension’s own bases. |
158
+ | `views` | Optional. Names start with `vw_`. |
159
+ | `permissions` | Required. `write` lists the roles the extension’s token may write. It may be empty. |
160
+ | `tools` | Required. May be empty. |
161
+ | `migrations` | Optional. See [Migrate the schema between versions](#migrate-the-schema-between-versions). |
162
+
163
+ An unknown key is an error, and the message suggests the closest real key. Tool definitions keep MCP’s own field names, such as `inputSchema` and `readOnlyHint`. Every other key is snake_case.
164
+
165
+ `datanotes extension check` reports every problem at once, each as `<path>: <text>`. A path looks like `tables.book.columns[2].ref`. Common problems:
166
+
167
+ * `name: must be 2 to 40 lowercase letters, digits or -, starting with a letter`
168
+ * `datanotes: must be ">=X.Y.Z", such as ">=3.3.0"`
169
+ * `tables.book.columns[1].type: "text" is not a column type (...)` lists the real types.
170
+ * `tables.book.columns[1].ref: must be "self" or "@<role>" of a table declared before this one`
171
+ * `tools[0].writes[0]: "book" is not in permissions.write`
172
+ * `tools[0]: a read-only tool writes nothing (empty writes, or readOnlyHint false)`
173
+ * `tools[0]: a tool that writes has readOnlyHint false`
174
+ * `tables.book.columns[0]: "id" is the engine's own column: leave it out`
175
+
176
+ ## Declare the tables
177
+
178
+ Each table has a role: a short name the manifest uses, such as `book`. Tools never use a table’s real name. They ask for it with `ctx.table("book")`, so the extension works on any database.
179
+
180
+ An extension relates to a role in one of two ways:
181
+
182
+ * **Owned** (`"owned": true`): the extension creates the table, or adopts one of that name. It declares columns, `constraints`, `instructions` and `bases`.
183
+ * **Dependency** (`"owned": false`): the table belongs to the user, such as `person`. `names` lists the names that identify a match. The default is the table’s name.
184
+
185
+ These rules apply to tables:
186
+
187
+ * **Order matters.** A column’s `ref: "@person"` names a role declared before it. Put the dependencies first. Use `ref: "self"` for a link to the table itself.
188
+ * **`need`** is `"required"` (default) or `"optional"`. On a dependency, it says whether a matched table must have the column.
189
+ * **`@{role}`** inside any text is replaced by the role’s real table name. Use it in `instructions` and hints.
190
+ * **CHECKs** carry a `hint`, what to do when a row breaks the rule. Write one for every CHECK.
191
+
192
+ The installer compares the manifest with a table it adopts or matches:
193
+
194
+ * Columns are compared by name and by their rules (type, `ref`, values).
195
+ * A missing column is added on an owned table. On a dependency, it is a problem the user solves with `--map` or `--add`.
196
+ * CHECKs are compared by name. Equal ones stay. Different ones are replaced, and the plan shows it.
197
+ * Instructions that differ are replaced, and the plan shows it.
198
+ * Tags are added. Nothing is migrated again.
199
+
200
+ Version 1 has three limits:
201
+
202
+ * A manifest cannot declare an `inverse` across two tables. Only `ref: "self"` takes `inverse`.
203
+ * The columns of the extension’s own tables keep the manifest’s names. Users map only dependency columns.
204
+ * A dotted key into a dependency, such as `project.kind`, is not mapped in CHECKs and view queries.
205
+
206
+ ## Write the tools
207
+
208
+ The entry file exports one handler per tool. The default export looks like this:
209
+
210
+ ```ts
211
+ import {
212
+ defineExtension,
213
+ ExtensionToolError,
214
+ } from "datanotes/extensions";
215
+
216
+
217
+ interface Rows {
218
+ rows: { path: string; title: string; author?: string }[];
219
+ }
220
+
221
+
222
+ export default defineExtension({
223
+ api: 1,
224
+ handlers: {
225
+ async list_reading(_args, ctx) {
226
+ const found = await ctx.callTool("select_rows", {
227
+ table: ctx.table("book"),
228
+ where: { status: "reading" },
229
+ columns: ["title", "author"],
230
+ });
231
+ if (!found.ok) throw new ExtensionToolError(String(found.value));
232
+ return { books: (found.value as Rows).rows };
233
+ },
234
+ async finish_book(args, ctx) {
235
+ const book = ctx.table("book");
236
+ const found = await ctx.callTool("select_rows", {
237
+ table: book,
238
+ where: { title: args.title },
239
+ columns: ["title"],
240
+ });
241
+ if (!found.ok) throw new ExtensionToolError(String(found.value));
242
+ const [row] = (found.value as Rows).rows;
243
+ if (row === undefined) {
244
+ throw new ExtensionToolError(
245
+ `no book titled "${String(args.title)}"`,
246
+ "Call list_reading to find the title as written.",
247
+ "tools#finish_book",
248
+ );
249
+ }
250
+ const set: Record<string, unknown> = { status: "read" };
251
+ if (args.rating !== undefined) set.rating = args.rating;
252
+ const done = await ctx.callTool("update_row", {
253
+ table: book,
254
+ path: row.path,
255
+ set,
256
+ });
257
+ if (!done.ok) throw new ExtensionToolError(String(done.value));
258
+ return { path: row.path, status: "read" };
259
+ },
260
+ },
261
+ });
262
+ ```
263
+
264
+ The types come from `datanotes/extensions`, the package’s module API. Your bundle may hold its own copy of `ExtensionToolError`, or a small class of your own. Either works: the host recognises a refusal by its name, `ToolError`.
265
+
266
+ A handler gets the agent’s arguments and a context:
267
+
268
+ | Member | What it does |
269
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
270
+ | `ctx.callTool(operation, args)` | Runs an engine operation with the extension’s token, on behalf of the agent. It never throws for a refusal: read `ok` and `value`. |
271
+ | `ctx.table(role)` | The qualified name of the table bound to a role. |
272
+ | `ctx.column(role, column)` | The name of a role’s column in the bound table: the user’s mapping, or the manifest’s name. |
273
+ | `ctx.hasColumn(role, column)` | Whether the bound table has that column. An optional column may be missing. |
274
+ | `ctx.actor()` | The agent that called: `session`, `harness`, `harness_version`, `model`. |
275
+ | `ctx.timezone` | The database’s time zone, or an empty string. |
276
+ | `ctx.log(level, message, attributes)` | A log record through the MCP server’s log. |
277
+ | `ctx.extension` | `{ name, version }`. |
278
+
279
+ The extension never sees a token. The MCP server checks the arguments against the tool’s `inputSchema` before the handler runs.
280
+
281
+ ### Refuse with a hint
282
+
283
+ Throw `ExtensionToolError(message, hint, docs)` for a mistake the agent can fix. The message says what is wrong. The hint says how to fix it. `docs` is a page of your docs, with an optional anchor: `"tools#finish_book"`. The agent reads:
284
+
285
+ ```text
286
+ no book titled "Dune"
287
+ Hint: Call list_reading to find the title as written.
288
+ Docs: read_docs("extensions/reading/tools#finish_book")
289
+ ```
290
+
291
+ Write a `## finish_book` heading in `docs/tools.md` for every link you give. Any other error becomes `extension reading failed in finish_book: <message>`, and the server logs it.
292
+
293
+ ### Declare what a tool writes
294
+
295
+ Each tool lists `tables`, the roles it reads or writes, and `writes`, the roles it writes. Every role in `writes` must be in `permissions.write`. A tool with `readOnlyHint: true` has an empty `writes`. A tool that writes has `readOnlyHint: false`.
296
+
297
+ Before a tool with a non-empty `writes` runs, the server checks the calling agent’s token. That token must be able to write those tables. A token that may not write them gets a refusal. This is why a wrong `writes` list matters: declare it exactly.
298
+
299
+ A tool with an empty `writes` may not write rows, even if the extension’s token could. The server refuses these calls from the tool’s handler:
300
+
301
+ * `batch`
302
+ * `restore_table`
303
+ * any operation that writes rows, such as `insert_row` or `update_rows`
304
+
305
+ The refusal reads `tool <tool> of extension <name> declares no writes: it may not call <operation>`. To write, declare the roles in `writes` and set `readOnlyHint: false`. Otherwise, call a read operation.
306
+
307
+ ### Log without leaking
308
+
309
+ Call `ctx.log` with a constant message and put the variable parts in attributes. Never log argument values, tokens or note text.
310
+
311
+ ## Migrate the schema between versions
312
+
313
+ The manifest always describes the latest schema. `migrations` lists the operations that bring a database from an older version to the new one. A migration holds only declarative operations, the ones `batch` takes.
314
+
315
+ ```json
316
+ "migrations": [
317
+ {
318
+ "version": "1.1.0",
319
+ "description": "Record the day a book was finished",
320
+ "operations": [
321
+ {
322
+ "op": "add_column",
323
+ "table": "@book",
324
+ "column": {
325
+ "name": "finished_on",
326
+ "type": "date",
327
+ "description": "The day the book was finished"
328
+ }
329
+ }
330
+ ]
331
+ }
332
+ ]
333
+ ```
334
+
335
+ Follow these rules:
336
+
337
+ * Versions increase down the list, and none is later than the manifest’s `version`.
338
+ * `@book` is replaced by the real table name, as everywhere in the manifest.
339
+ * A step whose effect is already there is skipped, so a migration runs safely twice.
340
+ * A migration cannot run code. A data conversion is out of scope for version 1.
341
+
342
+ An update also reconciles the tables with the new manifest by itself. It adds missing columns, CHECKs, instructions, tags, bases and views, and replaces CHECKs that changed. List a migration only when you rename, move or drop something. The installer never drops a column unless a migration does.
343
+
344
+ ## Check and publish an extension
345
+
346
+ Run the check in the repository. It validates the manifest and the files without a database:
347
+
348
+ ```bash
349
+ datanotes extension check .
350
+ ```
351
+
352
+ While you develop, install from the folder on a test copy of a database, never your real one:
353
+
354
+ ```bash
355
+ datanotes extension install ./datanotes-ext-reading \
356
+ --database ~/scratch/notes-copy
357
+ datanotes extension update reading --from ./datanotes-ext-reading \
358
+ --database ~/scratch/notes-copy
359
+ ```
360
+
361
+ To publish, follow these steps:
362
+
363
+ 1. Set `version` in the manifest.
364
+ 2. Rebuild `dist/extension.mjs` and commit it.
365
+ 3. Run `datanotes extension check .`.
366
+ 4. Tag the commit `v<version>`, for example `v1.0.0`. The tag must name the manifest’s version.
367
+ 5. Push the tag. Users install with `owner/repo@v1.0.0`.
368
+
369
+ An out-of-date `dist` is the most common mistake: the installer ships what is committed.
@@ -6,7 +6,7 @@ A CHECK constraint is a rule written as a `where` filter, such as “a watched f
6
6
 
7
7
  ## Enforce rules with CHECK constraints
8
8
 
9
- `add_constraint` adds a CHECK constraint, a rule every row must satisfy, written as a `where` filter. The engine refuses it while existing rows break the rule and lists them; after that, it enforces the rule on every write. A condition on a column the row leaves empty passes, as in Structured Query Language (SQL). Inside `or`, `and` and `not`, the engine tests an empty value as it is, so a watched film without a `rating` breaks `rated_when_watched`.
9
+ `add_constraint` adds a CHECK constraint, a rule every row must satisfy, written as a `where` filter. The engine refuses it while existing rows break the rule and lists them; after that, it enforces the rule on every write. A condition on a column the row leaves empty passes, as in Structured Query Language (SQL). Inside `or`, `and` and `not`, the engine tests an empty value as it is, so a watched film without a `rating` breaks `rated_when_watched`. `hint` says what to do when a row breaks the rule. The refusal puts it first, and `describe_table` lists it in `constraint_hints`.
10
10
 
11
11
  **`add_constraint`**
12
12
 
@@ -14,7 +14,8 @@ A CHECK constraint is a rule written as a `where` filter, such as “a watched f
14
14
  {
15
15
  "table": "film",
16
16
  "name": "rated_when_watched",
17
- "check": { "or": [{ "status": { "ne": "watched" } }, { "rating": { "exists": true } }] }
17
+ "check": { "or": [{ "status": { "ne": "watched" } }, { "rating": { "exists": true } }] },
18
+ "hint": "A watched film needs a rating from 1 to 5: ask me for it."
18
19
  }
19
20
  ```
20
21
 
@@ -28,7 +29,7 @@ Answer:
28
29
  "commits": [
29
30
  { "table": "diary.classics.film", "version": 8, "operation": "ADD CONSTRAINT" }
30
31
  ],
31
- "migration_id": "20261014T191534-add_constraint-add-constraint-rated_when_watc"
32
+ "migration_id": "20261020T173845-add_constraint-add-constraint-rated_when_watc"
32
33
  }
33
34
  ```
34
35
 
@@ -57,7 +58,7 @@ Refused:
57
58
  ```text
58
59
  insert_row refused — nothing was written:
59
60
  invalid datanotes/diary/classics/film/His Girl Friday.md: rated_when_watched violates CHECK rated_when_watched: {"or":[{"status":{"ne":"watched"}},{"rating":{"exists":true}}]}
60
- Hint: The row breaks a CHECK constraint of the table (named in the message, with its rule): change the values so the rule holds. describe_table lists the table's constraints. Then call insert_row again (dry_run: true checks without writing).
61
+ Hint: A watched film needs a rating from 1 to 5: ask me for it. The row breaks a CHECK constraint of the table (named in the message, with its rule): change the values so the rule holds. describe_table lists the table's constraints. Then call insert_row again (dry_run: true checks without writing).
61
62
  Docs: https://datanotes.dev/guides/data-quality/check-constraints.md#enforce-rules-with-check-constraints (offline: read_docs("guides/data-quality/check-constraints#enforce-rules-with-check-constraints"))
62
63
  ```
63
64
 
@@ -85,7 +86,7 @@ Answer:
85
86
  "files_created": 1,
86
87
  "planned_creates": ["datanotes/diary/classics/film/His Girl Friday.md"],
87
88
  "commits": [{ "table": "diary.classics.film", "version": 10, "operation": "WRITE" }],
88
- "migration_id": "20261014T191534-insert_row-insert-into-film-2"
89
+ "migration_id": "20261020T173845-insert_row-insert-into-film"
89
90
  }
90
91
  ```
91
92
 
@@ -107,7 +108,7 @@ Answer:
107
108
  "commits": [
108
109
  { "table": "diary.classics.film", "version": 11, "operation": "DROP CONSTRAINT" }
109
110
  ],
110
- "migration_id": "20261014T191534-drop_constraint-drop-constraint-at_most_three_"
111
+ "migration_id": "20261020T173845-drop_constraint-drop-constraint-at_most_three_"
111
112
  }
112
113
  ```
113
114
 
@@ -148,7 +149,7 @@ Answer:
148
149
  "commits": [
149
150
  { "table": "main.public.genre", "version": 4, "operation": "ADD CONSTRAINT" }
150
151
  ],
151
- "migration_id": "20261014T191519-add_constraint-add-constraint-parent_by_level"
152
+ "migration_id": "20261020T173831-add_constraint-add-constraint-parent_by_level"
152
153
  }
153
154
  ```
154
155
 
@@ -227,34 +228,34 @@ Answer:
227
228
  "ok": true,
228
229
  "op": "batch",
229
230
  "dry_run": false,
230
- "migration_id": "20261014T191519-batch-add-the-silent-era-and-slapsti",
231
+ "migration_id": "20261020T173831-batch-add-the-silent-era-and-slapsti",
231
232
  "steps": [
232
233
  {
233
234
  "index": 0,
234
235
  "op": "insert_row",
235
236
  "ok": true,
236
- "migration_id": "20261014T191519-insert_row-insert-into-genre-4",
237
+ "migration_id": "20261020T173831-insert_row-insert-into-genre-4",
237
238
  "result": {
238
239
  "ok": true,
239
240
  "op": "insert_row",
240
241
  "files_created": 1,
241
242
  "planned_creates": ["datanotes/main/public/genre/Silent cinema.md"],
242
243
  "commits": [{ "table": "main.public.genre", "version": 5, "operation": "WRITE" }],
243
- "migration_id": "20261014T191519-insert_row-insert-into-genre-4"
244
+ "migration_id": "20261020T173831-insert_row-insert-into-genre-4"
244
245
  }
245
246
  },
246
247
  {
247
248
  "index": 1,
248
249
  "op": "insert_row",
249
250
  "ok": true,
250
- "migration_id": "20261014T191519-insert_row-insert-into-genre-5",
251
+ "migration_id": "20261020T173831-insert_row-insert-into-genre-5",
251
252
  "result": {
252
253
  "ok": true,
253
254
  "op": "insert_row",
254
255
  "files_created": 1,
255
256
  "planned_creates": ["datanotes/main/public/genre/Slapstick comedy.md"],
256
257
  "commits": [{ "table": "main.public.genre", "version": 6, "operation": "WRITE" }],
257
- "migration_id": "20261014T191519-insert_row-insert-into-genre-5"
258
+ "migration_id": "20261020T173831-insert_row-insert-into-genre-5"
258
259
  }
259
260
  }
260
261
  ]
@@ -342,7 +343,7 @@ Answer:
342
343
  }
343
344
  ],
344
345
  "commits": [{ "table": "main.public.genre", "version": 7, "operation": "UPDATE" }],
345
- "migration_id": "20261014T191519-update_row-update-genre"
346
+ "migration_id": "20261020T173831-update_row-update-genre"
346
347
  }
347
348
  ```
348
349
 
@@ -35,7 +35,7 @@ You edit `datanotes/main/public/genre/Silent film.md` by hand, outside the engin
35
35
  ```markdown
36
36
  ---
37
37
  table: main.public.genre
38
- id: 01a13bd7-2bd8-71e9-a39c-6ec373b79149
38
+ id: 01a15a64-b310-70dd-a3f1-8f7d07ed2729
39
39
  name: Silent film
40
40
  kind: cinema
41
41
  parent: "[[Cinema]]"
@@ -47,7 +47,7 @@ Read the hand edit’s event. `list_changes` long-polls with `wait_ms` until the
47
47
  **`list_changes`**
48
48
 
49
49
  ```json
50
- { "since": 39, "tables": ["genre"], "wait_ms": 10000 }
50
+ { "since": 45, "tables": ["genre"], "wait_ms": 10000 }
51
51
  ```
52
52
 
53
53
  Answer:
@@ -55,7 +55,7 @@ Answer:
55
55
  ```json
56
56
  [
57
57
  {
58
- "seq": 40,
58
+ "seq": 46,
59
59
  "kind": "update",
60
60
  "path": "datanotes/main/public/genre/Silent film.md",
61
61
  "source": "external",
@@ -152,7 +152,7 @@ Answer:
152
152
  }
153
153
  ],
154
154
  "commits": [{ "table": "main.public.genre", "version": 9, "operation": "UPDATE" }],
155
- "migration_id": "20261014T191521-update_row-update-genre"
155
+ "migration_id": "20261020T173832-update_row-update-genre"
156
156
  }
157
157
  ```
158
158
 
@@ -167,7 +167,7 @@ You edit `datanotes/main/public/watch/2026-10-03T2130.md` by hand, outside the e
167
167
  ```markdown
168
168
  ---
169
169
  table: main.public.watch
170
- id: 01a13bd7-2222-7352-9dea-dd1d7658c61f
170
+ id: 01a15a64-ab35-73d2-a276-9c24d3c889dd
171
171
  film: His Girl Friday
172
172
  occurred_at: 2026-10-03T21:30
173
173
  where: cinema
@@ -226,7 +226,7 @@ Answer:
226
226
  }
227
227
  ],
228
228
  "commits": [{ "table": "main.public.watch", "version": 3, "operation": "RECOMPUTE" }],
229
- "migration_id": "20261014T191518-recompute_columns-recompute-id-day-label-of-watc"
229
+ "migration_id": "20261020T173830-recompute_columns-recompute-id-day-label-of-watc"
230
230
  }
231
231
  ```
232
232
 
@@ -292,7 +292,7 @@ Answer:
292
292
  ```markdown
293
293
  ---
294
294
  table: main.public.watch
295
- id: 01a13bd7-8a28-7d6f-843c-977d6d33e981
295
+ id: 01a15a65-0df1-7af6-9ccd-c58c8b5346c0
296
296
  occurred_at: 2026-09-25T21:00
297
297
  film: "[[Charade]]"
298
298
  where: home
@@ -311,7 +311,7 @@ You edit `datanotes/main/public/film/Metropolis.md` by hand, outside the engine.
311
311
  ```markdown
312
312
  ---
313
313
  table: main.public.film
314
- id: 01a13bd7-4bf3-7a1e-b23c-2769c0dc41d3
314
+ id: 01a15a64-cea3-776e-b81b-389706767aa1
315
315
  year: 1927
316
316
  minutes: 153
317
317
  status: to watch
@@ -384,7 +384,7 @@ You edit `datanotes/main/public/film/Sherlock Jr..md` by hand, outside the engin
384
384
  ```markdown
385
385
  ---
386
386
  table: main.public.film
387
- id: 01a13bd7-3226-73d9-ba6b-80f93c52766a
387
+ id: 01a15a64-b991-7728-99e2-bf13797d5532
388
388
  title: Sherlock Jr.: the projectionist
389
389
  year: 1924
390
390
  status: watched
@@ -471,7 +471,7 @@ You edit `datanotes/main/public/film/Sherlock Jr..md` by hand, outside the engin
471
471
  ```markdown
472
472
  ---
473
473
  table: main.public.film
474
- id: 01a13bd7-3226-73d9-ba6b-80f93c52766a
474
+ id: 01a15a64-b991-7728-99e2-bf13797d5532
475
475
  title: "Sherlock Jr.: the projectionist"
476
476
  year: 1924
477
477
  status: watched
@@ -30,7 +30,7 @@ Answer:
30
30
  "ok": true,
31
31
  "op": "define_base",
32
32
  "planned_texts": ["datanotes/metastore.yaml"],
33
- "migration_id": "20261014T191534-define_base-create-base-entry"
33
+ "migration_id": "20261020T173845-define_base-create-base-entry"
34
34
  }
35
35
  ```
36
36
 
@@ -50,7 +50,7 @@ Answer:
50
50
  "commits": [
51
51
  { "table": "diary.classics.film", "version": 6, "operation": "SET TABLE BASES" }
52
52
  ],
53
- "migration_id": "20261014T191534-set_table_bases-diary-classics-film-extends-en"
53
+ "migration_id": "20261020T173845-set_table_bases-diary-classics-film-extends-en"
54
54
  }
55
55
  ```
56
56
 
@@ -118,14 +118,14 @@ Answer:
118
118
  "rows": [
119
119
  {
120
120
  "path": "datanotes/diary/classics/actors/Max Schreck.md",
121
- "id": "01a13bd7-65fe-747a-b7cc-7c6c8d09e39d",
121
+ "id": "01a15a64-e8e3-73eb-9164-1fc8de45795b",
122
122
  "added_on": "2026-10-05",
123
123
  "note": "The face of Nosferatu",
124
124
  "_source": "diary.classics.actors"
125
125
  },
126
126
  {
127
127
  "path": "datanotes/diary/classics/film/Nosferatu.md",
128
- "id": "01a13bd7-659b-7233-a73c-4f8191e95b0e",
128
+ "id": "01a15a64-e863-705a-96ae-42d6c22c878b",
129
129
  "added_on": "2026-10-05",
130
130
  "note": null,
131
131
  "_source": "diary.classics.film"
@@ -169,7 +169,7 @@ Answer:
169
169
  "commits": [
170
170
  { "table": "diary.classics.actors", "version": 3, "operation": "DEFINE BASE" }
171
171
  ],
172
- "migration_id": "20261014T191534-define_base-alter-base-entry"
172
+ "migration_id": "20261020T173845-define_base-alter-base-entry"
173
173
  }
174
174
  ```
175
175
 
@@ -246,7 +246,7 @@ Answer:
246
246
  "commits": [
247
247
  { "table": "diary.classics.actors", "version": 4, "operation": "DEFINE BASE" }
248
248
  ],
249
- "migration_id": "20261014T191534-define_base-alter-base-entry-2"
249
+ "migration_id": "20261020T173845-define_base-alter-base-entry-2"
250
250
  }
251
251
  ```
252
252
 
@@ -280,7 +280,7 @@ Answer:
280
280
  "ok": true,
281
281
  "op": "define_base",
282
282
  "planned_texts": ["datanotes/metastore.yaml"],
283
- "migration_id": "20261014T191534-define_base-create-base-credited"
283
+ "migration_id": "20261020T173845-define_base-create-base-credited"
284
284
  }
285
285
  ```
286
286
 
@@ -300,7 +300,7 @@ Answer:
300
300
  "commits": [
301
301
  { "table": "diary.classics.actors", "version": 5, "operation": "SET TABLE BASES" }
302
302
  ],
303
- "migration_id": "20261014T191534-set_table_bases-diary-classics-actors-extends--2"
303
+ "migration_id": "20261020T173845-set_table_bases-diary-classics-actors-extends--2"
304
304
  }
305
305
  ```
306
306
 
@@ -329,7 +329,7 @@ Answer:
329
329
  "ok": true,
330
330
  "op": "define_base",
331
331
  "planned_texts": ["datanotes/metastore.yaml"],
332
- "migration_id": "20261014T191534-define_base-create-base-tree"
332
+ "migration_id": "20261020T173845-define_base-create-base-tree"
333
333
  }
334
334
  ```
335
335
 
@@ -369,7 +369,7 @@ Answer:
369
369
  "commits": [
370
370
  { "table": "diary.classics.genre", "version": 1, "operation": "SET TABLE BASES" }
371
371
  ],
372
- "migration_id": "20261014T191534-set_table_bases-diary-classics-genre-extends-t"
372
+ "migration_id": "20261020T173845-set_table_bases-diary-classics-genre-extends-t"
373
373
  }
374
374
  ```
375
375
 
@@ -475,6 +475,6 @@ Answer:
475
475
  "operation": "SET TABLE BASES"
476
476
  }
477
477
  ],
478
- "migration_id": "20261014T191534-set_table_bases-diary-classics-genre_draft-ext"
478
+ "migration_id": "20261020T173845-set_table_bases-diary-classics-genre_draft-ext"
479
479
  }
480
480
  ```