Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
47fb266
plan: file listings epic (bd-61cd) and 12 sub-issues
May 5, 2026
cb238df
plan: extend listings epic table with sub-plan filenames
May 5, 2026
e5b81e1
plan: resolve all six listings-epic open questions
May 5, 2026
f4773e8
plan: write L0 sub-plan (bd-n8a4 — ListingItemInfo profile extension)
May 5, 2026
dd00584
plan: record L0 clarifications (C1–C6, D7) ahead of impl
May 5, 2026
ab28ea0
L0: ListingItemInfo profile extension (bd-n8a4)
May 5, 2026
57671f9
Merge L0 (bd-n8a4): ListingItemInfo profile extension
May 5, 2026
85870c8
sync beads: bd-n8a4 closed (L0 listings — ListingItemInfo profile ext…
May 5, 2026
cb3c82e
plan: write L1 sub-plan (bd-izqh — ListingItemInfoStage auto-fill)
May 5, 2026
14d956c
plan: defer inlines_to_text consolidation (bd-zzke); simplify L1
May 6, 2026
188f5bc
plan: route L1 mtime through SystemRuntime; file bd-a3we
May 6, 2026
3874999
L1: ListingItemInfoStage auto-fill (bd-izqh)
May 6, 2026
a56e0e9
Merge L1 (bd-izqh): ListingItemInfoStage auto-fill
May 6, 2026
e6ee0bd
sync beads: bd-izqh closed (L1 listings — ListingItemInfoStage)
May 6, 2026
acfa4fb
plan: mark L1 (bd-izqh) closed in listings epic table
May 6, 2026
3b8dd64
L3+L4: doctemplate pipes, listing types + Q-12 catalog (bd-ml8z, bd-b…
May 6, 2026
2bac0f3
sync beads: file L3 follow-up issues bd-0fd0, bd-0jyl, bd-0wyo
May 6, 2026
7e614c1
L3 phase 4: ListingGenerateTransform (bd-ml8z)
May 6, 2026
51e975b
L3 phase 5: ListingRenderTransform + built-in templates (bd-ml8z)
May 6, 2026
20f79b2
L3 phase 6: pipeline wiring + e2e listings rendering (bd-ml8z)
May 6, 2026
c5b4ccc
L3 phase 7: vendor list.min.js + quarto-listing.js (bd-ml8z)
May 6, 2026
a45f92f
sync beads: file bd-57y4 (quarto-listing.scss integration)
May 6, 2026
8c63e4e
plan: L3 close-out — verification status + final test counts
May 6, 2026
92749df
scripts: upload-project.mjs — push on-disk Q2 project to a sync server
May 6, 2026
ff23a2a
L3 fix: emit .qmd source paths from listing items so LinkRewrite hand…
May 6, 2026
782bcc4
plan: L3 hand-off summary for the next session (bd-ml8z)
May 6, 2026
b630f9f
sync beads: file bd-xs2u (em-dash in titles breaks hub-client)
May 6, 2026
8939528
plan: L2 — Listing data model reference doc (bd-j60g)
May 7, 2026
b4f2238
Merge L2/L3/L4 (bd-j60g, bd-ml8z, bd-b5jm): listings resolve transfor…
May 7, 2026
23345bb
sync beads: close L2/L3/L4 (bd-j60g, bd-ml8z, bd-b5jm)
May 7, 2026
43256c1
plan: L5 sub-plan — categories sidebar (bd-5vsr)
May 7, 2026
2750546
L5: categories sidebar (bd-5vsr)
May 7, 2026
67a985f
L5 follow-on: snapshot tests + parse_contents PandocInlines fix
May 7, 2026
9e8afa0
Merge L5 (bd-5vsr): categories sidebar
May 7, 2026
4e271de
sync beads: close bd-5vsr (L5) + file follow-ups
May 7, 2026
ffa4d22
L6: dep-graph integration for listings (bd-xbnf)
May 7, 2026
8b5efb9
Merge L6 (bd-xbnf): listings dep-graph integration
May 7, 2026
cd4b77f
sync beads: close bd-xbnf (L6) + epic plan field-name update
May 7, 2026
d487714
L7: post-render listing-preview substitution (bd-qf7r)
cscheid May 7, 2026
dc3a0f7
Merge L7 (bd-qf7r): listings post-render placeholder upgrade
cscheid May 7, 2026
52944cf
sync beads: close bd-qf7r (L7) + epic plan update
cscheid May 7, 2026
92ca4c5
L8: custom listing templates (bd-rqgx)
cscheid May 8, 2026
b3cf9fb
sync beads: close bd-rqgx (L8) + L8 follow-ups + epic plan update
cscheid May 8, 2026
cd2410f
Merge L8 (bd-rqgx): listings custom templates
cscheid May 8, 2026
f017dc9
listings epic: record L8 merge hash
cscheid May 8, 2026
b8c9b8b
listings L9 plan: file sub-plan for RSS feeds (bd-o90m)
cscheid May 8, 2026
8b7a928
L9 phase 1: diagnostics + imagesize dep (bd-o90m)
cscheid May 8, 2026
16ece34
L9 phase 2: feed binding (bd-o90m)
cscheid May 8, 2026
f4c241c
L9 phase 3: ListingFeedStageTransform (bd-o90m)
cscheid May 8, 2026
d2f79cc
L9 phase 4: ListingFeedLinkTransform (bd-o90m)
cscheid May 8, 2026
33abdb8
L9 phase 5: reader extension (bd-o90m)
cscheid May 8, 2026
4cdef21
L9 phase 6: complete_staged_feeds post-render step (bd-o90m)
cscheid May 8, 2026
0bdd219
L9 phase 7: end-to-end CLI verification + module-doc cleanup (bd-o90m)
cscheid May 8, 2026
ccd0086
sync beads: L9 (bd-o90m) close + 13 follow-ups (bd-o90m)
cscheid May 8, 2026
f5475bb
Merge L9 (bd-o90m): listings RSS feeds
cscheid May 8, 2026
24c8447
listings epic: record L9 merge hash
cscheid May 8, 2026
dd20d24
plan: L11 — listings epic close-out (bd-qb4o)
cscheid May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .claude/rules/wasm.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,42 @@ Correct pattern:
#[cfg(not(target_arch = "wasm32"))]
// Native code (full Lua stdlib via Lua::new())
```

## Async traits use `#[async_trait(?Send)]`

All async traits in this project must use `#[async_trait(?Send)]`,
not the default `#[async_trait]`.

Correct pattern:
```rust
use async_trait::async_trait;

#[async_trait(?Send)]
impl PipelineStage for MyStage {
async fn run(&self, input: PipelineData, ctx: &mut StageContext)
-> Result<PipelineData, PipelineError> { /* ... */ }
}
```

**Why.** The `#[async_trait]` macro rewrites `async fn` on a trait into a
function returning a `Pin<Box<dyn Future + 'a>>`. The default form adds a
`+ Send` bound to that future, requiring everything captured across `await`
points to be `Send`. The `?Send` form drops that requirement.

The same trait definitions are used by both:

- **Native CLI** — could satisfy `Send`, but the codebase uses
single-task execution; `Send` would be over-restrictive.
- **WASM (hub-client)** — `wasm32-unknown-unknown` is single-threaded;
`Send` is meaningless there. Several captured types in WASM contexts
(e.g. `Rc<RefCell<…>>`, JS interop handles) are not `Send` and would
make the trait uncompilable for WASM if `Send` were required.

`?Send` is the lowest-common-denominator that lets one trait definition
serve both targets. The cost is that you cannot `tokio::spawn` such a
future onto a multi-threaded runtime — but the pipeline doesn't do that;
stages run sequentially within a single task.

If you find yourself wanting to drop `?Send`, that is a signal something
is wrong with the design of the calling context, not with the trait.
Stop and ask before changing it.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 129 additions & 0 deletions claude-notes/designs/document-profile-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ produced.
| `nav_dependencies` | `Vec<PathBuf>` of project-relative `.qmd` paths the user explicitly declares as cross-doc dependencies via `meta.project.nav-dependencies`. The Phase-8 dependency graph adds an edge to each declared target. The escape hatch for Lua filters that walk siblings without using sidebar / link / prev-next channels. Default empty. |
| `always_render` | `bool` from `meta.project.always-render`. When `true`, Mode B (subset render) pulls this page into the render set if any of its dependents is among the user-named targets. Mode A re-renders every page anyway, so this flag has no Mode-A effect. Default `false`. |
| `body_link_targets` | `Vec<PathBuf>` of project-relative `.qmd` paths this page links to from its body content. Populated by `LinkResolutionStage` (Pass-1) using the same `resolve_doc_relative_target` helper Phase 6's `LinkRewriteTransform` calls in Pass-2 — equivalence test asserts the two produce the same set. The Phase-8 dependency graph turns each target into an edge. See `body-link-resolution-contract.md`. Default empty. |
| `resources` | `Vec<String>` of document-level `resources:` patterns from the merged frontmatter (`bd-o8pr`). Raw patterns; expansion happens at the post-render collector. The snapshot of what the author declared at frontmatter-freeze time — engines and Lua filters that run later contribute through a separate channel (`DocumentResourceReport`) and cannot retroactively shrink this list. Default empty. |
| `categories_raw` | `Option<ConfigValue>` carrying the originating tagged value of the top-level `categories:` key (`bd-n8a4`). Mirrors `categories` but preserves `!prefer` / `!concat` merge tags so listings consumers can feed it (alongside `listing_item.categories_raw`) into `quarto_config::MergedConfig` for tag-aware merging. Most consumers should keep reading the flattened `categories`; only listings reach for the raw form. Default `None`. |
| `listing_content_globs` | `Vec<String>` of unresolved glob strings from the host page's `listing.*.contents:` declarations (`bd-xbnf`, listings L6). Flattened across all listings on the page. The dependency-graph builder expands these against `ProjectIndex` at graph-build time (host-relative first, project-relative fallback — matches L3's render-time rule) to add forward edges from each listing host to its content files; hosts with non-empty entries are also added to the graph's `force_render` set so Mode B (`quarto render posts/foo.qmd`) pulls in listing hosts when any of their content files is targeted. Resolution is **not** cached on the profile (the per-doc cache cannot represent dependency on the full project source set safely). Default empty. |
| `listing_item` | `ListingItemInfo` advertising per-document data for listings consumers (`bd-n8a4`). **Scoped feature surface — listings only**; non-listing consumers must use the corresponding top-level fields (`title`, `description`, `image`, …). Author-supplied values populate during `DocumentProfile::extract`; `ListingItemInfoStage` (`bd-izqh`, L1, landed) auto-fills holes pre-checkpoint for `description` (full first paragraph), `image` (first inline image's URL), `word_count` (Q1-parity tokenization, footnote text excluded), `reading_time_minutes` (`ceil(word_count / 200)`), and `date_modified` (filesystem mtime via `SystemRuntime::path_metadata` formatted as `YYYY-MM-DD` UTC). Author values always win — the stage strictly fills holes. The nested `extra: BTreeMap<String, ConfigValue>` is the **only** open-shape field in the profile and is forbidden to non-listing consumers — see §"Scoped feature surfaces". Default empty (`ListingItemInfo::is_empty()`). |

## Non-guarantees (explicit)

Expand All @@ -87,6 +91,52 @@ What a profile **does not** contain:
- **Absolute filesystem paths.** Everything path-shaped is
project-relative by construction.

## Scoped feature surfaces

Most profile fields are typed, narrowly defined, and globally
readable: any consumer that needs `title`, `categories`,
`outline`, etc. reaches for the top-level field directly. The
contract is closed-shape, versioned, and stable.

The `listing_item` field is an **explicit exception**, scoped to
one feature (listings) by name and by convention.

**Allowed:** the listings code path (planned
`L3 ListingResolveTransform`, `L5 CategoriesSidebarTransform`,
`L7 post-render upgrade`, `L9 RSS feeds`) reads
`profile.listing_item` to materialize listing items.

**Forbidden:** any code outside the listings module reaches into
`profile.listing_item` (and especially into
`profile.listing_item.extra`). Sidebar generation, navbar
rendering, cross-doc link rewriting, freeze, and other features
must continue to use the typed top-level fields. If a future
feature finds itself wanting to read `listing_item`, that is a
**redesign trigger** — either widen the typed top-level field set
with a versioned bump, or define a new scoped feature surface. Do
not silently broaden listings' scope.

The discipline is enforced by code review, not the type system.
The `listing_item` field is `pub` for serde and for listings' own
use; the contract above is the boundary that matters.

This is the same discipline `bd-fegm` (Phase 8) used when it
declined to add a generic `extras: HashMap` field for filter-
introduced data and chose typed fields instead. The exception
here is granted because (a) custom listing templates genuinely
need access to author-declared free-form metadata, and (b) the
"named, scoped" framing keeps the cost of the exception locally
bounded.

The companion field `categories_raw: Option<ConfigValue>` and its
sibling `listing_item.categories_raw` are likewise listings-only
surfaces: their purpose is to preserve `!prefer` / `!concat`
merge tags so listings consumers can apply tag-aware merging via
`quarto_config::MergedConfig`. Non-listing consumers continue to
read the flattened `categories: Vec<String>`. See
`claude-notes/plans/2026-05-05-listings-L0-profile-extension.md`
§"D7" for the design rationale.

## Mutability

**Profiles are read-only.** A Phase 1+ user filter that wants to
Expand Down Expand Up @@ -280,3 +330,82 @@ Tracking: `bd-creo` (CLI strictness), `bd-mwtf` /
See companion contracts:
`claude-notes/designs/body-link-resolution-contract.md`,
`claude-notes/designs/sidebar-auto-expansion-contract.md`.
- **2026-04-29 — v3 (`bd-o8pr`).** `DOCUMENT_PROFILE_VERSION`
bumped 2 → 3. One new field:
- `resources: Vec<String>` — document-level `resources:`
patterns from frontmatter, snapshot at frontmatter-freeze
time. The post-render collector expands the patterns and
augments them with engine/filter contributions through a
separate channel (`DocumentResourceReport`); the profile
field is read-only and immutable downstream of the
checkpoint. Default empty.
v2 cache entries on disk are rejected with
`DocumentProfileError::VersionMismatch` and silently
regenerated.
- **2026-05-05 — v4 (`bd-n8a4`, listings epic L0).**
`DOCUMENT_PROFILE_VERSION` bumped 3 → 4. Two new fields, both
additive at the on-disk layer (`skip_serializing_if` keeps
default profiles compact):
- `listing_item: ListingItemInfo` — scoped per-feature
surface for listings consumers. Curated typed sub-fields
plus `extra: BTreeMap<String, ConfigValue>` for custom
listing-template fields. Default empty
(`ListingItemInfo::is_empty()`). Outer profile shape
stable; additions or removals of keys inside `extra` do
**not** require a future bump. Non-listing consumers are
forbidden from reading this field — see new §"Scoped
feature surfaces".
- `categories_raw: Option<ConfigValue>` — tagged form of the
top-level `categories:` value, preserving `!prefer` /
`!concat` merge tags for listings consumers' tag-aware
merging via `quarto_config::MergedConfig`. Most consumers
keep reading the flattened `categories: Vec<String>`;
only listings reach for the raw form. Default `None`.
v3 cache entries on disk are rejected with
`DocumentProfileError::VersionMismatch` and silently
regenerated, identical to the v2 → v3 cascade.
Plan: `claude-notes/plans/2026-05-05-listings-L0-profile-extension.md`.
Parent epic: `bd-61cd`
(`claude-notes/plans/2026-05-05-listings-epic.md`).
- **2026-05-06 — `ListingItemInfoStage` lands (`bd-izqh`, listings
epic L1).** No version bump; the field shape is unchanged. New
pre-checkpoint stage between `IncludeExpansionStage` and
`DocumentProfileStage` auto-fills `meta.listing-item.{description,
image, word-count, reading-time-minutes, date-modified}` from the
post-include AST (and filesystem mtime via
`SystemRuntime::path_metadata`) when the author hasn't supplied
them. `DocumentProfileStage` then extracts the enriched
`ListingItemInfo` via the same path it used for purely
author-supplied values in v4. `categories` is **not** auto-filled
by L1 (D8 — listings consumers do their own L0-`categories_raw`-aware
merge). The hub-client/WASM pipeline runs the same stage, but
`date_modified` stays `None` until `bd-a3we` teaches the Automerge
VFS to surface change-history time. Plan:
`claude-notes/plans/2026-05-05-listings-L1-autofill-stage.md`.
- **2026-05-07 — v5 (`bd-xbnf`, listings epic L6).**
`DOCUMENT_PROFILE_VERSION` bumped 4 → 5. One new field, additive
at the on-disk layer (`skip_serializing_if` keeps default
profiles compact):
- `listing_content_globs: Vec<String>` — flattened glob strings
from the host page's `listing.*.contents:` declarations.
*Unresolved* globs only; resolution happens at graph-build
time inside
`crate::project::dependency_graph::ProjectDependencyGraph::build`,
which expands each glob against the full project source set
(host-relative first, project-relative fallback — same rule
`ListingGenerateTransform` uses at render time) to add forward
edges from each listing host to its content files. Hosts with
non-empty entries are also added to the graph's
`force_render` set so Mode B (`quarto render posts/foo.qmd`)
automatically pulls in listing hosts when any of their
content files is in the user-named target set. Resolution is
**not** cached on the profile because it depends on the full
project source set, which a per-doc profile can't represent
safely (a new sibling `.qmd` would not invalidate the host's
profile cache, leaving the resolution stale). Default empty.
v4 cache entries on disk are rejected with
`DocumentProfileError::VersionMismatch` and silently
regenerated, identical to every prior bump.
Plan: `claude-notes/plans/2026-05-07-listings-L6-dep-graph.md`.
Parent epic: `bd-61cd`
(`claude-notes/plans/2026-05-05-listings-epic.md`).
Loading
Loading