Templates for notes (not daily notes) #54

Open
opened 2026-09-24 15:36:04 +00:00 by kayg · 15 comments
Owner

Owner decision (PKM round, K5): templates exist for any note except daily notes, which are app-managed (gatekept) and not edited in the block editor.

  • Templates live in Notes/Templates/*.md (plain Markdown, file over app) with variables {{date}}, {{time}}, {{title}}, {{week}}, {{cursor}}; unknown variables are left verbatim.
  • "New note from template" in ⌘K and in the note creation menu; per-folder default templates optional.
  • Template frontmatter merges into the new note through the single YAML-safe frontmatter writer (calternal id minted fresh).

Context for the owning job

  • Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§4, §9, §12, §17, §24, §29–§31) first.
  • Owner rules: file over app (plain files are the truth; the DB is a rebuildable index); the server is the single writer; data loss is unacceptable; performance first but never at the cost of finesse; UI in the calternal.js design system (Claude reviews screenshots); never ship sample/mock data; atomic commits; adversarial testing after API work; good enough, not perfect.
  • Comment on this issue when you start (branch, base SHA), on each finding, when blocked, and when finished (head SHA + gate output). Never close it.
Owner decision (PKM round, K5): templates exist for any note **except** daily notes, which are app-managed (gatekept) and not edited in the block editor. - Templates live in `Notes/Templates/*.md` (plain Markdown, file over app) with variables `{{date}}`, `{{time}}`, `{{title}}`, `{{week}}`, `{{cursor}}`; unknown variables are left verbatim. - "New note from template" in ⌘K and in the note creation menu; per-folder default templates optional. - Template frontmatter merges into the new note through the single YAML-safe frontmatter writer (calternal id minted fresh). ## Context for the owning job - Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§4, §9, §12, §17, §24, §29–§31) first. - Owner rules: file over app (plain files are the truth; the DB is a rebuildable index); the server is the single writer; data loss is unacceptable; performance first but never at the cost of finesse; UI in the calternal.js design system (Claude reviews screenshots); never ship sample/mock data; atomic commits; adversarial testing after API work; good enough, not perfect. - Comment on this issue when you start (branch, base SHA), on each finding, when blocked, and when finished (head SHA + gate output). Never close it.
Author
Owner

Notes UI job (#38, branch worktree-agent-a956f77f3d4d0b6ac): templates UI not built. There is no templates backend on main (no Notes/Templates handling, no "new from template" endpoint). The note creation entry points (explorer "+", All notes "New note") are ready to gain a template menu when the backend lands.

Notes UI job (#38, branch `worktree-agent-a956f77f3d4d0b6ac`): templates UI not built. There is no templates backend on main (no `Notes/Templates` handling, no "new from template" endpoint). The note creation entry points (explorer "+", All notes "New note") are ready to gain a template menu when the backend lands.
Author
Owner

Starting implementation on branch job/templates, based on 6a1d2aa7f1771e84cb2fd38c0c019e3df0f0527a (main). I confirmed the UI entry points are waiting on the templates backend; this job will add the Notes API and contract support.

Starting implementation on branch `job/templates`, based on `6a1d2aa7f1771e84cb2fd38c0c019e3df0f0527a` (main). I confirmed the UI entry points are waiting on the templates backend; this job will add the Notes API and contract support.
Author
Owner

Implementation decisions where the design is silent:

  • GET /api/v1/notes/templates?folder=Notes/... returns top-level Markdown templates from Notes/Templates and that folder's default_template.
  • POST /api/v1/notes/from-template creates a Note from template_id, title and optional folder; omitted template_id uses the folder default. PUT /api/v1/notes/templates/default sets or clears a folder default.
  • Store folder defaults as user metadata in .calternal/note-template-defaults.json, with file access through calternal-fs.
  • {{date}} and {{time}} use the same local zone and clock as Note creation (x-calternal-timezone, default UTC); {{week}} uses ISO week format YYYY-Www. Remove every {{cursor}} and return the first position as a UTF-8 byte offset in the Note body.
  • Template frontmatter user keys are YAML-parsed and safely serialized before the central Note frontmatter writer adds fresh identity and app-managed fields. Reject templates over 1 MiB. Template creation rejects folders under Notes/Journal so a Daily note cannot be created this way.
Implementation decisions where the design is silent: - `GET /api/v1/notes/templates?folder=Notes/...` returns top-level Markdown templates from `Notes/Templates` and that folder's `default_template`. - `POST /api/v1/notes/from-template` creates a Note from `template_id`, `title` and optional `folder`; omitted `template_id` uses the folder default. `PUT /api/v1/notes/templates/default` sets or clears a folder default. - Store folder defaults as user metadata in `.calternal/note-template-defaults.json`, with file access through `calternal-fs`. - `{{date}}` and `{{time}}` use the same local zone and clock as Note creation (`x-calternal-timezone`, default UTC); `{{week}}` uses ISO week format `YYYY-Www`. Remove every `{{cursor}}` and return the first position as a UTF-8 byte offset in the Note body. - Template frontmatter user keys are YAML-parsed and safely serialized before the central Note frontmatter writer adds fresh identity and app-managed fields. Reject templates over 1 MiB. Template creation rejects folders under `Notes/Journal` so a Daily note cannot be created this way.
Author
Owner

Integration findings with reproductions:

  • Creating from a template with summary: '{{title}}' returned Note.properties.summary with the surrounding single quotes ('Review: Europe'). The shared scalar projection only decoded double-quoted YAML. It now decodes valid YAML single-quoted scalars, including doubled apostrophes, with a regression test.
  • A template with empty frontmatter (--- followed immediately by ---) produced a blank line inside the generated block; the shared frontmatter boundary parser then left the fences in Note.body. The template builder now emits the canonical empty block and the default-template creation test checks the body and cursor offset.
Integration findings with reproductions: - Creating from a template with `summary: '{{title}}'` returned `Note.properties.summary` with the surrounding single quotes (`'Review: Europe'`). The shared scalar projection only decoded double-quoted YAML. It now decodes valid YAML single-quoted scalars, including doubled apostrophes, with a regression test. - A template with empty frontmatter (`---` followed immediately by `---`) produced a blank line inside the generated block; the shared frontmatter boundary parser then left the fences in `Note.body`. The template builder now emits the canonical empty block and the default-template creation test checks the body and cursor offset.
Author
Owner

Cross-plugin regression found in Notes tests: a real Notes/Templates/Project.md file entered note_items both when Files watcher changes called adopt_change (count 1, expected 0) and after store::reconcile_user rebuilt the Index (count 1, expected 0). I will exclude the template repository from Note adoption and reconciliation so templates remain available to the template API without appearing as regular Notes.

Cross-plugin regression found in Notes tests: a real `Notes/Templates/Project.md` file entered `note_items` both when Files watcher changes called `adopt_change` (count 1, expected 0) and after `store::reconcile_user` rebuilt the Index (count 1, expected 0). I will exclude the template repository from Note adoption and reconciliation so templates remain available to the template API without appearing as regular Notes.
Author
Owner

A deleted template left its ID in default_template even though the list had no matching item. The Notes API regression reproduces this by setting Project as the folder default, moving the template to Trash, then listing templates (stale ID returned). The list now needs to return a default only when its Markdown template still exists.

A deleted template left its ID in `default_template` even though the list had no matching item. The Notes API regression reproduces this by setting `Project` as the folder default, moving the template to Trash, then listing templates (stale ID returned). The list now needs to return a default only when its Markdown template still exists.
Author
Owner

The full Notes package suite also exposed a stale assertion in daily_and_composer_preserve_unrelated_bytes: it expected a legacy Log line to have no block ID, but the unchanged NotesJournalProvider::ensure_ids assigns stable IDs when the Journal view opens. I updated that assertion to match the existing deep-link behavior; the file-byte preservation assertions remain unchanged.

The full Notes package suite also exposed a stale assertion in `daily_and_composer_preserve_unrelated_bytes`: it expected a legacy Log line to have no block ID, but the unchanged `NotesJournalProvider::ensure_ids` assigns stable IDs when the Journal view opens. I updated that assertion to match the existing deep-link behavior; the file-byte preservation assertions remain unchanged.
Author
Owner

Template syntax decision for #54: template variables and the cursor marker work in Markdown text. YAML frontmatter is parsed before variable replacement, so authors must quote a token used as a YAML value (for example summary: '{{title}}'). Unknown tokens stay verbatim. The API removes every cursor token and returns the first token's UTF-8 byte offset in the Note body. I documented this in contracts/README.md.

Template syntax decision for #54: template variables and the cursor marker work in Markdown text. YAML frontmatter is parsed before variable replacement, so authors must quote a token used as a YAML value (for example `summary: '{{title}}'`). Unknown tokens stay verbatim. The API removes every cursor token and returns the first token's UTF-8 byte offset in the Note body. I documented this in `contracts/README.md`.
Author
Owner

Generated contract finding: the first generator run found contracts/openapi.json and packages/api-client/src/generated.ts were stale against the server routes. I regenerated and committed both snapshots, including the three Notes template operations; bash packages/api-client/check-generated.sh then exited 0 and reported no duplicate operation IDs. The server embeds apps/web/build, so I built that bundle without editing its source. The verification build uses OPENSSL_NO_VENDOR=1 because the default vendored OpenSSL build had compiled only 350/1,278 C sources after about 50 minutes; the environment has OpenSSL 3.5.7. The repository dependency configuration is unchanged.

Generated contract finding: the first generator run found `contracts/openapi.json` and `packages/api-client/src/generated.ts` were stale against the server routes. I regenerated and committed both snapshots, including the three Notes template operations; `bash packages/api-client/check-generated.sh` then exited 0 and reported no duplicate operation IDs. The server embeds `apps/web/build`, so I built that bundle without editing its source. The verification build uses `OPENSSL_NO_VENDOR=1` because the default vendored OpenSSL build had compiled only 350/1,278 C sources after about 50 minutes; the environment has OpenSSL 3.5.7. The repository dependency configuration is unchanged.
Author
Owner

Workspace test finding: cargo test --workspace failed in calternal-server::wire::tests::full_app_setup_session_config_and_backup with InvalidEnvironmentValue. The two build_live_app tests use separate temporary Data directories while Calendar holds one process-global OnceLock key. With no CALTERNAL_SECRET_KEY, each directory generates a different random key, so the second setup is rejected. I am rerunning tests with one temporary key set only in the test process; I did not change the server crate because it is outside this job's owned files.

Workspace test finding: `cargo test --workspace` failed in `calternal-server::wire::tests::full_app_setup_session_config_and_backup` with `InvalidEnvironmentValue`. The two `build_live_app` tests use separate temporary Data directories while Calendar holds one process-global `OnceLock` key. With no `CALTERNAL_SECRET_KEY`, each directory generates a different random key, so the second setup is rejected. I am rerunning tests with one temporary key set only in the test process; I did not change the server crate because it is outside this job's owned files.
Author
Owner

Workspace gate finding (2026-09-25): rerunning cargo test --workspace with one ephemeral fixed CALTERNAL_SECRET_KEY cleared the Calendar OnceLock key conflict, but calternal-server::wire::tests::full_app_setup_session_config_and_backup then failed at crates/calternal-server/src/wire.rs:1751 with notifications runtime already initialized. This is another pre-existing process-global test race outside the owned files. I am rerunning workspace tests with a single test thread; no production behavior changes are planned for this unrelated server test harness.

Workspace gate finding (2026-09-25): rerunning `cargo test --workspace` with one ephemeral fixed `CALTERNAL_SECRET_KEY` cleared the Calendar OnceLock key conflict, but `calternal-server::wire::tests::full_app_setup_session_config_and_backup` then failed at `crates/calternal-server/src/wire.rs:1751` with `notifications runtime already initialized`. This is another pre-existing process-global test race outside the owned files. I am rerunning workspace tests with a single test thread; no production behavior changes are planned for this unrelated server test harness.
Author
Owner

Adversarial run finding: the Notes/Files integration probe reported trash template through Files: unexpected 422 and then a stale default. Inspection showed the Files contract is Vec<PathInput> (array of {path}), while the probe sent an object with a paths property. The list handler already hides a default when its template file is absent, so that follow-on was a false positive. I corrected the probe body to match the Files contract. The same run marked task-create requests slow (>5s) while several other builds and adversarial probes were running on the shared host; I will rerun once those complete to distinguish host load from an endpoint issue.

Adversarial run finding: the Notes/Files integration probe reported `trash template through Files: unexpected 422` and then a stale default. Inspection showed the Files contract is `Vec<PathInput>` (array of `{path}`), while the probe sent an object with a `paths` property. The list handler already hides a default when its template file is absent, so that follow-on was a false positive. I corrected the probe body to match the Files contract. The same run marked task-create requests slow (>5s) while several other builds and adversarial probes were running on the shared host; I will rerun once those complete to distinguish host load from an endpoint issue.
Author
Owner

Adversarial round 2 also reports unrelated findings in its deletion section. DELETE /api/v1/admin/users/{id} calls admin(&state, &headers, true) in crates/calternal-server/src/wire.rs; admin rejects missing fresh authority with 403. attack2.py sends the owner bearer token without completing /api/v1/auth/assert/start and /finish, then expects deletion/archive success. The subsequent archive, transfer, share, public-link, and session findings cascade from those 403 responses. attack2.py is outside the files owned by this job, so I will leave that probe unchanged and rerun round 2 excluding its unrelated deletion section while retaining the full first-round probe.

Adversarial round 2 also reports unrelated findings in its `deletion` section. `DELETE /api/v1/admin/users/{id}` calls `admin(&state, &headers, true)` in `crates/calternal-server/src/wire.rs`; `admin` rejects missing fresh authority with 403. `attack2.py` sends the owner bearer token without completing `/api/v1/auth/assert/start` and `/finish`, then expects deletion/archive success. The subsequent archive, transfer, share, public-link, and session findings cascade from those 403 responses. `attack2.py` is outside the files owned by this job, so I will leave that probe unchanged and rerun round 2 excluding its unrelated `deletion` section while retaining the full first-round probe.
Author
Owner

Finished issue #54.

Head: 0df82db655d3df2def8609258997d18ef5e328f1

Built the Notes templates API, rendering and YAML frontmatter merge helpers, folder defaults, generated API types and template endpoint adversarial coverage. The final adversarial correction sends Files Trash the documented Vec<PathInput> payload. All template-specific probes passed after that correction.

Gates (output copied verbatim):

  • cargo fmt --check — exit 0; stdout and stderr were empty.
  • cargo clippy --workspace --all-targets -- -D warnings with OPENSSL_NO_VENDOR=1:
    Finished \dev` profile [unoptimized + debuginfo] target(s) in 31m 38s`
  • bash packages/api-client/check-generated.sh:
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.54s
         Running `target/debug/calternal-server openapi`
    $ bunx --package openapi-typescript@7.13.0 openapi-typescript ../../contracts/openapi.json -o src/generated.ts
    ✨ openapi-typescript 7.13.0
    🚀 ../../contracts/openapi.json → src/generated.ts [275.3ms]
    
  • cargo test --workspace -- --test-threads=1 with an ephemeral fixed test key failed in the pre-existing server test:
    ---- wire::tests::full_app_setup_session_config_and_backup stdout ----
    thread 'wire::tests::full_app_setup_session_config_and_backup' (2885940) panicked at crates/calternal-server/src/wire.rs:1751:51:
    called `Result::unwrap()` on an `Err` value: Custom { kind: Other, error: "notifications runtime already initialized" }
    test result: FAILED. 11 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.30s
    error: test failed, to rerun pass `-p calternal-server --bin calternal-server`
    
    The Notes plugin suite passed 31 tests; its later Unicode-folder regression test passed in a focused run. An isolated run of the unrelated server test also reached a separate existing 401-versus-204 assertion at wire.rs:2305.
  • The full adversarial run found no template endpoint issues after the probe correction. It still reports unrelated Task storm requests over its 5-second limit and the round-2 deletion section sends no fresh admin assertion, so admin deletion correctly returns 403 and later findings cascade. With ROUND2_SECTIONS=feed,isolation,collab,public,exhaustion,cli,sync,notifications, round 2 reported ==== ROUND 2 FINDINGS 0. The full run did not meet the requested zero-findings gate.

The dependency build used the installed OpenSSL rather than the default vendored build: OpenSSL 3.5.7 9 Jun 2026 (Library: OpenSSL 3.5.7 9 Jun 2026). I did not run cargo clean: active jobs in other worktrees were using the shared Cargo target and server binaries, and cleaning it would disrupt them.

Decisions not covered by DESIGN.md: store folder defaults in the user's .calternal/notes/template-defaults.json with schema version 1; only expose a default while its template file exists; require YAML quoting around placeholders used as scalar values. Daily Notes and the reserved Templates folder remain invalid creation targets.

Finished issue #54. Head: `0df82db655d3df2def8609258997d18ef5e328f1` Built the Notes templates API, rendering and YAML frontmatter merge helpers, folder defaults, generated API types and template endpoint adversarial coverage. The final adversarial correction sends Files Trash the documented `Vec<PathInput>` payload. All template-specific probes passed after that correction. Gates (output copied verbatim): - `cargo fmt --check` — exit 0; stdout and stderr were empty. - `cargo clippy --workspace --all-targets -- -D warnings` with `OPENSSL_NO_VENDOR=1`: `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 31m 38s` - `bash packages/api-client/check-generated.sh`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.54s Running `target/debug/calternal-server openapi` $ bunx --package openapi-typescript@7.13.0 openapi-typescript ../../contracts/openapi.json -o src/generated.ts ✨ openapi-typescript 7.13.0 🚀 ../../contracts/openapi.json → src/generated.ts [275.3ms] ``` - `cargo test --workspace -- --test-threads=1` with an ephemeral fixed test key failed in the pre-existing server test: ``` ---- wire::tests::full_app_setup_session_config_and_backup stdout ---- thread 'wire::tests::full_app_setup_session_config_and_backup' (2885940) panicked at crates/calternal-server/src/wire.rs:1751:51: called `Result::unwrap()` on an `Err` value: Custom { kind: Other, error: "notifications runtime already initialized" } test result: FAILED. 11 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.30s error: test failed, to rerun pass `-p calternal-server --bin calternal-server` ``` The Notes plugin suite passed 31 tests; its later Unicode-folder regression test passed in a focused run. An isolated run of the unrelated server test also reached a separate existing 401-versus-204 assertion at `wire.rs:2305`. - The full adversarial run found no template endpoint issues after the probe correction. It still reports unrelated `Task storm` requests over its 5-second limit and the round-2 `deletion` section sends no fresh admin assertion, so admin deletion correctly returns 403 and later findings cascade. With `ROUND2_SECTIONS=feed,isolation,collab,public,exhaustion,cli,sync,notifications`, round 2 reported `==== ROUND 2 FINDINGS 0`. The full run did not meet the requested zero-findings gate. The dependency build used the installed OpenSSL rather than the default vendored build: `OpenSSL 3.5.7 9 Jun 2026 (Library: OpenSSL 3.5.7 9 Jun 2026)`. I did not run `cargo clean`: active jobs in other worktrees were using the shared Cargo target and server binaries, and cleaning it would disrupt them. Decisions not covered by DESIGN.md: store folder defaults in the user's `.calternal/notes/template-defaults.json` with schema version 1; only expose a default while its template file exists; require YAML quoting around placeholders used as scalar values. Daily Notes and the reserved Templates folder remain invalid creation targets.
Author
Owner

Hygiene review: the template API is merged, but the issue also requires “New note from template” in ⌘K and the note creation menu, plus optional per-folder defaults. The current Notes UI has no template entry point, so #54 stays open.

Hygiene review: the template API is merged, but the issue also requires “New note from template” in ⌘K and the note creation menu, plus optional per-folder defaults. The current Notes UI has no template entry point, so #54 stays open.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#54
No description provided.