From 37a7fd144939563ea3bb6e51f62d4e037acd08b9 Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Sun, 2 Aug 2026 11:01:48 -0700 Subject: [PATCH] docs(openspec): propose property-authoring --- openspec/changes/property-authoring/design.md | 132 ++++++++++++++++++ .../changes/property-authoring/proposal.md | 67 +++++++++ .../specs/block-graph/spec.md | 23 +++ .../specs/property-authoring/spec.md | 107 ++++++++++++++ openspec/changes/property-authoring/tasks.md | 50 +++++++ 5 files changed, 379 insertions(+) create mode 100644 openspec/changes/property-authoring/design.md create mode 100644 openspec/changes/property-authoring/proposal.md create mode 100644 openspec/changes/property-authoring/specs/block-graph/spec.md create mode 100644 openspec/changes/property-authoring/specs/property-authoring/spec.md create mode 100644 openspec/changes/property-authoring/tasks.md diff --git a/openspec/changes/property-authoring/design.md b/openspec/changes/property-authoring/design.md new file mode 100644 index 0000000..af96ead --- /dev/null +++ b/openspec/changes/property-authoring/design.md @@ -0,0 +1,132 @@ +# Design: property-authoring + +## Context + +Properties exist end-to-end below the UI: typed values (text/number/date/ +bool/ref) encoded into each block's Loro meta `properties` map, a derived +key→block→value index, and `prop`/`prop-eq`/`table` query primitives. The +only UI writer is the ctrl-shift-q query toggle. Query blocks already render +an attached sub-surface (the RESULT table) below block content — the display +slot this change generalizes. The sidebar was considered and rejected for +this: properties must stay colocated with their node so several blocks' +properties are visible at once. Decisions settled in the 2026-08-02 explore +session. + +## Goals / Non-Goals + +**Goals:** + +- Make the existing property system authorable: add, edit, delete on any + block, keyboard-first. +- Establish the display-and-editing surface that supertag schema-backed + fields will later render into (same rows, declared instead of inferred + types). + +**Non-Goals:** + +- Schema definition or validation (add-supertags; schemas stay Scheme per + its D3 — this change is reading (a) from the explore session). +- Inline `key:: value` syntax in block text (explicitly rejected: messy for + complicated blocks). +- Migrating the `query` flag to a meta key (noted for later; it stays a + property, hidden from the grid). +- Bulk/multi-block property editing, property renaming across the graph. + +## Decisions + +### D1: Colocated attached grid, not a sidebar panel, not inline syntax + +Property rows render below the block's content and above its descendants +(and above a query block's RESULT), as owned display in the same structural +slot the RESULT table already occupies — not child blocks, not text. This +keeps properties glanceable across many blocks simultaneously, which a +focus-reactive sidebar panel cannot do, without polluting block content. + +### D2: The grid is a separate focus surface; outline navigation stays block-only + +Up/down walk blocks, never property rows — fast vertical travel is the +outliner invariant. The grid is its own small focus world (calendar +day-selection precedent): `ctrl-shift-p` (or the context-menu item) enters +the grid of the focused block; escape returns to the block. Entering a block +with no properties opens the grid with one fresh empty row ready to fill. + +### D3: Two fold axes; the grid disclosure is document content + +Folding the block hides properties and descendants together (fold is the +density gesture; properties are detail). A separate per-block disclosure +collapses the grid alone — children stay visible — rendered as a +`▸ properties (n)` header row, toggled by `ctrl-alt-p` (and click). The +disclosure persists as a meta key beside `folded`, with identical semantics: +document content (survives restart, merges, follows future sync), not a +`properties` entry, invisible to property queries and indexes, absent reads +as expanded, no format bump. A collapsed grid still answers "does this block +carry data?" at a glance via the header count. + +### D4: Web-form navigation inside the grid + +Tab and shift-tab move key → value → next row's key. Tab from the last +value field appends a fresh empty row (the append gesture — no separate add +binding inside the surface); an untouched empty row is discarded on exit or +blur. Shift-tab from the first field stops (no wrap). Enter commits the +current field; escape exits the surface. + +### D5: Values are typed by inference at commit + +Committed value text is best-effort typed, in order: ISO `YYYY-MM-DD` → +Date; parseable number → Number; `true`/`false` → Bool; a completed `[[…]]` +reference → Ref (reusing the editor's `[[`/`#` completion machinery, dates +included); anything else → Text. Each row shows a type glyph for what +inference produced. This matches add-supertags D4's "best-effort typed" +ad-hoc semantics; when schemas land, schema-backed keys flip to declared +types in the same rows. Persisted rows always carry a value — there is no +key-without-value storage state; a row with an empty value exists only as +transient surface state (and, later, as schema-slot display), so the grid's +row model is "key slot, value maybe empty" from day one. + +### D6: Deletion is explicit, never commit-of-empty + +A per-row delete gesture (shift-delete inside the surface, plus a +context-menu item on the row) removes the property. Committing an emptied +value field does not delete — empty text is a legal value (`t:`), and +silent delete-on-empty eats data during editing. This gives +`Outline::remove_property` its first real consumer beyond the query toggle. + +### D7: Key fields autocomplete against the graph's property vocabulary + +The index's `properties` map keys are offered as completions while typing a +key. Cheap, and it fights key drift (`due` vs `due-date` vs `deadline`) — +which matters doubly later, since add-supertags D4 matches schema fields to +ad-hoc keys by name. + +### D8: Machine properties are filtered from the grid + +A single machine-keys constant (today exactly `["query"]`) is excluded from +grid display and editing. The flag remains a real property — `(prop +"query")` still works, ctrl-shift-q still toggles it — the grid is just a +user surface. The fold flag set the precedent that machine state should be +meta, not properties; `query` predates that and migrating it is deliberately +deferred. + +## Risks / Trade-offs + +- [A second focus surface in gpui — focus transfer, blur commits, overlay + interaction with the virtualized row list] → the calendar's day-selection + and the completion popup establish the patterns; the grid is per-focused- + block only, never multiple grids in edit mode at once. +- [Inference surprises: "2026-13-45" is Text, "007" becomes Number 7] → the + type glyph makes the result visible at commit time; explicit type override + is an open question below, deferred rather than designed around. +- [Grid rows add vertical space for property-heavy blocks] → the per-block + disclosure (D3) is the pressure valve; density-minded users collapse. +- [`ctrl-alt-*` can collide with AltGr layouts on Windows] → bindings are + provisional; resolve against the keymap at implementation. + +## Open Questions + +1. Explicit type override (e.g. cycling the glyph to force Text for a + date-shaped string). Deferred from v1 — inference plus re-edit covers the + common cases; revisit when schema-locked types arrive and the glyph + becomes interactive anyway. +2. Exact visual weight of the grid rows (indent, mono vs proportional, + glyph placement) — resolve in dev-loop screenshot passes, per the + UI-verify workflow. diff --git a/openspec/changes/property-authoring/proposal.md b/openspec/changes/property-authoring/proposal.md new file mode 100644 index 0000000..800040b --- /dev/null +++ b/openspec/changes/property-authoring/proposal.md @@ -0,0 +1,67 @@ +# Proposal: property-authoring + +## Why + +Typed properties are fully modeled, stored, indexed, and queryable — and +unauthorable. Nothing in the app can set, edit, or remove a property except +the query-flag toggle; every property in a real graph today was written by +fixture seeding or tests. This blocks any actual use of the property system, +and it blocks supertags: schema-backed fields need a value-authoring surface, +and per the 2026-08-02 explore session that surface should be the *same +affordance* for ad-hoc and schema-backed keys (reading (a): the panel authors +values; schema *definition* stays Scheme per add-supertags D3). + +## What Changes + +- **Property grid**: each block's properties render as structured key/value + rows directly below the block's content and above its descendants (and + above a query block's RESULT) — colocated so multiple blocks' properties + are visible at a glance, not a sidebar panel, not inline `key:: value` + syntax in block text. +- **Two fold axes**: folding the block hides the grid along with descendants; + a separate per-block grid disclosure collapses the grid alone (children + stay visible), persisted in the doc like the block fold flag. +- **A separate editing surface**: outline navigation stays block-only — + up/down never enter the grid. A dedicated keybinding (and a context-menu + item on the bullet) enters the grid for the focused block, opening a fresh + empty row when the block has none. Inside: web-form navigation (tab / + shift-tab through key and value fields, tab from the last value appends a + new row, no wrap), escape returns to the block. +- **Typing by inference**: committed values are best-effort typed (ISO date → + Date, numeric → Number, true/false → Bool, `[[ref]]` → Ref via the existing + completion machinery, else Text), shown with a per-row type glyph. + Deletion is an explicit per-row gesture, never commit-of-empty. Key fields + autocomplete against the graph's existing property vocabulary. +- **Machine keys hidden**: the `query` flag is filtered from display and + editing (a single machine-keys constant), keeping the grid a user surface. + Migrating `query` to a meta key is noted for later, out of scope here. + +## Capabilities + +### New Capabilities + +- `property-authoring`: the property grid, its two fold axes, the editing + surface and its navigation/commit/delete semantics, and machine-key + filtering. + +### Modified Capabilities + +- `block-graph`: gains a persisted per-block property-grid disclosure flag + beside the fold flag (document content, not a property, no format bump). + +## Impact + +- `crates/trawler-core`: `set_properties_folded`/`properties_folded` accessor + pair on `Outline` (mirrors `set_folded`/`folded`). +- `crates/trawler/src/main.rs`: grid rendering in the outline rows, the + editing surface and its focus management, keybindings (`ctrl-shift-p` + enter/edit, `ctrl-alt-p` toggle disclosure — provisional against the + keymap), context-menu items (the menu was built for per-node growth; + pinned-nodes requirements are about pin actions and need no delta), + key-vocabulary completion, devtools dump exposure. +- No graph format version bump: the disclosure flag is an optional meta key, + absent reads as expanded. +- Sequencing: independent of and prerequisite to supertags value-affordances; + add-supertags D4's `key:: value` mention describes remoras, and the + authoring surface decided here (grid, not inline syntax) supersedes it — + annotate add-supertags design.md when this lands. diff --git a/openspec/changes/property-authoring/specs/block-graph/spec.md b/openspec/changes/property-authoring/specs/block-graph/spec.md new file mode 100644 index 0000000..04ff9f9 --- /dev/null +++ b/openspec/changes/property-authoring/specs/block-graph/spec.md @@ -0,0 +1,23 @@ +# block-graph Specification (delta) + +## ADDED Requirements + +### Requirement: Property-grid disclosure is document content +Each block SHALL support a boolean property-grid disclosure flag stored in +the block's Loro meta map beside the fold flag, persisted and merged with +the document like any other content. The flag SHALL NOT appear in the +block's `properties` map, property queries, or derived property indexes. An +absent flag SHALL be read as expanded; the flag's presence MUST NOT require +a graph format-version bump or migration. + +#### Scenario: Disclosure round-trips through storage +- **WHEN** a block's grid disclosure is set to collapsed and the graph is + closed and reopened +- **THEN** reading the flag returns collapsed, and blocks never flagged + read as expanded + +#### Scenario: Disclosure is invisible to the property system +- **WHEN** a block's grid is collapsed and the graph's properties are + queried +- **THEN** no property derived from the disclosure flag appears on the + block, and property indexes are unchanged diff --git a/openspec/changes/property-authoring/specs/property-authoring/spec.md b/openspec/changes/property-authoring/specs/property-authoring/spec.md new file mode 100644 index 0000000..e847c11 --- /dev/null +++ b/openspec/changes/property-authoring/specs/property-authoring/spec.md @@ -0,0 +1,107 @@ +# property-authoring Specification (delta) + +## ADDED Requirements + +### Requirement: Properties render colocated with their block +Each block's properties SHALL render as structured key/value rows directly +below the block's content and above its descendants — and above a query +block's rendered result — so that the properties of multiple blocks are +visible simultaneously. Property rows are owned display, not child blocks +and not block text. Each row SHALL show the key, the value, and a glyph for +the value's type. Folding a block SHALL hide its property rows along with +its descendants. + +#### Scenario: Properties visible for multiple blocks at a glance +- **WHEN** two sibling blocks each carry a `due` property +- **THEN** both blocks' grids render their `due` rows simultaneously, + each below its block's content and above that block's children + +#### Scenario: Block fold hides the grid +- **WHEN** a block with properties and children is folded +- **THEN** neither its property rows nor its children render, and unfolding + restores both + +### Requirement: Independent grid disclosure +Each block's property grid SHALL be collapsible independently of the block's +fold state via a disclosure header showing the property count; collapsing +the grid MUST NOT hide the block's descendants. The disclosure state SHALL +persist with the document per the block-graph requirement, and an absent +state SHALL read as expanded. + +#### Scenario: Collapsed grid keeps children visible +- **WHEN** the user toggles the grid disclosure on a block with two + properties and a child +- **THEN** the rows collapse to a header line indicating two properties, + the child remains visible, and toggling again restores the rows + +### Requirement: The grid is a separate editing surface +Outline navigation SHALL remain block-only: moving focus up or down SHALL +never land on a property row. A dedicated keybinding and a context-menu item +on the block's bullet SHALL enter the focused block's grid for editing; +entering the grid of a block with no properties SHALL present one empty row +ready to fill. Inside the grid, tab and shift-tab SHALL move through key and +value fields in order; tab from the last value field SHALL append a fresh +empty row; shift-tab from the first field SHALL do nothing; escape SHALL +return focus to the block. An empty row never committed SHALL be discarded +on exit without creating a property. + +#### Scenario: Outline navigation skips property rows +- **WHEN** a block with three properties is focused and the user presses + down +- **THEN** focus moves to the next block in the outline, not into the grid + +#### Scenario: Adding the first property +- **WHEN** the user invokes the properties keybinding on a block with no + properties, types a key, tabs, types a value, and presses escape +- **THEN** the block has exactly that property, and invoking the binding + and escaping immediately on another empty block creates nothing + +#### Scenario: Tab appends a row +- **WHEN** the user is in the last value field of a block's grid and + presses tab +- **THEN** a fresh empty row is appended with its key field focused + +### Requirement: Values are typed by inference at commit +A committed value SHALL be typed best-effort: an ISO `YYYY-MM-DD` string as +a date, a parseable number as a number, `true`/`false` as a boolean, a +completed reference as a node reference (offering the editor's existing +reference completion inside the value field), and anything else as text. +The row's type glyph SHALL reflect the inferred type. Key fields SHALL +offer completion against the property keys already present in the graph. +Properties SHALL only be persisted with a value; committing a key with an +empty value SHALL NOT store a key-without-value state. + +#### Scenario: Date round-trips as a date +- **WHEN** the user authors `due` = `2026-08-05` in the grid +- **THEN** the block's `due` property is a typed date (a property query + retrieves it as a date, not a string), and the row shows the date glyph + +#### Scenario: Reference value via completion +- **WHEN** the user types `[[` in a value field and accepts the completion + for an existing page +- **THEN** the committed property is a node reference to that page + +### Requirement: Deletion is an explicit gesture +Removing a property SHALL require an explicit per-row delete action (a +keybinding within the grid and a context-menu item). Committing an emptied +value field SHALL NOT delete the property; empty text is a legal value. + +#### Scenario: Explicit delete removes the property +- **WHEN** the user deletes the `priority` row via the delete gesture +- **THEN** the block no longer has a `priority` property and the row is + gone from the grid + +#### Scenario: Emptying a value does not delete +- **WHEN** the user clears a text value to empty and commits it +- **THEN** the property still exists with an empty text value and its row + still renders + +### Requirement: Machine properties are hidden from the grid +Properties on a single machine-keys list (initially exactly the query flag) +SHALL be excluded from grid display and editing, while remaining ordinary +properties for storage, indexing, and queries. + +#### Scenario: Query flag is invisible in the grid +- **WHEN** a query block whose only property is the query flag is focused +- **THEN** its grid renders as empty (entering it presents a fresh row), + while `(prop "query")` still returns the block diff --git a/openspec/changes/property-authoring/tasks.md b/openspec/changes/property-authoring/tasks.md new file mode 100644 index 0000000..e9650c0 --- /dev/null +++ b/openspec/changes/property-authoring/tasks.md @@ -0,0 +1,50 @@ +# Tasks: property-authoring + +## 1. Core + +- [ ] 1.1 Add `set_properties_folded`/`properties_folded` to `Outline` + (meta key beside `folded`, same semantics: absent = expanded, not a + `properties` entry). Unit tests: round-trip through storage, absent + reads expanded, invisible to `properties()` and the index. + +## 2. Grid display + +- [ ] 2.1 Render property rows below block content, above descendants and + above a query block's RESULT: key, value, type glyph per row; + machine-keys constant (`["query"]`) filtered out. +- [ ] 2.2 Disclosure header (`▸ properties (n)`) driven by the core flag; + click and `ctrl-alt-p` toggle it; block fold hides the grid entirely. +- [ ] 2.3 UI tests: rows render for multiple blocks at once; block fold + hides grid; disclosure collapses rows but not children and survives + relaunch; query flag never renders. + +## 3. Editing surface + +- [ ] 3.1 Grid focus surface: `ctrl-shift-p` (and context-menu item) enters + the focused block's grid, presenting a fresh empty row when the block + has none; escape returns to the block; outline up/down never enter + the grid. +- [ ] 3.2 Form navigation: tab/shift-tab across key/value fields; tab from + last value appends an empty row; shift-tab at the first field stops; + untouched empty rows discarded on exit. +- [ ] 3.3 Commit semantics: inference (date/number/bool/ref/text) on value + commit via `set_property`; type glyph reflects the result; `[[` + completion inside value fields; key completion from the index's + property vocabulary; no persistence of empty rows. +- [ ] 3.4 Explicit delete: shift-delete on a row and a context-menu item, + calling `remove_property`; commit-of-empty keeps the property as + empty text. +- [ ] 3.5 UI tests: nav skips grid; first-property flow; tab-append; + empty-row discard; date/number/bool/ref inference round-trips typed + through a query; delete removes; emptied value persists as text. + +## 4. Verification + +- [ ] 4.1 Devtools dump: expose per-row grid state (keys, encoded values, + disclosure) and grid focus, for UI-test and dev-loop assertions. +- [ ] 4.2 Full check: `cargo fmt`, clippy `-D warnings`, + `cargo test --workspace`; dev-loop screenshot pass (grid on fixture + blocks, collapsed disclosure, editing surface with completion open) + and user confirmation before commit. +- [ ] 4.3 Annotate add-supertags design.md D4: the ad-hoc authoring surface + is this change's grid, not remoras' inline `key:: value` syntax. -- 2.51.2