@firedrill-tools/notion 0.1.1

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 (65) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +402 -0
  3. package/firedrill/agent.target.json +17 -0
  4. package/firedrill/baseline.scenario.json +5 -0
  5. package/firedrill/bounded.scenario.json +19 -0
  6. package/firedrill/conformance.suite.json +23 -0
  7. package/firedrill/notion-bounded.drill.json +318 -0
  8. package/firedrill/notion-byte-budget.drill.json +116 -0
  9. package/firedrill/notion-mcp-aliases.drill.json +150 -0
  10. package/firedrill/notion-page-authoring.drill.json +254 -0
  11. package/firedrill/notion-rate-limited.drill.json +118 -0
  12. package/firedrill/notion-schema-growth.drill.json +88 -0
  13. package/firedrill/notion-scope-agent-only.drill.json +131 -0
  14. package/firedrill/notion-scope-auditor.drill.json +86 -0
  15. package/firedrill/notion-scope-board-bot.drill.json +128 -0
  16. package/firedrill/notion-scope-notes-bot.drill.json +303 -0
  17. package/firedrill/notion-scope-stranger.drill.json +773 -0
  18. package/firedrill/notion-task-triage.drill.json +277 -0
  19. package/firedrill/notion-trash-and-restore.drill.json +186 -0
  20. package/firedrill/notion-update-lost.drill.json +88 -0
  21. package/firedrill/notion-workspace-read.drill.json +258 -0
  22. package/firedrill/notion-write-unavailable.drill.json +161 -0
  23. package/firedrill/rate-limited.scenario.json +11 -0
  24. package/firedrill/tools/notion/app/assets/ATTRIBUTION.md +35 -0
  25. package/firedrill/tools/notion/app/assets/fonts/OFL.txt +93 -0
  26. package/firedrill/tools/notion/app/assets/fonts/inter-latin.woff2 +0 -0
  27. package/firedrill/tools/notion/app/assets/notion-wordmark.svg +1 -0
  28. package/firedrill/tools/notion/app/assets/notion.svg +1 -0
  29. package/firedrill/tools/notion/app/site/app.js +797 -0
  30. package/firedrill/tools/notion/app/site/assets/fonts/inter-latin.woff2 +0 -0
  31. package/firedrill/tools/notion/app/site/assets/notion-wordmark.svg +1 -0
  32. package/firedrill/tools/notion/app/site/assets/notion.svg +1 -0
  33. package/firedrill/tools/notion/app/site/chrome.js +104 -0
  34. package/firedrill/tools/notion/app/site/cover-picker.js +83 -0
  35. package/firedrill/tools/notion/app/site/database.js +648 -0
  36. package/firedrill/tools/notion/app/site/editors.js +320 -0
  37. package/firedrill/tools/notion/app/site/format-bar.js +97 -0
  38. package/firedrill/tools/notion/app/site/icons.js +131 -0
  39. package/firedrill/tools/notion/app/site/index.html +125 -0
  40. package/firedrill/tools/notion/app/site/page.js +826 -0
  41. package/firedrill/tools/notion/app/site/rich.js +159 -0
  42. package/firedrill/tools/notion/app/site/state.js +170 -0
  43. package/firedrill/tools/notion/app/site/styles.css +826 -0
  44. package/firedrill/tools/notion/app/site/ui.js +418 -0
  45. package/firedrill/tools/notion/behavior.mjs +1123 -0
  46. package/firedrill/tools/notion/lib/blocks.mjs +371 -0
  47. package/firedrill/tools/notion/lib/identity.mjs +123 -0
  48. package/firedrill/tools/notion/lib/ids.mjs +63 -0
  49. package/firedrill/tools/notion/lib/json-depth.mjs +26 -0
  50. package/firedrill/tools/notion/lib/markdown.mjs +381 -0
  51. package/firedrill/tools/notion/lib/properties.mjs +513 -0
  52. package/firedrill/tools/notion/lib/query.mjs +272 -0
  53. package/firedrill/tools/notion/lib/render.mjs +137 -0
  54. package/firedrill/tools/notion/lib/rich-text.mjs +134 -0
  55. package/firedrill/tools/notion/lib/size.mjs +44 -0
  56. package/firedrill/tools/notion/lib/state.mjs +192 -0
  57. package/firedrill/tools/notion/lib/wire.mjs +89 -0
  58. package/firedrill/tools/notion/notion.tool.json +9837 -0
  59. package/firedrill/update-lost.scenario.json +11 -0
  60. package/firedrill/world.json +7039 -0
  61. package/firedrill/write-unavailable.scenario.json +11 -0
  62. package/firedrill.json +5 -0
  63. package/package.json +63 -0
  64. package/starter.json +6482 -0
  65. package/test/conformance.mjs +1186 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Reload Tech Inc.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # @firedrill-tools/notion
2
+
3
+ A synthetic **Notion workspace** for [Firedrill](https://firedrill.run): one fictional workspace with a wiki page tree,
4
+ three databases (the 2025-09-03 *database container + data source* model), blocks, comments and users, exposed through a
5
+ bounded subset of the Notion REST API (`/v1/...`, `Notion-Version: 2025-09-03` shapes plus the 2026-03-11 page-markdown
6
+ endpoints) and through the tool names of the open-source Notion MCP server. Agents that talk to Notion through
7
+ `@notionhq/client`-style REST calls or over MCP can be exercised against it without a workspace, an integration token or
8
+ network access.
9
+
10
+ Everything is computed from the world's state: ids come from a counter row, timestamps from Firedrill's virtual clock,
11
+ visibility and capabilities from the calling integration. Nothing here contacts Notion; no notification, e-mail or webhook
12
+ is ever sent, and no `notion.so` URL in a response resolves anywhere.
13
+
14
+ Package id `notion`, version `0.1.0`, engine `>=0.1.0 <0.2.0`, license Apache-2.0. The package ships a **browser app**
15
+ (a recreation of the Notion web client over the same operations — see *App*) next to the REST routes and MCP aliases.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ firedrill tool add @firedrill-tools/notion --install
21
+ firedrill serve --scenario baseline --no-open
22
+ ```
23
+
24
+ `firedrill serve` prints the HTTP and MCP endpoints and their tokens. A new world receives the starter data and the
25
+ grants below. Existing worlds do not change silently: add the grants and (optionally) the `starter.json` rows to your
26
+ own world or scenario.
27
+
28
+ ### Grants and identities
29
+
30
+ Every actor that should use the Tool needs the grants for the operations it may call (`packageId: "notion"`):
31
+
32
+ ```
33
+ users.me users.list users.get search
34
+ pages.create pages.retrieve pages.update pages.retrieve-property pages.move pages.retrieve-markdown pages.update-markdown
35
+ databases.create databases.retrieve data-sources.retrieve data-sources.query data-sources.update
36
+ blocks.retrieve blocks.children.list blocks.children.append blocks.update blocks.delete
37
+ comments.create comments.list workspace.context workspace.trash
38
+ ```
39
+
40
+ Two optional actor **attributes** model *which internal integration* is calling and *which person* it acts for:
41
+
42
+ | attribute | meaning |
43
+ |---|---|
44
+ | `integrationId` (optional) | Row id in the `integrations` namespace. Selects the bot user returned by `users.me`, the capability set (`content`, `comments`, `user_information`) and the visibility scope (`workspace` or a list of shared root pages/databases). **Absent = the first `integrations` row in row-id order** — the seeded workspace-wide *Firedrill Agent* — so a grant-only actor created by `firedrill tool add --install` sees the whole starter workspace. An id that names no row fails every operation with `UNAUTHORIZED` (HTTP 401 `unauthorized`, "The bearer token is not valid."). |
45
+ | `userId` (optional) | Row id of a **person** user. When present, writes are attributed (`created_by`, `last_edited_by`, comment author with `display_name.type: "user"`) to that person and `workspace.context.user` returns them. When absent, writes are attributed to the integration's bot user, exactly as the real API does. An id that is not a person row also fails `UNAUTHORIZED`. |
46
+
47
+ The seeded integrations are `fd000000-0000-4000-8000-000700000001` **Firedrill Agent** (`content: read_update_insert`,
48
+ `comments: read_insert`, `user_information: with_emails`, workspace-wide), `…000700000002` **Notes Sync** (`content: read`,
49
+ `comments: read`, `user_information: none`, shared only with the *Meeting notes* subtree) and `…000700000003` **Status
50
+ Board** (`content: read`, `comments: none`, `user_information: without_emails`, workspace-wide). The conformance world
51
+ (`firedrill/world.json`) shows every combination as actors `member`, `agent-only`, `notes-bot`, `board-bot`, `stranger`
52
+ and `auditor`.
53
+
54
+ Rules the behavior enforces: an object is visible only if it, or an ancestor in its parent chain, is one of the
55
+ integration's shared roots (Notion never distinguishes "missing" from "not shared": both answer `object_not_found`);
56
+ trashed objects stay readable by id (`in_trash: true`) but leave search, queries and children listings; every content
57
+ read needs `content ≥ read`, updates/moves/markdown edits/block edits/deletes `read_update`, creating pages, databases and
58
+ blocks `read_update_insert`, listing comments `comments ≥ read`, creating them `read_insert`, and `users.list` /
59
+ `users.get` need `user_information ≠ none` (`person.email` appears only with `with_emails`). Violations answer
60
+ `RESTRICTED_RESOURCE` (HTTP 403 `restricted_resource`). Framework grants are checked before any of this: an actor
61
+ without the grant is denied by Firedrill (HTTP 403 with a Notion-shaped `restricted_resource` body).
62
+
63
+ ### Connections
64
+
65
+ Two connection recipes are declared: `notion-sdk` (`NOTION_BASE_URL` = the HTTP binding, `NOTION_TOKEN` = its token, to be
66
+ passed as `baseUrl` / `auth` to `@notionhq/client` in a test-only process) and `notion-mcp` (`NOTION_MCP_URL` /
67
+ `NOTION_MCP_TOKEN` for the MCP binding). The values are isolated world tokens for test processes only. Neither client has
68
+ been exercised against this package yet (see *Compatibility*).
69
+
70
+ ## Starting data
71
+
72
+ `starter.json` (and `firedrill/world.json`) contain 103 fictional rows, virtual time `2026-09-14T09:00:00Z`: the workspace
73
+ **Halvard Robotics** (`@halvard.example`), five people (Ines Okafor, Tomas Lindqvist, Priya Raman, Marco Belli and the
74
+ guest Yuki Sato), three bot users, three integrations, a wiki of nine pages (*Halvard Robotics Home* at the workspace root
75
+ → *Engineering* → *Onboarding guide* and the empty *Empty scratchpad*; *Design principles* with 13 top-level blocks
76
+ including a toggle with children, numbered and bulleted lists, a quote, a callout, a code block, a divider and a bookmark;
77
+ *Meeting notes* → two *Weekly sync* pages; and *Roadmap 2025* in the trash), three databases — **Projects** (5 rows;
78
+ status, people, date range, relation to Tasks), **Tasks** (14 rows; status with groups, assignee, due date, priority
79
+ select, tags multi-select, checkbox, number, url, rich text with a user mention, relation to Projects, computed
80
+ `created_by` / `last_edited_time`) and the empty inline **Decisions log** — 49 blocks and 6 comments in four discussions.
81
+ The data deliberately covers an unassigned task, a task due "today", an overdue one, one without a due date, an `Urgent`
82
+ one, three titles containing "Firmware", shared tags, a bot-authored comment and an unshared subtree.
83
+
84
+ Row ids are UUID-shaped (`fd000000-0000-4000-8000-KKKKNNNNNNNN`, `KKKK` = kind, `NNNNNNNN` = sequence) so `format: uuid`
85
+ clients accept them; seeded rows use sequences below `0x1000`, created rows take the next value of `meta/counters`
86
+ (`next_id`, shared by rows, property ids and select-option ids). Ids are accepted dashed or as 32 hex characters and always
87
+ returned dashed.
88
+
89
+ ## Operations
90
+
91
+ Every operation has one canonical shape: the **flattened Notion request** (path + query + body fields in one snake_case
92
+ object) in, and the **exact Notion response body** out. That is also the shape the Notion MCP server exposes per tool, so
93
+ the aliases carry the same schemas, and the HTTP codecs only move fields between the path/query and that object.
94
+
95
+ | operation | MCP alias | HTTP route |
96
+ |---|---|---|
97
+ | `users.me` | `API-get-self` | `GET /v1/users/me` (served by the `get-user` route, see *Compatibility*) |
98
+ | `users.list` | `API-get-users` | `GET /v1/users` |
99
+ | `users.get` | `API-get-user` | `GET /v1/users/{user_id}` |
100
+ | `search` | `API-post-search` | `POST /v1/search` |
101
+ | `pages.create` | `API-post-page` | `POST /v1/pages` |
102
+ | `pages.retrieve` | `API-retrieve-a-page` | `GET /v1/pages/{page_id}` |
103
+ | `pages.update` | `API-patch-page` | `PATCH /v1/pages/{page_id}` |
104
+ | `pages.retrieve-property` | `API-retrieve-a-page-property` | `GET /v1/pages/{page_id}/properties/{property_id}` |
105
+ | `pages.move` | `API-move-page` | `POST /v1/pages/{page_id}/move` |
106
+ | `pages.retrieve-markdown` | `API-retrieve-page-markdown` | `GET /v1/pages/{page_id}/markdown` |
107
+ | `pages.update-markdown` | `API-update-page-markdown` | `PATCH /v1/pages/{page_id}/markdown` |
108
+ | `databases.create` | — | `POST /v1/databases` |
109
+ | `databases.retrieve` | `API-retrieve-a-database` | `GET /v1/databases/{database_id}` |
110
+ | `data-sources.retrieve` | `API-retrieve-a-data-source` | `GET /v1/data_sources/{data_source_id}` |
111
+ | `data-sources.query` | `API-query-data-source` | `POST /v1/data_sources/{data_source_id}/query` and legacy `POST /v1/databases/{database_id}/query` |
112
+ | `data-sources.update` | `API-update-a-data-source` | `PATCH /v1/data_sources/{data_source_id}` |
113
+ | `blocks.retrieve` | `API-retrieve-a-block` | `GET /v1/blocks/{block_id}` |
114
+ | `blocks.children.list` | `API-get-block-children` | `GET /v1/blocks/{block_id}/children` |
115
+ | `blocks.children.append` | `API-patch-block-children` | `PATCH /v1/blocks/{block_id}/children` |
116
+ | `blocks.update` | `API-update-a-block` | `PATCH /v1/blocks/{block_id}` |
117
+ | `blocks.delete` | `API-delete-a-block` | `DELETE /v1/blocks/{block_id}` (200 + block object, as Notion) |
118
+ | `comments.create` | `API-create-a-comment` | `POST /v1/comments` |
119
+ | `comments.list` | `API-retrieve-a-comment` | `GET /v1/comments?block_id=` |
120
+ | `workspace.context` | — | — (canonical only: workspace, acting user, resolved integration, `now` from virtual time, row bounds) |
121
+ | `workspace.trash` | — | — (canonical only: the trash roots — trashed pages and databases whose own parent is live — newest deletion first, cursor-paginated; backs the app's Trash panel, Notion has no public trash listing) |
122
+
123
+ Canonical names are also reachable as `POST /v1/operations/notion/<operation>` and as MCP tools `notion.<operation>`.
124
+ Mutations accept an optional `X-Firedrill-Idempotency-Key` request header (Notion clients never send one).
125
+
126
+ Behaviour highlights, all computed from state:
127
+
128
+ - **Property types**: `title`, `rich_text`, `number`, `select`, `multi_select`, `status`, `date`, `checkbox`, `url`,
129
+ `email`, `phone_number`, `people`, `relation` (single direction), and the computed `created_time`, `created_by`,
130
+ `last_edited_time`, `last_edited_by`. Values are validated against the data source's schema (`X is not a property that
131
+ exists.`, `Status is expected to be status.`, unknown status option, relation outside the related source, computed
132
+ property in a write → `400 validation_error`); unknown `select` / `multi_select` option names are added to the schema
133
+ as Notion does. `null` clears a value; a create fills omitted properties with empty values. Plain pages hold exactly
134
+ `title` (`properties: { title: [...] }` and `{ title: { title: [...] } }` are both accepted).
135
+ - **Query** (`data-sources.query`): Notion's per-type condition table (text `equals … is_not_empty`, number comparisons,
136
+ checkbox, select/status `equals`, multi-select/people/relation `contains`, date `before/after/on_or_before/on_or_after/
137
+ equals` and the relative windows `this_week`, `past_week/month/year`, `next_week/month/year` computed from virtual
138
+ time; date-only values compare by UTC calendar day, weeks start on Monday), `and`/`or` compounds nested at most two
139
+ levels with ≤ 100 conditions per group, `sorts` by property or `timestamp` applied stably (empties last, select/status
140
+ by option order), `filter_properties`, `in_trash` / `archived`, cursor pagination (`page_size` 1–100, `start_cursor` =
141
+ id of the last item; unknown → `The start_cursor provided is invalid.`). Anything else answers `400 validation_error`
142
+ with Notion's `body.filter…` wording.
143
+ - **Search**: case-insensitive substring match of every whitespace-separated token against page and data-source titles,
144
+ `filter.value: page | data_source`, `sort.timestamp: last_edited_time` ascending/descending (default descending),
145
+ cursor pagination; trashed and unshared objects are excluded.
146
+ - **Blocks**: `paragraph`, `heading_1/2/3`, `bulleted_list_item`, `numbered_list_item`, `to_do`, `toggle`, `quote`,
147
+ `callout`, `code`, `divider`, `bookmark`, `image` (external only) plus the maintained `child_page` / `child_database`
148
+ (their block id equals the page / database id). Appends take ≤ 100 children nested ≤ 2 levels, at the end or
149
+ `after` a sibling; updates replace `rich_text` wholly and merge `checked`, `color`, `language`, `icon`, `url`, `caption`
150
+ (the MCP server's `type: { <type>: … }` wrapper is accepted; a type change is rejected); deletes trash the subtree
151
+ (`archived: false` restores). Rich text supports `text` (annotations, link) and `mention` (`user`, `page`, `date`);
152
+ items are capped at 2000 characters (link URLs at 2000) and 100 per array, and one rich-text array may render to at most
153
+ 800 000 UTF-8 bytes (the text is sent twice, as `text.content` and `plain_text`).
154
+ - **Trash**: `pages.update { in_trash: true }` (or `archived`) trashes the page, its blocks, child pages and child
155
+ databases; `in_trash: false` restores the subtree; editing a trashed object answers `Can't edit block that is archived…`.
156
+ `data-sources.update { in_trash: true }` trashes the source and its pages. Restore is a whole-subtree operation and
157
+ diverges from Notion here: blocks deleted individually with `blocks.delete` before the page was trashed come back
158
+ when the page is restored.
159
+ - **Move**: into a page (properties reduced to the title, a `child_page` block appears at the end of the target) or
160
+ into a data source (title kept under the target's title property, other values re-created empty); moving a page into
161
+ itself, a descendant or a trashed parent is rejected.
162
+ - **Markdown** (`pages.retrieve-markdown` / `pages.update-markdown`): a deterministic dialect — `#`/`##`/`###`,
163
+ paragraphs, `- ` bullets, `1. ` numbered items (renumbered), `- [ ] / - [x]` to-dos, `> ` quotes, `> <emoji> text`
164
+ callouts, fenced code with the Notion language name, `---`, `<details><summary>…</summary>` toggles, two-space
165
+ indentation for nested list children, inline `**bold**`, `*italic*`, `~~strike~~`, `` `code` ``, `[text](url)`,
166
+ user mentions as `[@Name](notion://user/<id>)`, child pages / databases as `[📄 Title](notion://page/<id>)` /
167
+ `[🗃️ Title](notion://database/<id>)` (kept in place, never duplicated, never removable through markdown), and
168
+ `bookmark` / `image` blocks as `<unknown type="…" id="…" url="…"/>` listed in `unknown_block_ids`. Nesting
169
+ (`<details>`, indented list children) follows the append rule — at most two levels below a top-level block, else the
170
+ append's `body.content[0]….children nests blocks deeper than 2 levels` error — and parsing is linear in the content
171
+ length (an unclosed `[`, `` ` ``, `**` or `~~` stays plain text). `replace_content`
172
+ needs `allow_deleting_content: true` when blocks would be removed; `update_content` applies `old_str` → `new_str`
173
+ (`replace_all_matches` for several hits; zero or ambiguous hits are rejected; `new_str` is literal text) and edits
174
+ unchanged blocks in place — at most 100 `content_updates` per request, and a step that would grow the markdown past
175
+ `max_markdown_bytes` is rejected before it is built (`the resulting markdown exceeds 102400 bytes (at
176
+ body.content_updates[i])`);
177
+ `insert_content` adds at `position.type: start | end`. `replace_content_range` and `allow_async` are rejected.
178
+ - **Comments**: `comments.create` with `parent.page_id` starts a discussion, with `discussion_id` joins one;
179
+ `comments.list` lists a page's or block's comments in creation order, paginated.
180
+ - **Bounds**: every scan is bounded by `meta/limits.max_rows_per_namespace` (default 10 000) and fails with
181
+ `FAILED_PRECONDITION` (HTTP 400 `validation_error`, "state exceeds the supported bound of N rows in <namespace>")
182
+ instead of truncating; `max_page_size` 100, `max_children_per_append` 100, `max_markdown_bytes` 102 400 (UTF-8 bytes), 1000 block
183
+ elements per parsed markdown document, 100 `content_updates` per request. Block positions are gapless among a
184
+ parent's live children; trashed blocks keep their last position (a restored block returns near it). Lower the
185
+ `limits` row in a scenario to prove the failure (the shipped `bounded` scenario uses 4).
186
+
187
+ ### Errors
188
+
189
+ Tool errors are rendered with Notion's envelope `{ "object": "error", "status", "code", "message", "request_id" }`:
190
+ `VALIDATION_ERROR` → 400 `validation_error`, `FAILED_PRECONDITION` → 400 `validation_error`, `UNAUTHORIZED` → 401
191
+ `unauthorized`, `RESTRICTED_RESOURCE` → 403 `restricted_resource`, `OBJECT_NOT_FOUND` → 404 `object_not_found`,
192
+ `CONFLICT_ERROR` → 409 `conflict_error`, `RATE_LIMITED` → 429 `rate_limited` (+ `Retry-After: 1`), `SERVICE_UNAVAILABLE` →
193
+ 503 `service_unavailable`. Messages follow Notion's wording where known (`Could not find page with ID: … Make sure the
194
+ relevant pages and databases are shared with your integration.`, `path.page_id should be a valid uuid`, `query failed
195
+ validation: query.page_size should be ≤ 100, instead was 250.`). Every validation message names the section the value
196
+ came from, so a body value is reported as `body.parent.page_id`, a query value as `query.block_id` and a path segment
197
+ as `path.page_id`. Framework outcomes keep their statuses — schema-invalid
198
+ input 400, missing grant 403, unknown route 404/405 — but the bodies of the declared routes are Notion-shaped too.
199
+
200
+ Query `page_size` on `GET /v1/users`, `GET /v1/blocks/{block_id}/children`, `GET /v1/comments` and
201
+ `GET /v1/pages/{page_id}/properties/{property_id}` is never rejected by the route codec: a value that is not a plain
202
+ decimal integer (`abc`, empty, `1e999`, `1.5`) answers 400 `validation_error` `query failed validation: query.page_size
203
+ should be a number, instead was \`"abc"\`.`, a non-integer number answers `should be an integer`, a value below the range
204
+ (`0`, `-1`) answers `should be ≥ 1, instead was 0.` and one above it (`101`, `99999999999`) `should be ≤ 100`. The same
205
+ messages are used for `page_size` in a JSON body (`POST /v1/search`, data source and database query, canonical
206
+ `workspace.trash`), where the field is named `body.page_size` and any JSON type reaches the handler, so
207
+ `{"page_size":"abc"}` answers Notion's message rather than a schema rejection. Non-string values in validation messages
208
+ are printed as JSON (`instead was \`{}\``).
209
+
210
+ `property_id` path segments are decoded once by the server. A property matches when its id or name equals that value,
211
+ when its (URL-encoded, Notion-style) id decodes to it, or when the value itself decodes once more to an id or name — so
212
+ both `/properties/%3AUPp` and a client-side re-encoded `/properties/%253AUPp` resolve an id stored as `%3AUPp`, and
213
+ percent escapes are compared case-insensitively, so a client that re-encodes with lower-case hexadecimal (`%3a…`)
214
+ resolves the same property. A
215
+ malformed leftover encoding (`%25`, `%25E0%25A4%25A`, `%25ZZ`) never throws; it answers 400 `validation_error`
216
+ `Could not find property with name or id: %`. Error messages that echo caller input (ids, property keys) are capped at the
217
+ framework's 1000-character outcome-message limit and end with `…` when cut.
218
+
219
+ ## Events and faults
220
+
221
+ | id | kind | when |
222
+ |---|---|---|
223
+ | `page.created` | event | `pages.create` (`entity_id`, `parent_type`, `parent_id`, `database_id`, `author_id`) |
224
+ | `page.properties_updated` | event | `pages.update` with properties/icon/cover (`updated_properties` = property ids, `icon`, `cover`); `pages.move` (`updated_properties: []`) |
225
+ | `page.content_updated` | event | `pages.create` with `children`, `blocks.children.append`, `blocks.update`, `blocks.delete`, `pages.update-markdown` (`updated_blocks`) |
226
+ | `page.deleted` / `page.undeleted` | event | `pages.update` trashing / restoring a live / trashed page |
227
+ | `comment.created` | event | `comments.create` (`discussion_id`, `parent_type`, `parent_id`, `page_id`) |
228
+ | `rate-limited` | fault (`before`) | `search`, `data-sources.query`, `blocks.children.list`, `pages.retrieve` answer 429 `rate_limited` with `Retry-After: 1` |
229
+ | `write-unavailable` | fault (`before`) | `pages.create`, `pages.update`, `blocks.children.append`, `comments.create` answer 503 `service_unavailable`; nothing is written |
230
+ | `update-committed-response-lost` | fault (`after_commit`) | `pages.update` commits but the caller sees 409 `conflict_error` — models Notion's transient save conflict where a retry must re-read first |
231
+
232
+ Scenarios `rate-limited`, `write-unavailable`, `update-lost` and `bounded` ship in `firedrill/`; no subscriptions or
233
+ callbacks are declared (attach your own callbacks to the events above).
234
+
235
+ ## App
236
+
237
+ `firedrill serve` also serves a browser app (declared as `ui: { root: "app/site" }`) that recreates the Notion web
238
+ client a workspace member sees, with every control wired to the operations above through `/_firedrill/client.js`:
239
+
240
+ - **Sidebar**: workspace switcher (workspace icon and name; the menu shows the acting person or bot and the integration's
241
+ capabilities), Search (`⌘/Ctrl K`), Home, Inbox (every comment of the 25 most recently edited live pages, read with
242
+ `start_cursor`; it says *Showing comments from the 25 most recently edited pages.* when more exist), the *Private* page tree (roots from `search`, children loaded on expand
243
+ from `blocks.children.list` in block order, full-page databases included, inline databases and database rows excluded),
244
+ Settings / Templates / Trash / Invite members panels (lists the app loads with a request cap — the page index, a page's
245
+ subpages, workspace members, a page's comments — show a notice when the cap trips instead of hiding rows), `+` to create pages (`pages.create`; integrations cannot create at
246
+ the workspace root, so new top-level pages go under the first root page and the app says so).
247
+ - **Home**: greeting from virtual time, *Upcoming events* (not simulated: no calendar connection), *Recently visited* cards (index sorted by `last_edited_time` — the API has no visit
248
+ history) and a *Home database view* widget: the first data source with a people + status property, queried with
249
+ `people contains <acting user>`.
250
+ - **Page**: icon (emoji picker → `pages.update icon`), title (`pages.update`), property panel for database rows with the
251
+ right editor per type (text/number/url/email/phone inputs, date with optional end date, select/status/multi-select
252
+ option pickers that can create options, people picker from `users.list`, relation picker backed by
253
+ `data-sources.query`, checkbox), page comments under the title (`comments.list`, reply / new discussion via
254
+ `comments.create`), the block editor (`blocks.children.list` paginated with *Load more*; plain-text editing of text
255
+ blocks on blur → `blocks.update`, Enter appends a block after the current one, Backspace on an empty block deletes it,
256
+ `/` opens the block menu → `blocks.children.append` with `after`; to-do checkboxes; toggles load children on expand;
257
+ the `⋮⋮` handle menu offers Comment and Delete (confirmed) → `blocks.delete`; child pages / inline databases render in
258
+ place), the top-bar comments panel, and the `…` menu with Copy link, Markdown (`pages.retrieve-markdown` with a
259
+ *Replace page content* editor → `pages.update-markdown replace_content`, confirmed), Move to (`pages.move`) and Move
260
+ to Trash (confirmed, with Undo). Trashed pages show the red banner with *Restore page*.
261
+ - **Database** (full page and inline): Table and Board views, *Filter* builder composing real `data-sources.query`
262
+ filters (per-type conditions, `and`), *Sort* menu, search-in-view (title `contains`), *New* (`pages.create` in the
263
+ data source, then the side peek), inline cell editing (`pages.update`), *Load 50 more* pagination, `COUNT` footer,
264
+ column menu (sort, filter, rename, delete) and the *Properties* editor (`data-sources.update`: add, rename, delete).
265
+ Board groups by the first status (else select) property client-side over the same query.
266
+ - **States**: skeleton loading, empty page / *No pages yet* / *No results* / empty trash and inbox, the product's
267
+ "This content doesn't exist or you don't have access." page for `object_not_found`, capability and framework denials
268
+ as toasts or the *You don't have access* page, a rate-limit retry state, a connection screen when the app is opened
269
+ outside its local link; every timestamp and "today" comes from `workspace.context.now`, never the browser clock; the
270
+ page re-fetches when `getContext().revision` changes unless the viewer is typing; mutations carry idempotency keys and
271
+ destructive actions confirm. Usable at phone width (the sidebar becomes a drawer).
272
+
273
+ The app uses the same records as the API: a page created over REST appears in the sidebar within two seconds; a cell edited
274
+ in the table is visible to `pages.retrieve`. External images and bookmarks are never fetched (the serving policy blocks
275
+ remote requests), so image blocks and covers render as frames with the URL as caption. Assets: the official Notion logo
276
+ files under `firedrill/tools/notion/app/assets/` (sources in `ATTRIBUTION.md`) and the Inter typeface (SIL OFL).
277
+
278
+ ## Conformance
279
+
280
+ `firedrill tool test notion` runs 16 drills (`firedrill/*.drill.json`, suite `conformance`) with a Node-built-ins-only
281
+ target (`test/conformance.mjs`, bindings `http` + `mcp`) that exercises every operation, every declared error, every
282
+ event and every fault, then re-runs the suite to prove determinism: workspace reads with pagination and validation
283
+ errors, task triage with real filters/sorts/create/update/move/comments, page authoring with blocks, markdown and a new
284
+ database, trash and restore, the MCP aliases, four identity/scope drills, the framework-denied actor, and one drill per
285
+ fault plus the bounded scenario, and a byte-budget drill (oversized rich text refused with nothing stored; near-limit
286
+ blocks, comments, search results and query rows read back once each across byte-filled pages under 1 MiB; oversized
287
+ markdown and database writes answer `validation_error`), and a schema-growth drill (a data source schema is refused at
288
+ exactly one byte over what it may add to every page, accepted at the limit, and its rows stay readable).
289
+
290
+ ## Compatibility
291
+
292
+ - **Not verified against a real client.** `manifest.compatibility` is empty: neither `@notionhq/client` nor
293
+ `@notionhq/notion-mcp-server` has been run against `firedrill serve` yet. The MCP aliases reproduce the tool names and
294
+ flattened argument shapes of the open-source Notion MCP server 2.x (`API-` + OpenAPI operationId) as read from its
295
+ tool schemas; the hosted `mcp.notion.com` contract (`notion-search`, `notion-fetch`, …) is a different, undocumented
296
+ surface and is **not** claimed.
297
+ - `GET /v1/users/me` is served by the `get-user` route (`users.get` with `user_id: "me"`, no user-information capability
298
+ needed) because the framework rejects a literal path segment next to the `/v1/users/{user_id}` template. The canonical
299
+ `users.me` operation and its `API-get-self` alias are unaffected; in drill evidence a `GET /v1/users/me` counts as a
300
+ `users.get` call.
301
+ - `Notion-Version` is accepted but neither enforced nor echoed; responses always use the 2025-09-03 shapes (database
302
+ containers without `properties`, `parent.type: data_source_id` on database pages, `type: page_or_data_source` lists)
303
+ plus the 2026-03-11 markdown endpoints. A 2022-06-28 client works only through the legacy `POST /v1/databases/{id}/query`
304
+ and `pages.create` with `parent.database_id`. `missing_version` is never returned.
305
+ - Unsupported Notion paths (`/v1/oauth/*`, `/v1/file_uploads*`, `POST /v1/data_sources`, `PATCH /v1/databases/{id}`,
306
+ data-source templates, …) do not exist: the framework answers 404 (or 405 when only other methods exist on the path).
307
+ Never a plausible stub.
308
+
309
+ ## Limitations
310
+
311
+ - Single workspace, single-token model: no OAuth, no public integrations, no multi-workspace tokens; `users.me` always
312
+ returns a bot owned by the workspace.
313
+ - Property types limited to the list above — no formulas, rollups, files, unique ids, verification, buttons or places;
314
+ relations are single-direction (no synced property); `people` values must be workspace users; `date.time_zone` is
315
+ stored, not interpreted.
316
+ - Block types limited to the list above — `table`, columns, synced blocks, embeds, media uploads, `link_to_page` and
317
+ direct `child_page` / `child_database` appends are rejected with `validation_error`; `after` is supported but the
318
+ 2026-03-11 `position` object for appends is not.
319
+ - The markdown dialect is a deterministic subset, not Notion's enhanced markdown: text containing markdown markers does
320
+ not escape, date mentions render as plain text, `replace_content_range`, `allow_async` (202 tasks) and `include_transcript`
321
+ are not supported, and `update_content` keeps block ids only where the old and new markdown align block by block.
322
+ - Search matches titles only (no content search, fuzziness or relevance ranking); cursors are last-item ids valid only
323
+ against unchanged ordering; there is no `request_status.incomplete`, no `partial` objects, no `public_url`.
324
+ - No permanent deletion (Notion has none), no comment edits/deletes/attachments/`display_name` overrides, no
325
+ data-source schema events, no webhooks, no optimistic concurrency (`conflict_error` only occurs through the fault),
326
+ no `Retry-After` beyond the fault header.
327
+ - Framework-level `invalid` (schema) and `denied` outcomes use the framework's statuses; only their bodies are
328
+ Notion-shaped. `FAILED_PRECONDITION` (row bound) has no Notion equivalent and is sent as 400 `validation_error`.
329
+ - Response size: the framework refuses any response over 1 MiB, so every list (`users.list`, `search`, data source
330
+ query, block children, comments, property items, trash) ends a page at `page_size` items or about 900 KB of encoded
331
+ results, whichever comes first, with `next_cursor` naming the last item returned. Writes are bounded so one object
332
+ always fits one response: a block's content, a page, a comment, a database and a data source may render to at most
333
+ 800 000 UTF-8 bytes, a data source schema to 150 000 (and to at most 150 000 bytes added to the rendering of each of
334
+ its pages, counting `created_by`/`last_edited_by` as the widest expanded user, so a stored page still fits after any
335
+ later schema change), and one create/append request may add 900 000 bytes of block
336
+ content; beyond that the write answers `validation_error` and stores nothing. A read whose single response would
337
+ still pass 1 000 000 bytes (a page's markdown over many large blocks, for example) answers `validation_error`
338
+ `The response is too large: …`; read such a page through `GET /v1/blocks/{id}/children` instead. The request body
339
+ itself is capped at 1 MiB by the framework (HTTP 413).
340
+ - JSON request bodies may nest at most 512 levels: a deeper body is refused by the route codec with `400`
341
+ `framework.HTTP_REQUEST_MAPPING_FAILED` ("JSON nesting is deeper than the maximum depth of 512") before argument
342
+ validation, because the framework validates arguments recursively and a deeper body would overflow the stack.
343
+ Depth is measured iteratively, and ordinary bodies (blocks, rich text, filters) are far below the bound. The bound
344
+ is applied by the provider-shaped `/v1/...` routes; the canonical `POST /v1/operations/notion/<operation>` path is
345
+ decoded by the framework itself, where a body nested a few thousand levels deep still answers `413`
346
+ `{"error":"Maximum call stack size exceeded"}`; the exact depth depends on the serve process's stack limit and
347
+ has been measured anywhere from about 3,000 to more than 6,000 levels (framework limitation, recorded in
348
+ `specs/notion/VERIFICATION.md`).
349
+ - Malformed percent-encoding is decoded leniently by the framework: a bad escape such as `%E0%A4%A` becomes U+FFFD
350
+ (`%ZZ` is not a valid escape and stays literal in query strings and bodies — e.g. `filter_properties=%ZZ` reaches the
351
+ handler and answers Notion's own "Could not find property with name or id: %ZZ" — while any unresolvable escape,
352
+ `%ZZ` included, in a *path* segment is a `404` "route not found"). A query-language input carrying U+FFFD is
353
+ treated as a mangled request rather than a literal to match, and answers `validation_error` "… contains an invalid
354
+ character (U+FFFD); check the request's percent-encoding": `search` `query`, data-source query `filter` property
355
+ names and string condition values, `sorts[].property`, and `filter_properties`. Correctly encoded non-ASCII search
356
+ terms (accents, CJK, emoji) are unaffected.
357
+ - `GET /v1/comments` without `block_id` is refused by the declared input schema (`block_id` is required, as in
358
+ Notion), so the message is Notion's envelope carrying the schema text "arguments do not match notion.comments.list"
359
+ rather than a hand-written field message.
360
+ - Bounds: 10 000 rows per namespace (configurable through `meta/limits`), 100 items per page, 100 children per append
361
+ nested two levels, 2000 characters per rich-text item, 100 KB of markdown, 1000 block elements per parsed markdown
362
+ document (Notion's per-request block limit; `update_content` counts both the current page and the result, so pages
363
+ over 1000 blocks are edited through the blocks API), 64 stored block levels below a page (appends that would go
364
+ deeper fail with `validation_error`). Each bound answers `validation_error`; nothing is truncated. `__proto__` is
365
+ reserved as a property name, but the two forms differ: *renaming* a property to `__proto__`
366
+ (`data-sources.update { properties: { Highlights: { name: "__proto__" } } }`) is refused with `validation_error`
367
+ "… uses the reserved property name __proto__.", while a request that carries `__proto__` as a property *key* never
368
+ reaches the Tool — the framework strips own `__proto__` keys while decoding the body, so the call succeeds (`200`)
369
+ with that one field silently absent from the created or updated schema (see `specs/notion/VERIFICATION.md`).
370
+ - App: editing a text block replaces its rich text with one plain run (annotations, links and mentions of the edited
371
+ block are flattened; untouched blocks keep them); block types cannot be changed in place (the `/` menu inserts a new
372
+ block); the sidebar lists the first 5 000 visible pages; *Recently visited* is ordered by last edit; favorites,
373
+ expanded tree nodes and view filters are per-browser conveniences, not workspace state; Notion AI, teamspaces,
374
+ templates, sharing settings, notifications and permanent deletion have no API surface and are shown as informational
375
+ panels rather than working controls.
376
+ - App controls outside this Tool's scope render in place with the product's glyph, hover state and tooltip and open a
377
+ short *Not simulated by this Tool* panel (or are disabled): sidebar *Meetings*, *Notion AI* and *Marketplace*; the
378
+ top-bar *View all updates* (clock), *Add to Favorites* (star) and, on full-page databases, *Comments* and *Share*;
379
+ the database *Automations* (lightning) button, the *New* caret's *New template*, and the Timeline, Calendar, List,
380
+ Gallery, Chart, Feed and Map layouts in the *Add a new view* menu (Table and Board work); Home *Upcoming events*
381
+ (*Connect calendar*) and the Home *Learn* cards (text-only guide cards; the guides live on the vendor's help
382
+ center); the sidebar *Teamspaces* section (heading, hover *...* and *+*, *Browse teamspaces*) and *Shared* section
383
+ (heading, hover *+*, *Start collaborating*), drawn in place without invented entries because the API does not model
384
+ teamspaces or per-member sharing; the cover *Reposition* button and the cover picker's *Upload* and *Unsplash* tabs
385
+ (visibly disabled); and, in the inline text toolbar shown over a selection, *Ask AI*, *Turn into* (*Text*), *Link*,
386
+ *Bold*, *Italicize*, *Underline*, *Strike-through*, *Mark as code*, *Text color* and *...* (edited blocks are
387
+ written as one plain rich-text run). Working: *Add icon*, *Add cover* and *Change cover* (Gallery gradients, Link
388
+ and Remove through `pages.update` `cover`; external images are drawn as gradients), *Add comment*, and the toolbar's
389
+ *Comment* (opens the block's discussions).
390
+
391
+ ## Trademarks
392
+
393
+ Notion and the Notion logo are trademarks of Notion Labs, Inc. They are used here only to identify the simulated
394
+ service in a test environment; this package is an independent Firedrill Tool and is not affiliated with, sponsored by
395
+ or endorsed by Notion Labs, Inc. The logo files under `firedrill/tools/notion/app/` (`notion.svg`, `notion-wordmark.svg`)
396
+ are unmodified downloads whose sources are recorded in `firedrill/tools/notion/app/assets/ATTRIBUTION.md`, together with
397
+ the Inter font's SIL Open Font License.
398
+
399
+ ## Safety
400
+
401
+ Tools and conformance targets are trusted local executable code, not a sandbox. Review before running. Keep
402
+ credentials and generated worlds/reports out of the package and repository; the file list excludes `.firedrill/`.
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "target": {
4
+ "id": "conformance-agent",
5
+ "kind": "command",
6
+ "bindings": [
7
+ "http",
8
+ "mcp"
9
+ ],
10
+ "executable": "node",
11
+ "arguments": [
12
+ "test/conformance.mjs"
13
+ ],
14
+ "workingDirectory": ".",
15
+ "timeoutMs": 90000
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "baseline",
4
+ "title": "Baseline: the fictional Halvard Robotics workspace exactly as authored in world.json / starter.json (no faults)"
5
+ }