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

Part A of the per-import-folder work closes on a narrowed bar

Created Updated

Crosswalker imports compliance frameworks into an Obsidian vault as ordinary markdown notes and lets you link evidence to them.

A defect found on 2026-08-28 meant every import wrote into one shared folder instead of getting its own. Fixing that was easy. Making the fix safe for a vault that already holds imports was not: it took twenty-three attempts, each measured against a bar written down before the attempt started, and each of the first twenty-two failed it.

The twenty-third meets every check that was actually run. 2,961 unit tests pass and none fail. The build and the linter are clean. A full run of the walkthroughs inside real Obsidian shows 175 checks passing, nothing that passed before now failing, and nothing that used to be checked gone missing.

What it is not. Seven long-standing visual walkthroughs still fail and are still quarantined. Four large-corpus walkthroughs did not run at all. Nothing is merged and nothing is released. This is acceptance of one bounded piece of work against its own bar, not a statement that importing is safe for every input.


2. Two minutes: the concrete case, and what used to happen to it

Section titled “2. Two minutes: the concrete case, and what used to happen to it”

Say your vault holds a framework you imported last month, and beside its folder you keep your own note about it. Something ordinary:

---
title: Don't Panic
created: 2024-01-05
notes: |
  Reviewed with the audit team.
  Second pass scheduled.
# my own reminder
owner: security
---

My own prose about this framework.

Crosswalker maintains a contents list (an index of the notes in the folder this note sits beside) inside that note. To do it, it used to rebuild the whole note from its parsed properties. On a refresh that imported nothing new, that note came back as:

---
title: "Don't Panic"
created: "2024-01-05"
notes: Reviewed with the audit team. Second pass scheduled.
owner: security
---

My own prose about this framework.

Four things happened to a note nobody asked to change. The date gained quotes. The apostrophe forced quotes. The comment vanished. And the multi-line value was folded into one line, which is not a formatting change: it is a different value. On a file saved with Windows line endings, every line ending in the file changed too.

Now, the same refresh leaves that note byte-for-byte identical. The writer edits only the two blocks it owns and copies every other line through exactly as written, in the line endings the file already had.


3. Why this matters for a real organization

Section titled “3. Why this matters for a real organization”

An organization does not import a framework once. It imports NIST CSF, then MITRE ATT&CK, then a second release of the first one, and people write their own notes alongside all of them and link evidence to the controls.

Three properties have to hold for that to be usable, and all three are what this arc was about:

PropertyWhy an organization needs it
A refresh that changes nothing changes nothingIf a routine refresh silently edits notes people wrote, nobody trusts the tool with a vault that matters. Git history fills with noise and a real change is invisible in it
Notes are matched by what they are, not where they sitPeople move and rename notes. If identity is a file path, every reorganization looks like a deletion and a creation
When it cannot know, it refuses by nameA tool that guesses under uncertainty produces wrong answers confidently. One that names what it saw produces a question a person can answer

The third is the one worth dwelling on. Most of the twenty-three attempts failed because something was deciding a fact instead of reading one — inferring which collection an import belonged to from a folder name, inferring that a note it could not read was not its own, inferring a folder’s contents from the batch in front of it. Each fix removed one guess and revealed the next one underneath.


Beyond the note-preservation fix in section 2:

  • A damaged contents list is reported, not half-written. If the markers around the list are corrupted, the run says so and writes nothing to that note. Previously a corrupted marker was read the same as no list at all, so the properties half was rebuilt and the body half was left stale.
  • A home note you renamed by hand is named once, not reported missing. A framework’s home note whose recorded identity you had edited used to be refused and, on the same results screen, listed as gone from your source. The identity it carries is now claimed where the results are counted, so it is left alone and mentioned once.
  • A crosswalk import no longer silently drops edges. Several mappings that reduced to the same generated file name were written into one file, last writer winning, with nothing said. A mapping is identified by the pair of concepts it connects, not by the two file names it was built from.
  • A qualified identifier round-trips. When a second release is imported alongside the first, its identifiers are qualified (uniformly re-prefixed so the two releases do not collide). That transform is now recorded and reversible, so an export can be re-imported and mean what it meant. Previously a qualified set could not re-import its own export at all.
  • An evidence-link update preserves your review fields. Reviewer, review date, confidence, expiry, notes and scope survive an update byte-for-byte unless you changed them in the window yourself.

Delivery detail in user-facing language lives in CHANGELOG.md [Unreleased], which is the single canonical record of what is coming. It is not restated here.

The durable output is not the code. It is a short list of rules, each one bought with a failed attempt:

RuleThe failure that bought it
Record the fact, never infer itA destination guessed from folder shape
A path may seed an identity, never be needed againA note that moved became a duplicate
Absence is not a factAn unindexed vault read as an empty one
When you cannot know, refuse by nameA refusal that named no cause
A refusal names the population it looked atA list computed from a batch that could not see everything it named
A fact about a note is not a fact about its folderOne unreadable note condemning its whole folder
Provenance belongs to the run that wrote the noteA run that wrote nothing restamping what it did not touch
A mechanism extended to a new writer states what that writer’s subject is made ofA byte comparison designed for notes Crosswalker wrote, moved onto a note the user wrote
A fact a run carries is a fact a run claimsAn identity carried at the writer and absent from the list that accounts for it

The last two are this stretch’s contribution, and they are the same miss twice: a rule was extended to a new subject without asking what the new subject is made of — whose bytes, whose identity, whose list.


GateResult
Unit tests2,961 passing, 2 skipped, 0 failing
The frozen regression fileunmodified, its 299 tests passing
Build, lintboth clean
Obsidian walkthroughs, default set208 checks: 175 pass, 7 fail, 26 skipped
Versus the frozen baseline0 newly failing, 0 gone missing; 2 added, both passing
Versus the previous run0 newly failing, 0 gone missing, 0 added — all 208 unchanged
The seven failuresmatched by identity, line for line, against the known quarantine list
Refresh-that-changes-nothing probe4 index notes, 0 timestamps changed, 0 notes changed, 0 written, no orphans

An earlier unit run in this same stretch failed, on two checks covering Windows line endings. That log is kept rather than overwritten, because it is the evidence that prompted the corrections. The passing run is a separately named log. Both are retained.


ItemState
Seven quarantined visual walkthroughsStill failing. None fixed, none removed from the list. No removal condition was observed, so nothing was removed
Four large-corpus walkthroughs, three release-blockingDid not run. Unverified since 2026-08-27
Windows line endings, and corrupted contents markersUnit tests only. No walkthrough inside real Obsidian exercises either shape
One properties-append boundary on a Windows-line-ending fileUnresolved reachability, not proven safe. No route to it was found through the ownership checks that exist; no route was proven impossible either. Cosmetic if reached
A removed identity shortcutIts narrow cases have passing tests. The wider scenario it was reasoned out of is an argument from the invariant, not a reproduced failure
Replace-mode grouping-note observation, and a grouping note dragged to the vault rootDeferred to Part B, by decision, not closed
A readiness wait in the release-drift walkthroughKept as a guard and explicitly not credited with the recovery of the two checks that came back. Disabling it earlier did not change the result, so the mechanism is unestablished
  1. Merge timing for the branch. The evidence is now complete enough to prepare the funnel; the decision is not made.
  2. Whether the acceptance clause is amended to name the passing log directly, or left as written with the dated clarification carrying it.
  3. Where walkthrough quarantine evidence lives — it currently sits outside version control, so the rows cite artifacts a reader cannot open.
  4. Part B scope — the deferrals above, plus the identity-constructor and read-state work already specified.

One process note, recorded rather than smoothed over

Section titled “One process note, recorded rather than smoothed over”

Two consecutive attempts were decided by a step that produced nothing — one lost to a session limit, one to a server error — in both cases on work that was probably fine. A step that produced no output is not a step that passed. That is now written into the process: the gate a missing step owned has no evidence, and the next attempt owns it first.


7. Follow-up recorded 2026-09-05: preservation checked inside Obsidian

Section titled “7. Follow-up recorded 2026-09-05: preservation checked inside Obsidian”

The unit-only limitation in section 6 is now superseded for four specific cases. The kept-host preservation spec calls the production importer inside real Obsidian and reads the resulting files through its vault adapter. It does not click through the import wizard. No production code changed in this follow-up.

A kept host (an existing note beside the folder whose contents list it carries) was seeded with handwritten properties, comments, a multiline value, and CRLF (Windows line endings). On an unchanged refresh, the note stayed byte-for-byte identical and the run made zero recorded writes. When one source row was added, only the expected managed list content and the permitted timestamp field could differ; the comparison kept all other bytes exact, including line endings.

Two more cases supplied a duplicated end marker, with and without a children: property. Each reported the parser’s actual corruption verdict and left the host unchanged, including the old children list when present. These cases do not establish behavior for every malformed, nested, or unclosed marker sequence.

The targeted run passed all four declarations. The full default run then measured 212 declarations: 179 passing, 7 failing, 26 skipped, with exactly those four new passing declarations and all 208 prior results unchanged. Its exit code remains 1: the failing and skipped identities are unchanged, and no quarantine entry was cleared. Section 5 remains the original pass-23 measurement, not a table rewritten to conceal a later result.

The first targeted attempt was blocked by a fixture mistake: two differently located notes claimed the same identifier. The importer correctly refused the collision. The corrected fixtures use separate identities, not merely separate folders; all four cases were rerun, with their assertions unchanged. That failed attempt’s evidence is retained.

What this unblocks: the exercised preservation cases no longer depend solely on a mocked vault. It does not decide merge timing, clear the large-corpus or quarantine work, resolve the properties-append boundary, or change Part B’s scope. Delivery detail remains in the changelog.