Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
fff3053
Initial version of core
breezykermo Sep 19, 2026
94ee8f6
Keeps an inline element from splitting a truncated window's paragraph
breezykermo Sep 19, 2026
9246667
Documents how an unnamed idea takes its name from its own content
breezykermo Sep 20, 2026
2769e06
Adds content-derived slug and digest helpers to core's pure namespace
breezykermo Sep 20, 2026
9b2ce78
Names a titleless note from its own content instead of a container or…
breezykermo Sep 20, 2026
58f0c56
Fails the build when two different notes resolve to the same id.
breezykermo Sep 20, 2026
ff26b44
Covers content-derived note ids in the demos: a demo/pure case for th…
breezykermo Sep 20, 2026
36d4044
Documents content-derived note names in core's readme and naming.md
breezykermo Sep 20, 2026
122cc85
Warns instead of erroring when two identical notes resolve to the sam…
breezykermo Sep 20, 2026
c403c37
Includes the windowed note pair in the native demo root
breezykermo Sep 20, 2026
0449753
Tracks the bird ideas for every package in the rookery
breezykermo Sep 20, 2026
f118d96
Numbers a note's footnotes while its body is being built, not while i…
breezykermo Sep 20, 2026
4a9610f
Backlinks the page a tag-selected window shows notes from
breezykermo Sep 20, 2026
369eb47
Backlinks the note whose body carries a tag-selected window, resolvin…
breezykermo Sep 20, 2026
871c648
Documents backlink behaviour for tag-selected windows in the core readme
breezykermo Sep 20, 2026
64adebc
Drops the dead id parameter from _flatten and _body-at
breezykermo Sep 20, 2026
2d4b3e0
Renames #ideate-id to #ideate-name
breezykermo Sep 20, 2026
91002a0
Renames the display-id argument, display dictionary key and state bin…
breezykermo Sep 20, 2026
6b9d31b
Tracks the slipshow half of the display-id rename
breezykermo Sep 20, 2026
34f710b
Renames the id-color theme key and --idea-id-color custom property to…
breezykermo Sep 20, 2026
eebf3cb
Says name, not id, throughout core's readme
breezykermo Sep 20, 2026
ec315e6
Tracks the consumer half of the id-color rename
breezykermo Sep 20, 2026
d979ce7
Tracks five birds from the core package review
breezykermo Sep 20, 2026
22400e4
Binds the bibliography entry pattern once and resolves the registry o…
breezykermo Sep 20, 2026
2126251
Describes seven readme passages by what core does now rather than wha…
breezykermo Sep 20, 2026
d5d0d75
Records why an idea row tests the bare target() builtin rather than c…
breezykermo Sep 20, 2026
9d24419
Describes fourteen core comments by what the code does now rather tha…
breezykermo Sep 20, 2026
9874ce5
States what core's fixtures assert rather than the bugs they once caught
breezykermo Sep 20, 2026
1c5614f
Files the birds for core's 0.1.0 release review
breezykermo Sep 20, 2026
91799ae
Moves the design notes into docs/design and drops the two that are fi…
breezykermo Sep 20, 2026
1b69930
Renames the #rookery argument note-dir to idea-dir
breezykermo Sep 20, 2026
3a90bb6
Drops tracker ids and debugging narration from shipped comments
breezykermo Sep 20, 2026
00dc458
Strips the interior section banners from core's src
breezykermo Sep 20, 2026
c8337c7
Corrects comments in core's src that describe code the package lacks
breezykermo Sep 20, 2026
6e56819
Halves the comment volume in urls.typ and base.typ
breezykermo Sep 20, 2026
aa04d19
Raises core's readme to the rheo 0.6.2 floor its manifest declares
breezykermo Sep 20, 2026
17fb47f
Cuts the readme's migration guidance for a lineage nobody installed
breezykermo Sep 20, 2026
186f4e5
Exports idea-key, an idea's Typst label name as a string
breezykermo Sep 20, 2026
e8566a8
Drops the last section banner and corrects the rheo demo's stated floor
breezykermo Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .birds/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/workspaces/
/workspace-baselines/
/tombstones/
/ideas/.locks/
/ideas.cache
95 changes: 95 additions & 0 deletions .birds/ideas/rk-accept-and-or-not-and-a-leading-dash-fbf676d5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
id: rk-accept-and-or-not-and-a-leading-dash-fbf676d5
short-id: fb
title: Accept AND, OR, NOT and a leading dash
priority: 1
labels:
- type:feature
- search-clause-roles
deps:
- blocked-by:rk-make-the-whole-query-one-clause-tree-6f7c3949
closed: true
---
Accept `AND`, `OR`, `NOT` and a leading `-` as spellings of `&`, `|` and `!`, so
the query language matches the syntax readers already know from other search
boxes.

## Why both spellings and not one

Lucene, Xapian and Bleve all accept the terse symbol form AND the keyword form
simultaneously, and that is the field's settled answer rather than an oversight:

- Lucene's classic parser takes `AND` / `OR` / `NOT` as well as `+` / `-`:
<https://lucene.apache.org/core/8_0_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html>
- Xapian's parser takes `AND OR NOT XOR NEAR` alongside `+` / `-`:
<https://xapian.org/docs/apidoc/html/classXapian_1_1QueryParser.html>

The keyword form is the discoverable one; the symbols are the shortcut for
someone who has learned them. There is evidence a terse operator alone does not
get learned: when Google retired `+` in 2011 it reported the operator appeared in
under 0.5% of searches and that two thirds of those uses were wrong.

## What is already in the tree — do not re-derive

- `_prec` at `/home/lox/code/_fcl/rookery/search/0.1.0/src/tagquery.typ` line 40 is
`("!": 3, "&": 2, "|": 1)`, and the tokenizer tests membership with `c in _prec`
on a SINGLE cluster. A multi-character keyword cannot come out of that test as it
stands.
- `parse-tag-query` at line 58 accumulates non-operator clusters into an atom, so
`AND` currently parses as an atom with the value `and`.
- Its twin is `parseTagQuery` at `src/tagquery.js` line 39, with `PREC` mirroring
`_prec`.
- The escape set is `( ) | & ! \` and the file states it is FROZEN — a tag holding
one of those must be escapable, and promoting a new character to an operator
would change what already-written queries mean. Adding a KEYWORD does not touch
that set, which is why this bird is safe; adding a new symbol would not be.
- An earlier bird in this label made an unescaped space an implicit-AND operator.

## Steps

1. When an atom is about to be pushed, test whether its unfolded text is exactly
`AND`, `OR` or `NOT`, case-insensitively. If it is, emit the corresponding
operator token (`&`, `|`, `!`) through the existing precedence loop instead of
an atom token. Doing the test at PUSH time rather than in the tokenizer is what
keeps the single-cluster `c in _prec` test intact and the diff small.
2. A keyword must be a whole word to count. `android` is an atom, not `AND` plus
`roid`. Because the test runs on a complete accumulated atom, this falls out
for free — say so in a comment so nobody reintroduces a substring test.
3. An escaped keyword is an atom: `\AND` searches for the word "and". The escape
already forces its cluster into the atom, so make the keyword test skip any
atom that consumed an escape. Track that with a flag set in the `\` branch.
4. Accept a leading `-` on a clause as `!`: `-tags:draft` is `!tags:draft`, and
`window -depth` is `window & !depth`. Recognise it only where the `-` opens an
atom (nothing accumulated yet and an operator or the start of input precedes
it), so a `-` INSIDE a word stays part of the word. This matters: `_fold` turns
`-` into a space, and `in-progress` must keep working as a value.
5. Mirror every rule in `parseTagQuery` in `tagquery.js`.
6. Add `<tag-parity>` fixture cases in `test/parity.typ` for: `a AND b`,
`a and b`, `a OR b`, `NOT a`, `android`, `\AND`, `-tags:draft`,
`window -depth`, and `in-progress`.

## Do NOT

- Do not add any new character to the escape set or to `_prec`'s keys. The escape
set is frozen and widening it is a breaking change to queries already written.
- Do not add `+` as a synonym for `&`. A leading `-` earns its place because it is
near-universal for negation; `+` does not, and the Google data above is the
reason.
- Do not add `XOR`, `NEAR`, phrase quoting or ranges. Each is its own decision.
- Do not change precedence. `NOT` takes `!`'s precedence 3, `AND` takes 2, `OR`
takes 1, and `!`'s right-associativity rule at equal precedence stays exactly as
written.

## VERIFY

Run from `/home/lox/code/_fcl/rookery/search/0.1.0`:

1. `just parity` passes, including the nine new fixture cases.
2. `just test` passes.
3. `#context search-ideas("tags:draft AND window")` and
`#context search-ideas("tags:draft & window")` return identical rows in
identical order.
4. `#context search-ideas("android")` still matches the note whose name contains
"android", and `#context search-ideas("in-progress")` still matches the tag
`in-progress`.
5. `just build` succeeds.
200 changes: 200 additions & 0 deletions .birds/ideas/rk-accepts-only-heading-where-as-ideate-dc9435ee.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
---
id: rk-accepts-only-heading-where-as-ideate-dc9435ee
short-id: dc
title: Accepts only heading.where as ideate separator
priority: 3
labels:
- ideate-separator
deps: []
closed: true
---
Touches: core/0.1.0/src/ideate.typ, core/0.1.0/readme.md, core/0.1.0/test/units.typ

`#ideate` (in `@rookery/core`) accepts its `separator:` argument in five
spellings. Two of them name a heading level: the selector
`heading.where(level: 2)` and the element `heading(level: 2)[]`. Drop the
element form. After this bird the only heading spelling accepted is
`heading.where(level: N)`, for ANY N — not just 2 — and the element form
raises `#ideate`'s own panic instead of being honoured.

Why the element form goes: it exists only because `heading(level: 2)` bare is
illegal Typst (`error: missing argument: body`, since `heading` takes its body
positionally), so the element form has to carry a pointless `[]` to be written
at all. That is a trap dressed as a convenience — the selector spelling is
Typst's own idiom for naming a heading level, needs no empty body, and already
works. One accepted spelling is better than two when one of them is a
footgun.

`heading.where(depth: N)` must KEEP working — it is a second field spelling of
the same selector, already accepted, and nothing here changes that.

## Where the code is

All in `core/0.1.0/src/ideate.typ`. Run every `rg` from the repository root
(`/home/lox/code/_fcl/rookery`) so a moved file still resolves.

1. The classification block inside the `ideate` function.

```
rg -n 'let heading-elem = type\(separator\)' core/0.1.0
```

One hit, `src/ideate.typ:357` as of filing, inside `#let ideate(..)`. The
three lines there are:

- `let heading-elem = type(separator) == content and separator.func() == heading`
- `let heading-sel = type(separator) == selector`
- `let heading-mode = heading-elem or heading-sel`

2. The level-resolution line, immediately after the validation panic.

```
rg -n 'let want = if heading-elem' core/0.1.0
```

One hit, `src/ideate.typ:373` as of filing, inside `#let ideate(..)`. It
reads `let want = if heading-elem { _level-of(separator) } else if heading-sel { _sel-level(separator) }`.

3. The validation panic message listing the accepted spellings.

```
rg -n 'must be one of `par`' core/0.1.0
```

One hit, `src/ideate.typ:363` as of filing, inside `#let ideate(..)`. The
message runs from line 362 to line 371 and ends `+ repr(separator),`.

4. The file-header comment block documenting the spellings.

```
rg -n 'the same, as an element' core/0.1.0
```

Two hits, one per file — `src/ideate.typ:37` (the five-row list under the
comment heading `// ---- Choosing what starts a note: \`separator:\` ----`)
and `readme.md:597` (a table row). Both are the element form's own
documentation and both go.

5. The readme prose asserting the element form is supported.

```
rg -n 'stays supported and needs no string' core/0.1.0/readme.md
```

One hit, `readme.md:632` as of filing, in the section headed
`### Choosing what starts a note`.

```
rg -n 'naming both accepted forms' core/0.1.0/readme.md
```

One hit, `readme.md:658` as of filing, same section. It says the panic names
"both accepted forms" — after this bird there is one.

Anchors 4 and 5 are on text this bird DELETES. They stop matching once the step
lands, which is expected and is why they must not appear in VERIFY. If an
anchor does not hit at the start, widen the search to the repository root; if it
is still gone, stop and report the miss rather than guessing where the text
went.

## Steps

1. In `src/ideate.typ`, delete the `heading-elem` binding and rewrite
`heading-mode` to be `heading-sel` alone. Keep the `heading-mode` NAME — it
is read in four places further down (the `if not (none-mode or heading-mode
or par-mode)` gate, the group-splitting `is-separator` branch, the
`lead-i` position lookup, and the title/name/tags-function guard) and none of
those need to change.

2. Rewrite `let want = ...` to call `_sel-level(separator)` when `heading-mode`,
with no `heading-elem` branch.

3. Rewrite the validation panic message to name FOUR spellings, not five:
`par` (every paragraph becomes a note), `parbreak` (the same thing),
`heading.where(level: 2)` (every heading of that level starts a note — any
level, and `depth:` works in place of `level:`), and `none` (nothing splits
— the whole body is one note). Add one sentence telling a caller who wrote
`heading(level: 2)[]` to write `heading.where(level: 2)` instead. Keep the
trailing `+ repr(separator),` so the panic still shows what it got. Do NOT
describe `par` as "the default" in the new message — a sibling bird is
changing that default, and a message asserting it will be wrong.

4. Update the file-header comment block: the five-row list becomes four rows
(drop the `heading(level: 2)[]` row). Leave the `(the default)` annotation
where it is, on the `par` row — it is still true, and a sibling bird moves
it. The block further down headed
`// \`heading(level: 2)\` BARE IS ILLEGAL TYPST` currently explains why the
element form exists and why the bare form cannot be caught here; replace it
with at most three lines saying that `heading(level: 2)` and
`heading(level: 2)[]` are both refused, the first by Typst's own compiler
before this function is reached and the second by the panic above, and that
`heading.where(level: 2)` is the spelling to use. Follow the project comment
style in `CLAUDE.md`: describe the present, no history, no "used to".

5. Update `readme.md` in the section headed `### Choosing what starts a note`:
delete the element-form table row, delete the paragraph anchored by "stays
supported and needs no string", and reword the "both accepted forms"
sentence to the singular. The prose anchored by "on its own is illegal
Typst" should stay in some form but shrink — the bare form is still a thing
a reader will try — and must no longer present the element form as the
remedy. Say `heading.where(level: 2)` is the remedy.

6. In `core/0.1.0/test/units.typ`, find the `_level-of` assertions:

```
rg -n '_level-of\(heading\(level: 2\)\[\]\)' core/0.1.0/test/units.typ
```

One hit, `test/units.typ:639` as of filing, in the section headed
`// ---- #ideate's three pure predicates ----`. `_level-of` itself is NOT
being removed — it is still called on markup headings found in a body, which
carry `depth` rather than `level` — so keep the assertions as tests of that
helper. Add one assertion beside the existing `_sel-level` ones proving a
level OTHER than 2 resolves, e.g. `#assert.eq(_sel-level(heading.where(level: 1)), 1)`,
since "any level works" is the claim this bird makes and nothing currently
pins level 1.

## What NOT to do

- Do NOT remove `_level-of`. It is load-bearing for reading markup headings out
of the body being split.
- Do NOT change `_sel-level`, its `repr`-parsing regex, or its panic. They
already accept any level and both field spellings.
- Do NOT change the DEFAULT value of `separator:`. It stays `par` in this bird;
a separate bird changes it.
- Do NOT add a `title:`/`document.title` behaviour. A separate bird does that.
- Do NOT export a `heading` of your own, or shadow the `heading` element, to
make the bare form work. That breaks every consumer's `#show heading:` rule
outright (`error: only element functions can be used as selectors`).
- Do NOT touch any package other than `core/0.1.0`. A repository-wide search
for `ideate` and `separator:` found no hits outside it.

## VERIFY

Run from `core/0.1.0/`:

1. `just test` exits 0 and prints `units OK`.
2. `cd demo/rheo && just check` exits 0 and prints its own OK line. The demos
at `demo/rheo/content/ideated.typ` and `demo/rheo/content/ideated-named.typ`
already use the selector spelling, so they must keep passing untouched.
3. `cd demo/rheo && just check-typst` exits 0.
4. The element form now panics. Write a scratch file OUTSIDE the repository,
e.g. `/tmp/sep.typ`:

```typ
#import "@rookery/core:0.1.0": ideate
#ideate(separator: heading(level: 2)[])[== A

body]
```

and compile it with
`typst compile --features html --format html /tmp/sep.typ /tmp/sep.html`.
It must FAIL with `#ideate`'s own `separator:` panic naming
`heading.where(level: 2)`. Typst has no way to assert a panic inside
`test/units.typ`, which is why this is a manual step rather than a unit
assertion. Note that this scratch compile resolves `@rookery/core` through
the machine's package cache rather than the flight; if it picks up different
code, say so in the landing message instead of treating it as a failure.
5. `rg -n 'heading.where\(level: 1\)' core/0.1.0/test/units.typ` hits, showing
the any-level assertion was added.
Loading
Loading