Skip to content
🚧 Early alpha — building the foundation. See the roadmap →

v0.1.4 shipped — Junction notes + crosswalk edges

Created Updated

Milestone v0.1.4 — Junction notes + crosswalk edges. Status flipped to ✅ in the milestone hub.

After v0.1.4, the plugin produces all three Tier 1 shapes — concept notes, junction notes (evidence links), crosswalk edges (cross-ontology mappings) — through one unified pipeline. The same render() function, the same also_emit.frontmatter.managed template engine, the same provenance writer, the same merge semantics. The discriminator that selects which Tier 1 shape to produce is a single field on the recipe layout: kind.

SurfaceDelivered
spec/recipe.schema.jsonlayout_entry.kind: concept | junction-note | crosswalk-edge (default concept). Backwards-compatible — existing recipes (no kind field) continue to validate and produce concept notes.
src/render/index.tsNew Tier1Kind type. render() walks the layout for non-default kinds and emits frontmatter.kind discriminator. Last non-default wins (recipe authors typically declare on the leaf entry).
src/generation/generation-engine.tsNew generateFromRecipe() entry point. Bypasses v0.1.0 column-role legacy logic. Calls render() per row → composes frontmatter + tags + aliases + provenance → validates pre-write against spec/tier1.schema.json → merges with existing user-edited frontmatter on replace mode → writes to vault.
src/main.tsplugin.runImportFromRecipe(parsedData, recipe, options) exposed alongside the existing runImport. E2E tests + future native-recipe commands use this handle.
Pre-write validationvalidateTier1Frontmatter is called on every row’s rendered frontmatter. With strictValidation: true (default), invalid frontmatter blocks the write and surfaces in result.errors[]. STRM predicate enforcement is the load-bearing case — bad predicate_id values are rejected with AJV’s structured error before any file I/O happens.
recipes/starter/nist-csf-to-800-53-crosswalk.jsonMaps NIST CSF subcategories → NIST 800-53 controls via STRM predicates + SSSOM-shaped envelope (match_type, mapping_justification, mapping_provider).
recipes/starter/iso27001-to-800-53-crosswalk.jsonMaps ISO 27001 controls → NIST 800-53 controls. Identical recipe shape to the CSF crosswalk — proves recipe genericity across framework pairs.
recipes/starter/evidence-junction-notes.jsonBulk-import evidence-link junction notes from a CSV. Generic over any concept ontology + any evidence path.
recipes/starter/README.mdInventory of starters + the multi-framework recipe-pattern story (how to author a new recipe for any framework pair by copying + swapping ontology slugs).
tools/fixtures/synthetic/csf-800-53-crosswalk.csvSynthetic 5-row crosswalk fixture for tests + dev.

Multi-framework support — the user’s question

Section titled “Multi-framework support — the user’s question”

A reviewer asked mid-implementation: “are we accounting for all of the existing frameworks?”

Yes — the v0.1.4 design is generic over framework pairs. The recipe shape doesn’t bake in any particular ontology. subject_id and object_id are CURIEs (spec/tier1.schema.json $defs/curie), and CURIE prefixes can be any registered ontology slug. The starter recipe library demonstrates this concretely: CSF→800-53 and ISO→800-53 have byte-identical layout shape; only the source ontology slug + tag path + folder name differ.

FrameworkConcept-note import (kind=concept, default)Crosswalk-edge import (kind=crosswalk-edge)Junction-note import (kind=junction-note)
NIST 800-53 r5✅ wizard or native recipe✅ via crosswalk recipe✅ via junction recipe (evidence on 800-53 controls)
NIST CSF 2.0✅ same✅ same shape, swap ontology slug✅ same
MITRE ATT&CK✅ same✅ (evidence on techniques)
CIS Controls v8✅ same
ISO 27001:2022✅ same✅ shipped as starter
CRI Profile / FFIEC / COBIT✅ same
Any future framework✅ same

The recipes/starter/README.md page documents the swap-ontology-slug-and-folder-path pattern explicitly so users can produce their own framework-pair recipe in minutes. v0.2’s wizard milestone will surface this as an authoring UX (no JSON editing required); v0.1.4’s surface is the recipe JSON itself.

SuiteCountDetail
Jest unit tests108 (was 85, +23)New: tests/render-kinds.test.ts (5 tests for kind dispatch); tests/validation-strm-enum.test.ts (18 tests for STRM enforcement + recipe-schema kind field)
WebDriver E2E33 across 7 spec files (was 28 across 6)New: tests/e2e/crosswalks.spec.ts (5 tests for crosswalk-edge + junction-note import via runImportFromRecipe, including STRM rejection + user_preserve survival)
Total141 passingAll green before commit.

The new crosswalks.spec.ts is the v0.1.4 success-criterion gate. It does:

  1. Imports 3 valid crosswalk-edge rows via runImportFromRecipe(parsedData, crosswalkRecipe, options) → verifies all three files appear at Crosswalks/v0-1-4-test/cw-{subject}-{object}.md
  2. Reads first crosswalk’s frontmatter via metadataCache → verifies kind: 'crosswalk-edge', predicate_id: 'is_equivalent_to', spec-conformant _crosswalker block
  3. Imports 1 row with predicate_id: 'totally_invalid_predicate' → verifies write was BLOCKED (result.success === false, result.errors contains a Tier 1 validation error mentioning predicate_id, no file created)
  4. Imports 2 junction-note rows via runImportFromRecipe → verifies kind: 'junction-note', predicate field is open-string (not enum-constrained per Tier 1 schema)
  5. Adds review_status + creator_id + notes to a crosswalk file via processFrontMatter → re-imports → verifies user keys survived, managed keys still recipe-driven
  1. kind defaults to concept for backwards compat. Existing v0.1.0 saved configs continue to flow through legacyConfigToRecipe() → render() with no kind set → produces concept notes exactly as before. Zero migration cost. Per Ch 22 §10.7 four-phase migration plan.

  2. Native-recipe path lives alongside legacy column-role path. runImport (legacy) and runImportFromRecipe (Ch 22 native) are both first-class entry points. The wizard UI authors v0.1.0 column-role configs (still works, still ships); native recipes are JSON files + the runImportFromRecipe API. v0.2 will refactor the wizard to author native recipes directly; until then, both paths are supported.

  3. Pre-write validation is the v0.1.4 STRM enforcement mechanism. The recipe schema’s predicate_id is just a string template (renders to whatever the source CSV has). The Tier 1 schema’s crosswalk_edge_frontmatter.predicate_id is the closed STRM enum. Calling validateTier1Frontmatter on every rendered row pre-write is what catches bad predicates. Strict mode (default true) blocks the write; non-strict mode logs and continues. This is also the v0.1.3 task §Validation hook that wasn’t fully completed — now wired in v0.1.4 where it matters most.

  4. Numeric-typed managed values are deferred. match_confidence is type: number in the spec but render() produces strings. Rather than adding type coercion in v0.1.4, the starter recipes mark match_confidence as user_preserve (so reviewers can edit it) and omit it from managed. A future milestone adds {template, type: "number"} shape to managed values.

  5. Junction-note predicate is open-string. Per Tier 1 schema, junction-note’s predicate field is type: string, minLength: 1 — no enum constraint. Recipe authors pick from their workflow’s vocabulary (covers, evidences, partially_covers, attests, reviews, etc.). STRM enforcement is for crosswalk-edge predicates only, where the set-theoretic semantics are load-bearing.

  6. Three starter recipes ship from day one. CSF→800-53, ISO→800-53, evidence-junctions. Demonstrating multi-framework genericity by example beats documenting it abstractly. The recipes/starter/README.md makes the “swap-the-slug” pattern explicit.

                Crosswalker import pipeline (v0.1.4 view)
                ════════════════════════════════════════

  ┌─ INPUT ─────────────────────────────────────────────────────────┐
  │                                                                 │
  │  Source CSV / XLSX / JSON                                       │
  │       │                                                         │
  │       ▼                                                         │
  │  Parser (PapaParse, etc.)                                       │
  │       │                                                         │
  │       ▼                                                         │
  │  ParsedData                                                     │
  │                                                                 │
  └─────┬───────────────────────────────────────────────────────────┘

        ├─── PATH A (legacy column-role; unchanged) ─────────────────────┐
        │                                                                │
        │   Wizard UI (v0.1.0 ImportRecipe)                              │
        │     → legacyConfigToRecipe()                                   │
        │     → generateNotes() per-row loop                             │
        │     → buildNoteDataViaRender() calls render() with             │
        │       kind defaulting to 'concept'                             │
        │     → produces concept-note frontmatter                        │
        │                                                                │
        ├─── PATH B (native Ch 22 recipe; NEW in v0.1.4) ──────────┐     │
        │                                                          │     │
        │   Recipe JSON (recipes/starter/*.json or user-authored)  │     │
        │     → AJV validateRecipe()                               │     │
        │     → generateFromRecipe() per-row loop                  │     │
        │     │                                                    │     │
        │     ▼                                                    │     │
        │   render(recipe, identity) ◄── v0.1.2 pure function      │     │
        │     │                                                    │     │
        │     ├─ layout walk (folder/file/heading mechanisms)      │     │
        │     ├─ also_emit (tags/aliases/managed frontmatter)      │     │
        │     ├─ kind dispatch ◄── NEW v0.1.4                      │     │
        │     │   if any layout entry has non-default kind:        │     │
        │     │     address.frontmatter.kind = chosen kind         │     │
        │     │                                                    │     │
        │     ▼                                                    │     │
        │   Address (path + frontmatter + tags + aliases)          │     │
        │     │                                                    │     │
        │     ▼                                                    │     │
        │   Compose frontmatter:                                   │     │
        │     • Address.frontmatter (curie + managed + kind)       │     │
        │     • + tags / aliases arrays                            │     │
        │     • + buildProvenance() ⇒ _crosswalker block           │     │
        │     │                                                    │     │
        │     ▼                                                    │     │
        │   ┌──────────────────────────────────────┐               │     │
        │   │  validateTier1Frontmatter() ◄── NEW  │               │     │
        │   │  PRE-WRITE GATE                      │               │     │
        │   │                                      │               │     │
        │   │  • concept-note shape: open-tail     │               │     │
        │   │  • junction-note shape: 13 fields    │               │     │
        │   │  • crosswalk-edge shape:             │               │     │
        │   │      STRM predicate_id enum ENFORCED │               │     │
        │   │      ↑ this is the v0.1.4 milestone  │               │     │
        │   │      success-criterion gate          │               │     │
        │   │                                      │               │     │
        │   │  if invalid AND strictValidation:    │               │     │
        │   │     row error → result.errors[]      │               │     │
        │   │     no write happens                 │               │     │
        │   └──────────────────────────────────────┘               │     │
        │     │                                                    │     │
        │     ▼  (only if valid)                                   │     │
        │   if existing file in 'replace' mode:                    │     │
        │     mergeFrontmatter(existing, new, managedKeys)         │     │
        │     ↑ user_preserve patterns from recipe.target          │     │
        │       .also_emit.frontmatter.user_preserve               │     │
        │     │                                                    │     │
        │     ▼                                                    │     │
        │   Path collision check (emittedPaths Set)                │     │
        │     │                                                    │     │
        │     ▼                                                    │     │
        │   app.vault.create() / app.vault.modify()                │     │
        │                                                          │     │
        └──────────┬───────────────────────────────────────────────┘     │
                   │                                                     │
                   ▼                                                     │
  ┌─ OUTPUT — Tier 1 vault ──────────────────────────────────────┐       │
  │                                                              │       │
  │  Concepts/  Junctions/  Crosswalks/                          │       │
  │  ────────  ──────────  ───────────                           │       │
  │  AC-2.md   jn-AC-2-MFA-Policy.md   cw-CSF.PR.AC-01-AC-2.md   │       │
  │  AU-1.md   jn-AU-1-Audit-Run.md    cw-ISO.A.9.2.1-AC-2.md    │       │
  │  ...       ...                     cw-MITRE.T1078-AC-2.md    │       │
  │                                                              │       │
  │  Same render() pipeline, same provenance, same merge.        │       │
  │  Three Tier 1 shapes from one unified architecture.          │       │
  │                                                              │◄──────┘
  └──────────────────────┬───────────────────────────────────────┘

                         ▼  (later milestones)
  ┌─ DOWNSTREAM ─────────────────────────────────────────────────┐
  │  v0.1.5 → sqlite-wasm projector indexes all 3 kinds          │
  │            crosswalk-edge becomes the FK-rich edge table     │
  │            junction-note becomes the evidence-coverage       │
  │            join table for Bases queries                      │
  │                                                              │
  │  v0.1.6 → Bases queries: "controls without evidence",        │
  │            "CSF subcategories without 800-53 mapping",       │
  │            "ISO 27001 controls equivalent-to AC-2"           │
  │                                                              │
  │  v0.1.7 → STRM TSV exporter walks crosswalk-edge files       │
  │            OSCAL JSON exporter rolls up junction-notes       │
  │                                                              │
  │  v0.1.8 → audit-trail captures full lifecycle of every       │
  │            crosswalk-edge (proposed → approved → deprecated) │
  └──────────────────────────────────────────────────────────────┘

Interfaces this milestone introduces / changes

Section titled “Interfaces this milestone introduces / changes”
InterfaceStatus
layout_entry.kind field in spec/recipe.schema.json✅ Added; default concept; 3 enum values
Tier1Kind type in src/render/index.ts✅ Exported
Kind dispatch logic in render()✅ Live; non-default kind → address.frontmatter.kind
RecipeImportOptions interface in generation-engine.ts✅ Exported
generateFromRecipe() function✅ Exported; new entry point for native Ch 22 recipes
plugin.runImportFromRecipe()✅ Live; primarily for E2E + future commands
Pre-write Tier 1 validation in generateFromRecipe()✅ Live; STRM predicate enforcement
recipes/starter/ directory + 3 recipes + README✅ Shipped
  • Wizard UI — still authors v0.1.0 column-role configs that flow through legacyConfigToRecipe(). Wizard refactor to author native Ch 22 recipes is v0.2.
  • Existing concept-note imports — backwards-compatible. No saved-config migration required. v0.1.0 → v0.1.4 is a non-breaking surface change.
  • Tier 1 schema — already had the discriminator on kind (concept-note via if-not-junction-or-crosswalk; junction-note + crosswalk-edge via discriminated branches). Unchanged in v0.1.4.
  • Validator wiringinitValidator() already compiles both schemas; v0.1.4 just calls validateTier1Frontmatter from a new place (the new generation path).
  • Body templates — body content is still a minimal # {title} line in native-recipe path. Body templating via also_emit.body or similar is a future milestone.
  • ✅ Always test thoroughly — milestone gate is the new crosswalks.spec.ts E2E that exercises real file I/O for both new kinds + STRM rejection
  • ✅ No personal data in commits/logs (sweep before push)
  • ✅ No AI co-author attribution in commits
  • ✅ Brevity preference — terse table-heavy delivery log; system-design diagram replaces narrative architecture description
  • ✅ General-ontology positioning — recipe + log read in any domain (CURIE-shaped CURIEs, predicate vocabulary, evidence-link primitives all generic)

What this unblocks for v0.1.5 (sqlite-wasm projector)

Section titled “What this unblocks for v0.1.5 (sqlite-wasm projector)”
  • All three Tier 1 shapes are now produced by the engine. The Tier 2 projector indexes:
    • concept-note → primary entity table (CURIE + attributes)
    • junction-note → evidence-coverage join table (subject_concept × evidence_doc + coverage)
    • crosswalk-edge → FK-rich edge table (subject_id ←→ object_id with STRM predicate as edge type)
  • Junction-note + crosswalk-edge frontmatter shapes are SQL-projection-friendly (flat, type-stable, predicate enums map cleanly to enum-typed columns).
  • The validation gate guarantees Tier 2 doesn’t have to defensively handle malformed crosswalk predicates — STRM enforcement upstream means Tier 2 can rely on the closed enum.

v0.1.5 — Tier 2 sqlite-wasm sidecar projector — projects all three Tier 1 shapes into a .crosswalker.sqlite sidecar with sqlite-vec for vector queries. Per-milestone E2E spec is tests/e2e/sidecar.spec.ts — must verify project-from-vault, recover-on-delete, and full FK integrity for crosswalk-edge mappings.