v0.1.2 shipped — render() v1 (folder + file + heading)
What shipped
Section titled “What shipped”Milestone v0.1.2 — render() v1 (folder + file + heading mechanisms). Status flipped to ✅ in the milestone hub.
The pure-function render pipeline from Ch 22 synthesis §3.1 is now the single coupling point between recipes and vault layout.
| Surface | Delivered |
|---|---|
src/render/types.ts | Address, ConceptIdentity, SourceScope types matching Ch 22 §3.1 spec |
src/render/template.ts | R2RML-style {var|filter} interpolation + 7-filter closed pipeline (lower, upper, title, slug, tagsafe, fs-safe, truncate(N)); dotted-path variable resolution; throws RenderError for unknown filter / missing var / malformed expression |
src/render/mechanisms/folder.ts | Folder mechanism — appends directory segment to address.primary.path |
src/render/mechanisms/file.ts | File mechanism — leaf-bearing markdown file; auto-appends .md if missing |
src/render/mechanisms/heading.ts | Heading mechanism — intra-file anchor; nested anchors via Obsidian’s heading-range form (Note#H1#H2); level_depth 1..6 enforced |
src/render/mechanisms/tag.ts | Stub — throws informative “deferred to v0.2” error pointing users at also_emit.tags for the v0.1 path |
src/render/mechanisms/wikilink.ts | Stub — throws informative “deferred to v0.2” error pointing at v0.2 wikilink-graph + graph_edges work |
src/render/index.ts | Public render() function dispatching layout entries by mechanism, applying also_emit cross-cutting (tags + aliases + managed frontmatter), computing Pass-1 absolute-form wikilinkTarget, ensuring curie always lands in frontmatter |
src/main.ts | render exposed as plugin-instance handle for E2E reachability |
| Suite | Count | Passing |
|---|---|---|
Jest unit (tests/render.test.ts) | 37 (template engine + 3 worked NIST examples + also_emit + 6 error cases + 3 determinism) | ✅ |
| Jest unit total (all suites) | 64 | ✅ |
WebDriver E2E (tests/e2e/render.spec.ts) | 6 (handle exposure + 2 worked recipes + also_emit + tag-deferral + 50-iteration determinism inside Obsidian) | ✅ |
| WebDriver E2E total | 19 | ✅ |
| Grand total | 83 tests across 8 spec files | ✅ |
E2E tests run against real Obsidian v1.12.7 via wdio-obsidian-service. Build clean.
Notable design decisions made during implementation
Section titled “Notable design decisions made during implementation”- Determinism is a first-class test. Both unit + E2E include explicit determinism checks: same
(recipe, identity)→ byte-identicalAddressoutput across 100 (unit) and 50 (E2E) iterations. This protects the canonical-state hashing commitment from Ch 22 §8 — ifDate.now()orMath.random()ever leaks into render(), the determinism test fails red. - Filter pipeline left-to-right semantics.
{var|lower|truncate(3)}applieslowerfirst, thentruncate(3). Tests cover the chained case explicitly; otherwise a future refactor that reverses pipeline order would silently break recipes. fs-safefilter strips spaces too. Windows treats trailing spaces as illegal; sync clients (OneDrive notably) misbehave above 256 bytes per Ch 22 §1.1. The filter strips<>:"/\|?*-and-spaces in one pass, with trailing dot/space stripped after.- Heading anchors use
#separator (Obsidian’s heading-range form). Nested headings stack asH1#H2#H3in the anchor portion.[[Note#H1#H2]]is Obsidian’s way of resolving deep anchors. - Stubs for tag + wikilink mechanisms throw informative errors. Rather than silent no-ops or generic “not implemented”, they explain what to use instead (e.g.,
also_emit.tagsfor emitting tags when the layout mechanism is not tag) and when the deferred feature ships (v0.2). Prevents misdiagnosis. render()exposed on plugin instance for E2E reachability. Module-level pure function; the plugin instance just forwards a handle. No duplicate state.
How render() v1 plugs into the system
Section titled “How render() v1 plugs into the system”Per Ch 22 §3, render() is the single coupling point between recipe (the user’s declarative spec) and vault layout (the actual files Tier 1 contains). Everything upstream of render is recipe + source-data validation; everything downstream is bytes-on-disk.
Interfaces this milestone introduces
Section titled “Interfaces this milestone introduces”| Interface | Defined in | Consumers (current + planned) |
|---|---|---|
Recipe (loose runtime shape) | src/render/index.ts | render() itself; v0.1.3 generation engine; future external producers via spec/recipe.schema.json |
ConceptIdentity | src/render/types.ts | render(); v0.1.3 engine builds one per source row from CURIE + scope variables |
SourceScope | src/render/types.ts | Populated upstream of render — typically { catalog: {...}, family: {...}, control: {...} } per source level |
Address (render output) | src/render/types.ts | v0.1.3 engine writes notes from this; v0.1.4 junction notes consume it; v0.1.5 sidecar projects from frontmatter; v0.1.7 exporters read tags / aliases / managed frontmatter |
RenderError | src/render/template.ts | Surfaced to user via Obsidian Notice in v0.1.3 (the milestone E2E asserts the AJV-Notice path); thrown by template engine + mechanisms for any pre-write failure |
Template grammar ({var|filter|filter}) | src/render/template.ts | Recipe authors writing target.layout[].template, also_emit.tags[], also_emit.aliases[], also_emit.frontmatter.managed values |
Plugin-instance handle plugin.render | src/main.ts | E2E tests (already exercising 6 cases); v0.1.3 generation engine refactor will call this internally; future commands that want one-off rendering |
What did NOT change in this milestone
Section titled “What did NOT change in this milestone”- Tier 1 schema (
spec/tier1.schema.json) — render() produces anAddress, not a Tier 1 frontmatter object directly. The generation engine in v0.1.3 maps Address → Tier 1 frontmatter via the merge logic. The two schemas remain decoupled: render’s output is layout-shaped; Tier 1 is content-shaped. - Recipe schema (
spec/recipe.schema.json) — no changes. render() consumes the same shape AJV validated in v0.1.1. The schema still allowstag/wikilinkmechanism values; render() rejects them with informative deferral errors. This is correct: the spec admits the future grammar; the runtime implements a subset. - Plugin command surface — still two commands (
import-structured-data,browse-saved-configs). Render is internal infrastructure; users don’t invoke it directly. - Generation engine (
src/generation/generation-engine.ts) — still uses the v0.1.0 column-role logic. The refactor to call render() per row is the entire scope of v0.1.3. - Validator wiring — already calls
validateRecipe()at recipe load (v0.1.1). v0.1.3 addsvalidateTier1Frontmatter()calls pre-write once the engine emits spec-shaped frontmatter via render’s Address.
Where the architecture is “alive” now
Section titled “Where the architecture is “alive” now”| Tier | Status |
|---|---|
| Recipe-shape contract (machine-readable, validated, typed in code) | ✅ Live since v0.1.1 |
| Address-shape contract (what render produces) | ✅ Live since v0.1.2 (this milestone) |
| Tier 1 frontmatter contract (machine-readable, validated, typed) | ✅ Live since v0.1.1; not yet emitted by the generation engine |
| Generation engine consuming render() | 🚧 v0.1.3 (next) |
| Tier 2 sidecar | 📋 v0.1.5 |
| Output query layer (Bases) | 📋 v0.1.6 |
| Exporters (STRM TSV / OSCAL JSON) | 📋 v0.1.7 |
| Audit trail | 📋 v0.1.8 |
Worked example output
Section titled “Worked example output”From the unit + E2E tests, here are the three NIST examples from spec/recipe.schema.json rendered against nist:AC-2:
| Recipe | Output |
|---|---|
| (a) all-folders | path: Frameworks/NIST 800-53 r5/AC/AC-2.md; wikilinkTarget: Frameworks/NIST 800-53 r5/AC/AC-2 |
| (b) mostly-headings | path: Frameworks/NIST 800-53 r5.md; anchor: AC — Access Control#AC-2 Account Management; wikilinkTarget: Frameworks/NIST 800-53 r5#AC — Access Control#AC-2 Account Management |
| (e) hybrid (folder + folder + file + heading on enhancement) | path: Frameworks/NIST 800-53 r5/AC/AC-2.md; anchor (for nist:AC-2(1)): AC-2(1) Automated System Account Management |
What this unblocks for v0.1.3
Section titled “What this unblocks for v0.1.3”- Generation engine integration: the engine currently writes notes from the v0.1.0 column-role config shape. v0.1.3 refactors the engine to call
render()for each row, write toaddress.primary.path, embed body content underaddress.primary.anchor(when present), and mergeaddress.frontmatterwith user-preserve keys. - The
_crosswalkerprovenance writer now has a clearAddressto attach to. - Phase-0 compat shim: legacy
hierarchycolumn-role configs translate to atarget.layoutshape with all-folder mechanisms — a small adapter at the engine boundary.
What this does NOT do
Section titled “What this does NOT do”- Does not wire
render()into the generation engine yet (v0.1.3). - Does not implement Pass-2 link minimization (
linkStyle: shortest); that’s v0.3 — Pass-1 always emits absolute-form wikilinks. - Does not implement tag-as-layout-mechanism, wikilink-as-layout-mechanism, or
graph_edges— schema-reserved through v0.1; activated v0.2. - Does not implement the
Functionprimitive (Ch 20) — templates are limited to the closed 7-filter set; computation beyond that errors out. - Does not ship a
JSONataexpression layer — also Ch 20 territory; deferred until concrete need surfaces in real recipes.
Memory rules followed this session
Section titled “Memory rules followed this session”- ✅ Always test thoroughly before flipping milestone status (memory:
feedback_test_thoroughly.md) — this milestone shipped only after all 83 tests pass - ✅ No personal data in logs (memory:
feedback_no_personal_data_in_logs.md) — this log uses relative paths only
Related
Section titled “Related”- Milestone v0.1.2 page
- Ch 22 synthesis (target-structure grammar + render function spec)
- Ch 22 deliverable §3 (formal render signature)
- v0.1.1 delivery log — predecessor milestone
- Hierarchy primitives concept page
- Testing-patterns skill
Next milestone
Section titled “Next milestone”v0.1.3 — Generation engine integration — refactor src/generation/generation-engine.ts to call render() per row instead of computing path/frontmatter inline; managed/user_preserve frontmatter merge semantics; _crosswalker provenance writer; idempotent re-import. Per-milestone E2E spec is tests/e2e/re-import.spec.ts — must pass before flipping milestone status to ✅.