First run was broken, and every gate was green
1. Thirty seconds
Section titled “1. Thirty seconds”Crosswalker imports compliance frameworks into an Obsidian vault as ordinary markdown notes and lets you link evidence to them. The goal driving the project is being usable for real work at an organization.
What happened today: the very first thing a new user does was broken, and had been for some time. Import a framework and the plugin would report, on the same screen and at the same moment, that it had created 12 notes and that nothing had been imported. Nothing was actually wrong with the import. The notes were correct and on disk. Everything that looked at them disagreed that they existed.
The lesson, which is bigger than the bug: it was found by a person opening an empty vault, not by any test. Every automated test and every screenshot taken that week started from a vault that already had content in it, so not one of them walked the only path a new user takes.
What you need to do: nothing. It is fixed, and the walkthrough is now a test. Section 7 lists what is still open.
2. Two minutes: what a new user actually saw
Section titled “2. Two minutes: what a new user actually saw”The default settings, the bundled NIST recipe, and an empty vault. That is the shortest possible path through the product, and it ended here:
| Surface | What it said |
|---|---|
| The import results screen | Created 12 notes |
| The workspace home screen | Nothing imported yet |
| The status bar | no ontologies yet |
| The coverage report command | Refused, with Import a framework first |
Four surfaces, one vault, two mutually exclusive answers. A person seeing this cannot tell whether the import worked, and the reasonable conclusion is that the product does not.
3. The cause: paths are guesses, not facts, for the third time
Section titled “3. The cause: paths are guesses, not facts, for the third time”Installed frameworks were counted from folder shape. Discovery walked the output location and treated each subfolder as one installed framework, skipping loose files. There was a comment in the code saying that only folders count.
The bundled NIST recipe writes a flat layout. It puts notes directly into the configured output folder and creates no subfolder at all. So the default recipe and the default detector disagreed about what an import looks like, and the default settings put them in precisely the arrangement where they disagree. Nobody had to do anything unusual to hit this.
The fix is to stop asking the filesystem and start asking the notes. A note counts when its own provenance says Crosswalker produced it, and it joins the group named by its CURIE prefix (the short identifier namespace, such as nist800-53, that every generated note already carries). Path never participates. Flat and nested layouts now produce identical summaries because layout is no longer an input, and hand-authored folders stay out because they carry no provenance, which is a stronger exclusion than the folder-name check it replaces.
There was a second, independent failure on the same screen. The coverage report read the fast query index without refreshing it, so it saw whatever the one-time background pass at vault open had seen, which for a framework imported minutes after the vault opened is nothing. It now refreshes before reading, and reports the failure rather than an empty result if that refresh fails. Two unrelated bugs producing one symptom is part of why this looked, from the outside, like one confusing product rather than two fixable defects.
4. Why a week of green suites never saw it
Section titled “4. Why a week of green suites never saw it”This is the part worth carrying forward.
The suite was green. The screenshot runs were green. The static gates were green. A whole week of real-Obsidian E2E work had just landed, including a deliberate replacement of an 11,198-note dev-vault copy with a small purpose-built seed vault. And every one of those runs started from a seeded vault: a vault that already contained generated notes, arranged the way the tests expected.
A seeded vault is a good default. It makes runs fast and deterministic, and it is the right starting point for the great majority of assertions. But it silently encodes an assumption, content already exists, and that assumption is false for exactly one user: the new one. The empty-vault path was not undertested. It was untested, by every suite, with no gap visible anywhere, because a gap in coverage looks identical to coverage that passes.
tests/e2e/first-run.spec.ts now walks it: an empty vault, a real 12-row NIST 800-53 slice imported through the visible wizard, and then assertions that the contradiction cannot come back. The workspace names the framework, the status bar counts one, and the coverage command opens a report.
| Blind spot | What it looked like | What closed it |
|---|---|---|
| No suite crossed the empty-vault path | Green everywhere | An E2E spec that starts from an empty vault and imports through the real UI |
| Two pages describing first-day behavior were written from memory | Plausible prose | Both now carry a Status last verified marker citing the empty-vault run, not a reading of the source |
The two pages are quick start and the command reference. The quick start walked a flow that ended at a screen saying nothing had been imported. The command reference listed names that no longer matched the palette. Both were verified against the product rather than against recollection, and the markers say so.
That question, asked of all 48 specs
Section titled “That question, asked of all 48 specs”Later the same day the rule above was turned on the suite itself, as a read-only audit. The question was not which tests fail but what does each spec assume about the vault it starts in, and what could break without any of them noticing. It found one assumption of exactly the shape that produced this morning’s bug, and fifteen smaller ones.
The matching one: the configured output folder is never written to, by any spec, at any point in the run. The purpose-built seed vault does not set the output path, so it keeps the shipped default Ontologies/. Every spec writes somewhere else — Frameworks/, Crosswalks/, Evidence/. Installed-framework discovery resolves only the configured output folder, so for the whole run it resolves an empty folder and renders the empty state. first-run.spec.ts is the only spec in the repository that names Ontologies/ at all, and it is the one spec excluded from the shared configuration. The surface repaired this morning is therefore dark for all 48 remaining specs: discovery, the status-bar count, the import-again affordance and orphan scanning can all break with the suite fully green. This is the same “a gap in coverage looks identical to coverage that passes” that the first-run bug rode in on, still present, one layer over.
The other findings worth naming, ranked, with what each one hides:
| Finding | What it conceals |
|---|---|
| No spec ever opens a Tier 2 index written at an older schema version | A migration that throws on upgrade. Every existing vault fails to open its index after a plugin update, suite green |
The SSSOM spec declares its own copy of the predicate map instead of importing the shipped one, and its fixture contains neither broadMatch nor narrowMatch | Re-inverting the exact mapping direction this project has already got wrong once, invisibly |
| Two sidecar tests assert the opposite of their titles: the “respects the configured path” test never changes the setting, and the “skips auto-projection when disabled” test only checks that manual projection still works | A hardcoded index path, and a startup scan that runs for someone who turned it off |
moved and orphans are asserted by zero specs | Duplicate-instead-of-move, or every note reported as an orphan. Given that path inference has now produced three defects, this is the likeliest live one |
re-import.spec.ts performs no re-import, and full-import-flow.spec.ts never asserts result.success or result.errors | An import that reports failure while still writing files, and the cache-lag frontmatter-wipe risk that the re-import spec’s name implies is covered |
| Six specs are gated behind environment variables no runner sets, including everything proving real-corpus fidelity and the loudness contract for empty filtered imports | A source predicate regressing to a silent empty import, with zero coverage in any default run |
| Six specs contain no assertion at all, and a seventh stops asserting 60% of the way through, emitting the rest as console findings | Roughly twenty green declarations carrying near-zero evidence. Two of them navigate to paths that exist only in a retired vault |
The four scale specs each clean Frameworks rather than their own destination | Nothing today, because they are gated off. The first person to enable them destroys the seed corpus and about fifteen other specs’ output, and the failures will read as product regressions |
Four themes are structural rather than per-spec, in the sense that fixing any single file does not touch them: one shared mutable vault with no per-spec reset and no cleanup afterwards, so whole-vault assertions are ordering-coupled by construction; the unpopulated output root above; the assertion-free specs; and the env-gated specs nobody runs. The rest are one-to-five-line edits.
The recommended first move was one fixture key, not a spec rewrite: set the seed vault’s output path to Frameworks, run the suite once, and read what turns red. It is reversible, it edits no test, and any spec that fails is by construction a spec that was passing only because the discovery surface was empty — which is the audit’s finding, delivered as a failing test rather than as prose. The sidecar-migration gap is the largest remaining hole and is new-spec work; the SSSOM map, the two mistitled sidecar tests, the missing success assertion and the four scale-spec cleanups are small edits that should ride along with whatever commit follows.
The one-key experiment, run
Section titled “The one-key experiment, run”That key was added the same day and the suite was run twice, before and after, on an idle host. The prediction above is now a measurement, and it was wrong in an interesting direction: the change turned almost nothing red, and what little it moved was worth more than the failure count suggests.
| Before | After | |
|---|---|---|
| Spec files | 41 passed / 6 failed | 41 passed / 6 failed — identical |
| Declarations | 167 passed / 8 failed / 26 skipped | 166 passed / 9 failed / 26 skipped |
At spec-file granularity the two runs are indistinguishable, and the six failures are exactly the six visual specs already quarantined. One declaration moved from passing to failing. Nothing moved the other way.
The value is not in the one new failure. It is in an assertion that kept its verdict and inverted its evidence. visual-workspace-flow.spec.ts reads the workspace’s installed-frameworks list after generating a framework and asserts the framework’s name appears in it. Before the key, that assertion failed against an empty string: the list element was never rendered at all, because discovery resolved the configured Ontologies/, found nothing, and took its empty-state branch. After the key it fails against NIST CSF 2.0 (CPRT export, nested)8 notes · 0 linksImport again — a real row, with a real note count and a working import-again button, matched against the identifier form rather than the display title the interface renders.
That is the whole point of the exercise, and it is invisible in a pass/fail count. The assertion was failing for the reason that a feature did not run; it now fails for the reason that a feature runs and the expectation is written against the wrong string. For the entire life of this suite, the installed-frameworks list and the status-bar count had never once produced a non-empty result under any spec. The surface repaired that morning went from untested to tested by changing one line of fixture configuration, and no test was edited to get there.
What it revealed in the product
Section titled “What it revealed in the product”The single new failure is not a fixture problem. It is a live defect, and it is the more valuable half of the result.
Every import defaults its destination to the bare shared output folder, so a per-import folder rule that exists, is unit-tested, and was an explicit owner call never runs. On 2026-07-11 the rule was set down that each import nests under its own folder inside the configured output path, because writing straight into the shared root flattened one import’s top-level folders into everyone else’s. The function implementing it was changed accordingly and is covered by passing unit tests. Its only caller in the wizard sits behind a guard that fires only when the destination is blank — and the wizard’s constructor has already filled it with the configured output path, which ships non-empty. The guard cannot fire. The rule has never executed for any user who did not manually erase the setting.
The experiment surfaced it because it is only observable once the output folder has something in it:
| With defaults, a user | What happens |
|---|---|
| Imports their first framework | Lands in the shared root rather than its own folder. Nothing looks wrong yet |
| Imports a second, unrelated framework | The destination now holds one owned collection, so the wizard quietly preselects refresh that collection. Importing MITRE would stamp it with the identity of the earlier NIST import and report every NIST control as missing from the source |
| Imports a third | Two collections is ambiguous, so the wizard now requires a choice and refuses to advance. The requirement is announced only by a transient notice when the button is pressed |
Nothing is deleted — orphans are reported, never removed — so this is misattributed ownership and a spurious change list rather than data loss. It is still the ownership machinery firing correctly on an ambiguity that should never have been created.
The blocked-wizard case is what the new test failure actually is. That is a defect reaching a person through the shortest path in the product, found by a one-line fixture change, in a surface that six weeks of green runs never entered.
A second instance of the same flattening is a question for the owner rather than a fix. The recognized-recipe fast path also returns the bare configured root, discarding the curated per-framework folder the registry carries. Its comment says that is deliberate. But it was written four hours before the per-import-root rule on the same day, so it reads as a decision the later call superseded and nobody went back to reconcile. Which of the two governs is an owner call, not an inference — the runtime evidence is that a user on defaults sees the recognized-source card say their import “lands in Frameworks”, the shared root, never the curated folder.
Two things the audit deliberately declined to call defects, because reading them as defects is how audits lose their credibility: the pure in-memory unit tests wearing an Obsidian costume are cheap, deterministic and honest about it, and the nine bodiless it() calls for an unlanded phase are the correct representation of work that has not happened.
The defect, fixed the next day (recorded 2026-08-29)
Section titled “The defect, fixed the next day (recorded 2026-08-29)”The guard was asking the wrong question, and that is the whole story. It wanted to know one thing: has the user chosen a destination? What it actually asked was: is the destination empty? Those are the same question only in a world where nothing else may write to that field, and something else did — the constructor, fifteen hundred lines earlier, filling it with the configured output path before the user had seen a screen. One line seeded a value; another line read that value as an answer to a question about intent.
The fix does not make the guard smarter. It records the thing the guard was trying to infer. A flag is set when, and only when, the user edits the destination. The derived per-import root applies when that flag is false. Emptiness is never consulted again.
Three rulings rode along, all of them about not making the same class of mistake one layer over:
| Ruling | Why |
|---|---|
| The configured output path stays the global root; the per-import folder nests inside it | A user who customised that setting keeps it as the parent. The per-import rule adds a level, it does not relocate anyone |
| A curated folder for a recognised source beats a derived one, and both nest inside the global root | This closes the reconciliation section 4 called an owner call. The curated folder is a better per-import root than a filename-derived one, so it wins, but it does not get to escape the configured root. Crosswalk and junction recipes are the exception and keep their homes outside it, because they are not framework output and moving them would relocate the mapping and evidence surfaces that read them |
| One accessor answers “where does this land”, and every write path, preview, tree, breadcrumb and ownership check reads it | The old code derived the value inside a render method and wrote it back into the field it was reading. Two surfaces could disagree, and the field being read was also the field being mutated |
Verified against a real run, not only against unit tests. An import that had always written into the shared Frameworks folder now writes into Frameworks/attack-mini. visual-workbench.spec.ts had never once completed a generation in the life of that spec, reporting NO_GENERATE because the ownership question blocked the step before the generate button exists, and it now returns created: 22, ok: true. That is the blocked-wizard case from the table above, disappearing because the ambiguity that produced it is no longer created. The full end-to-end suite ended at 167 passing, 26 skipped, and 8 failures that are exactly the pre-existing quarantined visual specs: one net gain, no losses.
What the fix deliberately does not do. It never moves anything that already exists. A refresh keeps the destination its notes already use, except that nothing can currently look that up: an import set records its id and its identity scheme and no destination at all. The only way to answer “where does this set live” today is to read the paths of its notes, which is the exact inference this project has already banned three times. So the invariant holds by construction rather than by lookup. The derived root is a pure function of the output path and the source file name, so re-importing the same source resolves to the same folder and finds its own collection there.
That construction is sound for a vault created after the fix and hazardous for one created before it, and an adversarial review confirmed the shape of the hazard:
| Confirmed | What happens |
|---|---|
| An existing flat vault is not recognised | The next import resolves to the new per-source folder, finds nothing, and mints a new collection. On the default skip it splits one collection across two folders; on replace it moves every note into the new one. Nothing is deleted either way, and typing the old path keeps it |
| Home and grouping notes are matched by location, not identity | So a changed destination copies them rather than moving them. Duplicate names then make existing links to that name ambiguous. This is the one place the fix can produce a genuinely new duplicate, and it is confined to the first import after the change |
| Two renderings of one framework still share a folder | The curated folders are per framework, not per source layout, so the hierarchical and flat NIST CSF exports both land in NIST CSF 2.0 and the second is offered as a refresh of the first. The original defect, intact, in the recognised-source path: narrowed from every import to a specific pair, but not gone |
| Switching source file after typing a destination keeps the destination | Which sends the second source into the folder belonging to the first: the same misattribution, reachable inside one session, in the file the fix lives in |
The first two are migration questions and the honest answer to both is the same: before generating, compare where this import will land against where the selected collection actually keeps its notes, and if they differ, ask. Not because asking is nice, but because “the destination moved” is currently indistinguishable from an ordinary run, and every one of these failures is a consequence of that indistinguishability. The third and fourth are small edits.
The unblock attempt: written, tested, and still blocked (2026-08-29)
Section titled “The unblock attempt: written, tested, and still blocked (2026-08-29)”The migration hazard above was taken as the next piece of work, and given a falsifiable bar rather than a checklist. A vault holding a pre-fix flat import, refreshed after the fix, must show: zero duplicate identifiers, zero unsolicited moves of the user’s existing notes, zero new orphans, no ambiguity error, and a results screen naming folders that actually exist — under both the skip and replace settings. Anything less and the fix stays blocked.
Seven decisions were frozen before implementation started, so that implementation could not quietly redesign the thing it was fixing:
| # | Frozen decision |
|---|---|
| 1 | A collection’s destination is recorded when it is minted, and recovered for older collections as the folder prefix all its notes share, compared whole segment by whole segment. A string prefix would merge NIST-mini with NIST-minimal. An empty result or the vault root means refuse, not guess |
| 2 | Destination precedence, highest first: what the user typed, then the collection being refreshed, then the curated folder, then the derived one. Never adopt a root that would orphan or duplicate notes an existing collection already owns |
| 3 | Break the circularity — you cannot ask where a collection lives without first naming a destination — by identity, the way this codebase resolves everything else. Before defaulting the destination, look for a collection that already owns the identifiers this source will produce. Exactly one match adopts its root; several defer to the existing ambiguity path; none derives a new root |
| 4 | Home and grouping notes must not encode their folder in their identifier |
| 5 | Which changes those identifiers, so the old form must be recognised as an alias and reconciled, never orphaned. Whether that is even possible turns on a fact the implementation had to settle first: can such a note hold writing a person put there? |
| 6 | Home and grouping notes reconcile through the identity index, like every other note |
| 7 | A not-yet-indexed file means not indexed yet, never absent — the same raw-read fallback the ownership scan had already grown for exactly this reason |
All seven were implemented. Decision 5 resolved to the harder branch: those notes do carry user prose and user-preserved keys, through a merge path with no conflict return, so regenerating them at the new address would destroy writing. Aliasing was the only available answer.
The evidence behind them is not thin. The unit suite is green at 2,143 tests, including a new spec that builds a genuinely pre-fix vault by generating with the current engine and then de-migrating it, so it cannot pass by seeding a shape the engine would never produce. Every one of those tests was then falsified: each frozen decision was reverted in turn and the tests claiming to cover it were re-run, and no experiment came back empty. The end-to-end suite is unchanged against a corrected baseline, with no declaration moving in either direction, and it confirms the recorded destination landing in a real vault rather than only in a mock.
And the criterion is still not met.
Under replace it passes. Under skip it fails, deterministically. Under that setting every row returns before the enrichment records are collected, so the enrichment phase never runs at all: no home or grouping note is reconciled, none is restamped, and every one the collection owns is reported as gone. That was pinned as independent of this work, since the same phantom orphans appear on a vault the current engine generated moments earlier, with the setting as the only variable. It is marked in the suite as a known failure that will turn the suite red if it is ever fixed and the marker is not removed, rather than deleted or softened.
An adversarial pass then found more, seven of nine confirmed by trace:
| Found | Why it blocks |
|---|---|
| Home and grouping notes move under skip | The enrichment phase is never told which setting is in force. Its options carry three fields and that is not one of them. Ordinary note moves are explicitly gated on it; these are not. A person selects the setting whose entire contract is leave my notes alone, and their grouping notes are renamed |
| The results screen names folders that do not exist | Pre-existing and live today, independent of all of this. When a note is found by identity somewhere else and the move is suppressed, the skipped list is filled with the address the note would have had rather than the one it has. It does not arise in the tested scenario only because decision 3 holds the destination still. It arises everywhere decision 3 declines |
| Nothing reports moves or orphans to a person | Both are recorded in the result and read by no code anywhere in the plugin. Two of the five clauses in the bar are, today, unobservable through the interface: a test can check them and a user cannot. “Verified in a real vault” is not currently something a person is able to do |
| The alias covers a changed rule, not a changed folder | The old identifier is reconstructed from this run’s folder, so it matches only while the destination stands still. The whole criterion therefore rests on decision 3 never declining, and it declines on a streamed source, a source above the probe limit, a cold cache, several claiming collections, or notes with no shared root |
| Removing the folder from the identifier created a new collision | Two imports of one framework into two folders now claim the same grouping identifiers. The second silently takes the first’s notes; the next refresh of the first takes them back. Where both already exist it reproduces the original ambiguity error from an entirely new cause |
| Recovery guesses where it was told to refuse | The deepest folder every note shares is the import root only when something is actually written at that root. With a single-branch hierarchy, or a layout that opens with a fixed folder, it returns a folder several levels too deep, and every note in the collection is then relocated to match. The refusal rule covers the empty case and the vault root; it does not cover this one |
| The cache fallback landed on one read path and the new lookup uses the other | So on a cold cache the recovered root is computed from a subset of the collection, and a subset shares a deeper prefix than the whole. The over-deep case again, arriving by a second route, with nothing left to make it decline |
The status is therefore: written, tested, honestly measured, and not shippable.
Two of these are small and deterministic: tell the enrichment phase which setting is in force, and report the address a skipped note actually has. The rest turn on one unresolved question, which is the next decision rather than the next patch.
May a grouping note’s identity be independent of the folder it sits in?
Taking the folder out of the identifier is what makes the identifier survive a move. It is also what makes two folders collide. Half of that question was answered and half was not, and answering only half is what produced the last four rows of the table.
The answer proposed in the paragraph immediately above this subsection — before generating, compare where this import will land against where the selected collection actually keeps its notes, and ask — is still the answer. This attempt tried to turn that question into a lookup. The lookup is correct for a vault created after the fix, and still a guess for the vault that needs it.
The second attempt: the right rule, and four clauses it still does not clear (2026-08-30)
Section titled “The second attempt: the right rule, and four clauses it still does not clear (2026-08-30)”The attempt above was blocked on a question it could not answer. This one reframed the question instead, and the reframing is the durable result of the day, even though the code that came with it is still not shippable.
The reframing
Section titled “The reframing”Both blocked passes were solving relocation. Each asked the same thing: the destination changed, so find where these notes used to live and either move them or teach the new address to answer to the old one. Answering that requires knowing the old folder, which is the very fact the recovery was trying to establish. So every mitigation ended up resting on a guess that was allowed to decline, and each decline reopened one of the clauses. That is why the previous table has seven rows and four of them are the same shape.
The rule that deletes the question: a refresh never changes where a collection lives. The destination for a refresh is the collection’s existing folder, read once from the notes it already owns and then recorded so it is never worked out again. A brand-new collection gets its own derived folder, and a brand-new collection owns no existing notes, so a refresh has nothing to move and nothing to leave behind. Under this rule the relocation problem is not solved. It is absent.
This is the page’s own rule applied one level up. Identity, never path has been written down five times here and on its neighbours, and every time it was about a note. A collection’s folder is the same kind of fact: not something to infer from where its notes happen to sit, but a property of the collection, which is already identified by a recorded identifier. Once the folder is a recorded property, there is nothing left to infer, and nothing left for a heuristic to decline.
Moving a framework to a different folder does not disappear; it becomes something a person asks for, with its own confirmation and its own summary of what moved. That is also the honest migration path for a vault whose imports all sit in one shared folder: not a rule that quietly rearranges notes on the next refresh, but an operation the owner of the vault chooses and watches. It is purely additive — nothing in this attempt has to change to add it, and it belongs on the v0.1 milestone track as its own piece of work — and it is the reason building the “what moved” reporting before anything can move was worth doing.
What was built
Section titled “What was built”| Rule | What it does |
|---|---|
| The destination of a refresh is the collection’s own folder | Shown read-only, as Refreshing into <folder>. Editing it during a refresh would be a relocation request, so the field is not offered |
| Ownership is decided from the whole vault, by what the notes are stamped with | Replaces the old rule, which looked only inside the destination folder and preselected refresh for whichever single collection happened to be sitting there, whatever it was. That rule was the misattribution bug |
| A collection with no recorded folder recovers one, whole folder name by whole folder name | So NIST-mini can never merge with NIST-minimal. If the notes are spread across unrelated folders, it refuses and says so, rather than picking one |
| Nothing decides ownership while the vault is still being indexed | A not-yet-indexed file means not indexed yet, never absent. On a cold cache the wizard waits, then blocks with a message, and never generates |
| Grouping notes reconcile by identity, and the old form of their identifier is still recognised | Unchanged from the previous attempt, and now sound rather than conditional: the folder cannot change, so the old identifier is always reconstructed against the folder it was written under |
| A suppressed move reports the folder the note is in, never the one it would have gone to | And the enrichment phase is finally told which overwrite setting is in force, so grouping notes stop moving under skip. These are the two small deterministic findings from the previous attempt, both closed |
| Moves and orphans are shown to a person | Both were recorded by the engine and read by no code anywhere in the plugin. Two clauses of the bar were unobservable through the interface; a count line each now renders, expandable to the list |
The evidence is not thin. The unit suite is green at 2,174 tests across 117 suites, including a rewritten specification for the removed guesswork and a new one pinning the whole-vault ownership scan. Fourteen mutations were run: each rule was neutralised in turn and the tests claiming to cover it re-run, and every one isolated. The end-to-end suite shows no declaration moving in either direction — the eight residual failures are byte-identical to the baseline and all already quarantined — and a screenshot from that run shows the new behaviour live, with two collections found elsewhere in the vault and the import correctly defaulting to a new one.
And the criterion is still not met
Section titled “And the criterion is still not met”Four clauses fail, independently. Two are carried forward and two are new.
| Found | Why it blocks |
|---|---|
| The ordinary wizard labels every import with the same placeholder | Closed 2026-08-30. The class it belongs to is not. The ordinary path now labels an import with the name of the file it came from, and matching ignores the placeholder labels. A second unrelated framework through that path now defaults to a new collection, falsified against the previous behaviour. The three rows below are the same defect through labels that are not placeholders |
| Under skip, every grouping note the collection owns is still reported as gone | Unchanged from the previous attempt and pinned as a known failure that will turn the suite red if it is ever fixed and the marker is not removed. Every existing row returns before the enrichment records are collected, so the enrichment phase never runs, no grouping note is restamped, and each one falls out of the “still present” set. Independent of the destination rule: it reproduces on a vault the current engine generated moments earlier, with the setting as the only variable |
| The summary screen is drawn only on the runs that do not need it | The “Moved: 0 notes” line was deliberately made unconditional, because a person who cannot see the zero cannot tell nothing moved from nobody checked. But the screen carrying it renders only when something moved or something orphaned. A clean refresh does neither, so the window simply closes. The orphan count is gated the same way one level down, so a zero is invisible even when the screen does appear. The clause asks for exactly the numbers the interface withholds |
| A genuine ambiguity error ends the run on the success path | Closed 2026-08-30. Any recorded error now forces the summary screen open with the collision named on it, and the finish message points at it. The rule is that a run which closes must show its errors. It is not yet applied to the mapping-import surface, or to a run whose only anomaly is a conflict; both are separate rows below |
Three smaller findings sit behind those, and are recorded rather than fixed:
- The indexing precondition can report success without having run. If a check is already in flight, a forced call returns passed whenever nothing has been refused yet — which is exactly the state during the bounded wait it exists to enforce. And it guards one door of four: the mapping-import surface and the bundled-fixture command never run it, and the mapping surface still uses the old folder-scoped ownership rule this change replaced.
- Recovery still returns the deepest folder every note shares, which is the collection’s folder only if something is actually written at that folder. The classic path emits no grouping notes at all, so a collection whose notes all sit one level deeper recovers a folder one level too deep, and under replace every note is relocated to match. Carried from the previous attempt; the refusal rule covers the empty case and the vault root, and still not this one.
- On the default path, neither the destination nor the ownership review appears until the final screen. The write itself is correct — the final screen re-checks before generating — but the two clauses about what a person is shown before deciding are not met where most people will look.
The status is therefore: reframed correctly, written, tested, honestly measured, and not shippable. The reframing stands on its own and should survive whatever the code does next. The first row of the table above is the thing to fix first, because it is the original defect wearing the new fix’s clothes.
The third attempt: four clauses closed, six more of the same shape found (2026-08-30)
Section titled “The third attempt: four clauses closed, six more of the same shape found (2026-08-30)”The second attempt named four failing clauses. This attempt fixed all four, and an adversarial pass over the result found six more failures of the same bar. The four fixes are correct and should survive. The bar is still not met, because the class of defect they were aimed at is wider than the four instances that had been found.
The three rules the fixes generalize into
Section titled “The three rules the fixes generalize into”The four amendments were written as separate items. They are one rule each, and each is worth more than the fix it produced.
| Rule | What it replaces |
|---|---|
| A refresh never changes where a collection lives | Guessing the old folder in order to move notes into a new one. Carried unchanged from the second attempt, and still the load-bearing idea |
| A placeholder is never an identity | Treating a filled-in default as a fact about the thing. The ordinary wizard stamped every import with the same two placeholder labels, and the matching rule then paired every import with every other |
| A run that closes must show its errors | Reporting success and closing the window over a recorded failure. The same family as the reset that reset nothing, one screen up |
What was built, and how it was measured
Section titled “What was built, and how it was measured”| Fix | What a person sees |
|---|---|
| The ordinary wizard labels an import with the name of the file it came from, and the matching rule ignores the placeholder labels entirely | A second, unrelated framework imported through the ordinary wizard now defaults to a new collection instead of being offered as a refresh of the first |
| A row that is skipped is still a row the run vouches for | On the default skip setting, refreshing an unchanged collection reports no missing notes. Previously every grouping note it owned was reported gone |
| The finish message states what moved and what is missing on every run | Created 6 notes in 1.2s. Nothing moved. No orphans. A person who cannot see the zero cannot tell nothing moved from nobody checked |
| Any recorded error forces the summary screen | A run that hits a genuine two-notes-one-identifier collision now draws the screen with the collision named on it, instead of closing |
The evidence is again not thin. 2,200 unit tests green, 26 of them new and written to the four rules; eleven mutations run, each neutralising one rule in turn, and every one turned its own tests red while the baseline stayed green. The end-to-end suite is identical to the previous run, declaration for declaration: 167 passing, 26 skipped, and the same 8 quarantined visual failures, with the raw comparison diffing to empty.
That last number is also the honest caveat. It proves the four fixes broke nothing. It proves nothing about whether they work, because no end-to-end test exercises them and the clauses they target have no coverage in that suite at all.
And the criterion is still not met
Section titled “And the criterion is still not met”Six findings, each independently defeating a named clause. Four of them are the same defect the whole change exists to end, arriving through a door the fix did not cover.
| Found | Why it blocks |
|---|---|
| Several bundled imports share one label | Closed 2026-08-30 on the ordinary wizard, by removing the match rather than fixing the labels. They still share labels. A shared label can now only draw a suggestion, and only when exactly one collection carries it, so two unrelated mapping imports land as two collections with nobody clicking anything. The guess it defeated survives on the mapping-import window, which is where the general-purpose mapping recipe actually runs — see the row below |
| A saved configuration reused across two frameworks does the same | The identifier of an applied saved configuration is stamped as the import’s recipe identity. It is not a placeholder, so the new exclusion never fires. The ownership rules already say in as many words that a recipe identifier names reusable instructions and not ownership, and the matching rule promotes it to ownership anyway |
| Two source files with the same name still collide | Closed 2026-08-30 for ownership, open for foldering. Two exports both called controls.csv no longer resolve to one collection, because the name no longer decides anything. The destination folder is still derived from the file name, so the two still land in one folder unless a destination is typed. That half is the row on renaming a source, below |
| There is now no way to refresh a collection imported before this change | Making old imports never auto-match is the safe half. But every refresh control in the interface is drawn from the matches, and a collection that matches nothing produces none: no dropdown, no “refresh that one instead” button. The only offer is a new collection, which silently writes a second copy of the framework. The amendment’s own text says the user can still choose refresh explicitly. There is no such control |
| Explicitly refreshing such a collection reports every note in it as missing | Closed 2026-08-30 by the pinned label. The refresh mints under the label the notes already carry, so the owned set and the produced set are the same set again, and nothing is restamped |
| ”No orphans” is still printed when nothing was checked | The count that was made unconditional is read from a field the engine leaves unset in two different situations: checked, found none and never checked. If the step that finds missing notes fails, the failure goes to a warnings channel the wizard displays nowhere, and the finish message says “No orphans.” anyway. That is precisely the distinction the amendment was written to make visible |
Two smaller findings sit behind those:
- A “notes left unchanged” run still closes over its own screen. That warning tells you to see the summary screen, and the summary screen is not drawn for it, although the screen has a section built for exactly that list. Errors were added to the condition that opens it; conflicts were not.
- The mapping-import window prints
[object Object]where the error should be, and the branch meant to report a partial success on that path is unreachable. It is the only other import surface, and it swallows the same collision the wizard now shows.
The one thing that closes most of it
Section titled “The one thing that closes most of it”Five of the six reduce to a single omission: a collection’s label is not pinned. When a collection is first created, its identifier scheme is fixed and a later refresh may not change it. Its label is not treated the same way, so it is re-derived from the source on every run and can quietly differ from the one on disk.
Pinning it at creation would make the misreported-refresh finding structurally impossible, make refreshing a pre-existing collection legitimate and therefore offerable, and turn the three auto-match findings from the wrong collection is silently chosen into nothing matched, so make a new one — which is the safe direction. That is an owner’s call about the data model, not an implementer’s, and it is the question to answer before a fourth attempt.
The status is therefore: three good rules, four correct fixes, and a bar that is still not met. The reframing from the second attempt and the two new rules from this one are the durable output. The branch is not shippable, and the next move is the owner’s decision on pinning a collection’s label, not another implementation pass.
The fourth attempt: the guess deleted, and three doors it was never behind (2026-08-30)
Section titled “The fourth attempt: the guess deleted, and three doors it was never behind (2026-08-30)”The third attempt ended by naming one owner’s decision: pin a collection’s label when the collection is created. That decision was taken, and with it went the thing underneath all four attempts. The rule that a source can be matched to the collection it belongs to was not sharpened again. It was removed, and matching demoted from a decision to a suggestion.
That is the right move, it is implemented, and the acceptance bar is still not met — this time for a reason none of the previous three passes had reached. The guess was deleted from the ordinary wizard. It was never only there.
What was built
Section titled “What was built”| Fix | What a person sees |
|---|---|
| Nothing preselects a refresh, anywhere in the ordinary wizard | The ownership step now lists every collection in the vault with its folder, how many notes it holds, and what produced it. The first and default choice is import as a new collection. A refresh happens only because someone picked one from that list |
| Matching became a suggestion | When exactly one collection carries the same label as the source, one line appears: “Looks like nist-csf-2. Refresh it instead?” One button. No match, or several, and there is no line. It suggests; it never decides |
| A collection’s label is pinned when the collection is created | A refresh mints identifiers under the collection’s own label rather than re-deriving one from this run’s source. A collection created before this change is pinned on its first refresh to whatever its notes already carry, because the notes are the fact |
| An orphan count that was never computed says so | Nothing moved. Orphans not checked. See results. The summary screen prints not checked where the number would be, and opens for it. Previously this case printed “No orphans.” |
| The mapping-import window shows its errors | It printed [object Object]. It now names each error, keeps the window open on a run that failed, and lists them on a screen |
Two of the third attempt’s blocking rows close as a consequence rather than by a separate fix, which is the sign the rule was the right one. A collection imported before this change is now listed, so it can be refreshed — the control the third attempt removed and assumed was still there. And refreshing it no longer reports every note in it as missing, because the label it is refreshed under is its own.
How it was measured, and where the evidence stops
Section titled “How it was measured, and where the evidence stops”The ownership tests went from 82 to 113, four more cover the mapping window, and eighteen mutations were run against them: each one restores a specific guess or drops a specific rule, and seventeen turned their own tests red. Three of those kills are the escape routes the third attempt found, now asserted directly: two crosswalks sharing the general-purpose label, one saved configuration applied to two frameworks, and two exports both called controls.csv. Each lands as its own new collection, with nobody clicking anything.
The evidence stops harder than in any previous pass, and both gaps are the honest caveat.
- The full unit suite has not been re-run green. It was run once, at 2,233 passing with two failures, both of which were assertions in other files pinning the exact shape of the stamped block, which now carries the pinned label. They were widened and re-checked on their own; the whole suite has not been run again since.
- There is no end-to-end run at all. Not a failed one: none. The host was carrying a foreground workload from unrelated software, with memory pressure three times the threshold and 7 GB already swapped, and starting 47 headed sessions on top of that is the exact arrangement that locked the machine on 2026-08-21. The agent holding that decision refused the run and said so, which is the correct answer. A static pass in its place established something worth keeping: no end-to-end test touches any of the four changes. The one specification that looked like a candidate calls the import directly and never opens the review at all.
And the criterion is still not met
Section titled “And the criterion is still not met”The bar’s last clause is a second unrelated framework, any path, with no click, lands in its own folder as a new collection, because nothing else is possible. Four ways it is still possible.
| Found | Why it blocks |
|---|---|
| The mapping-import window still guesses, and its default is to overwrite | It is the surface the general-purpose crosswalk recipe uses for every pair of frameworks. If one collection already sits in the folder for that pair, it is adopted silently: no control on the screen, no click, and that window writes in replace mode, so the earlier crosswalk is rewritten in place and the rows it no longer covers become missing notes nothing there reports. This is the deleted code, alive one window over. The folder for a pair of frameworks is deterministic; who owns what is inside it is not, and that distinction is the entire argument |
| ”Not looked yet” is read as “nothing there” | The wizard’s own defence is a list that reads the same when the vault holds no collections and when the scan has not finished. Two ways into the later steps do not wait for the scan — a recognised source, and resuming a saved draft, which can resume directly onto the button that starts the import — and that button does not check either. In that window the ownership step draws no control at all, so there is nothing to click, and the import falls through to the older rule underneath, which adopts whichever collection happens to share the folder. It is timing-gated; the missing check is not. This is cache lag is not absence, reproduced inside the code written to stop inferring |
| The bundled-example command has no ownership control at all | It imports a sample mapping with no way to say where it belongs, so it adopts whatever is already there. It also ends by reporting how many errors occurred and none of them, which is the one rule this pass applied everywhere else. Developer tooling, and the bar says any path |
| Two collections can claim one grouping note, and the write crosses the boundary | The general-purpose crosswalk recipe creates one top-level grouping note per label, and the index deciding where a note goes is vault-wide rather than scoped to the collection doing the writing. So a second crosswalk under the shared label does everything right — it lands as its own new collection, with no click, exactly as the bar requires — and still renames the first crosswalk’s top note into its own folder, leaving the first without one and reporting the move on its own screen. Pinned as a deliberately failing test rather than deleted, so the gap stays visible while the suite stays green |
One smaller finding sits behind them: the mapping window still has one exit, on an unreadable source file, that reports through the same truncating notice the rest of that window stopped using.
The status is therefore: the right rule, implemented, unverified, and three surfaces short. Pinning the label was the correct owner call and it closed two blocking rows on its own. What remains splits cleanly in two, and only the second is a decision. Applying the same ownership rule to the mapping window and the bundled command is mechanical and already specified by what the wizard now does. Scoping the index that places a note to the collection being written — or refusing outright a move whose source belongs to another collection — is a data-model call and the question to answer before a fifth attempt. Neither is worth starting before the branch has been through an end-to-end run, because nothing in this pass has had one.
The fifth attempt: the guess found at last, and the write it never governed (2026-08-30)
Section titled “The fifth attempt: the guess found at last, and the write it never governed (2026-08-30)”The fourth attempt ended by naming the next two pieces of work. Apply the ownership rule on every surface that writes, rather than on the screen where the defect was reported. And decide whether the index that places a note should be scoped to the collection doing the writing. The first was done. The second was left, and it is now the whole of what blocks.
Before either, an adversarial pass answered a question four attempts had not asked: where was the guess actually written?
Underneath all four passes, one line of code
Section titled “Underneath all four passes, one line of code”Every preselect removed in passes one through four was a copy. The original sat one layer below all of them, in the single function that answers which collection does this import belong to. Given no explicit answer it looked inside the destination folder, and if exactly one collection was already there, it silently adopted it.
So four passes each deleted a screen that had been asking the user the question, and each time handed the unanswered question down to the thing that had been guessing all along. The wizard’s own defence made this concrete: when its scan of the vault had not finished, it treated “we have not looked” as “there is nothing here”, and passed no answer to the layer below, which is precisely the adoption path.
That function now has exactly two behaviours. Name a collection and it refreshes that one. Anything else creates a new one. The branch that looked in the folder is deleted, and so is the “two collections here, I cannot tell” error that went with it, because the question is no longer the engine’s to ask. A folder is an address, and an address does not name an owner: two teams’ mappings share one folder by design, a legacy vault has everything in one folder, and a folder dragged in from elsewhere is not a claim about anything.
What was built
Section titled “What was built”| Fix | What a person sees |
|---|---|
| The engine never adopts | A collection is refreshed because someone named it, on a screen, by clicking. With no click, the import is a new collection, whatever is already in the folder. There is no longer a code path that decides otherwise |
| ”We have not looked yet” is no longer read as “there is nothing there” | The scan is a genuine three-state answer now, and all four ways into the later steps wait for it. If a decision is reached before the scan finishes, the import refuses with the indexing message rather than proceeding on an empty list. Previously the ownership step drew no control at all in that window, so there was nothing to click |
| The mapping-import window follows the same rule as the wizard | It lists every collection at that pair’s folder with its folder and note count, opens on keep this release as a new collection, and reaches a refresh only through the button that names one. It no longer overwrites by default: a new collection is written in skip mode, and only a refresh you chose replaces anything. A second release of the same mapping is also qualified so it cannot collide with the first on a shared identifier |
| The bundled-example command names its own choice | It asks for a new collection and for skip, so loading a sample mapping can no longer replace a real one you imported for the same pair of frameworks |
| The schema page describes the collection block again | It was one line behind: the pinned label added by the fourth attempt was in the machine-readable contract and not in the page that documents it. All four properties are now named, with which are required, why the label is pinned, and the statement that neither optional property is an ownership decision |
Three of the four blocking rows from the fourth attempt close here, and two of them close as a consequence rather than by their own fix, which is again the sign that the rule was the right one.
How it was measured, including the end-to-end run that had never happened
Section titled “How it was measured, including the end-to-end run that had never happened”The unit suite is green as a whole for the first time in three passes: 2,286 passing, two skipped, nothing failing. Eighteen mutations were run against the new tests — each one restores a specific deleted guess or drops a specific new rule — and every one of them turned its own tests red. The sharpest is the first: restoring the adopted-from-the-folder branch fails, among others, an assertion that a second framework written into an occupied folder “reports none of the first framework’s notes as missing”. That is the damage stated as a test, and it is the damage the branch exists to prevent.
Fifteen existing tests turned out to have been passing for the wrong reason. They were refreshes, and they had never said so, because the engine was answering for them. None was retired: a refresh is still exactly what each one is about, and each now names the collection it refreshes, the way a person does after the ownership step.
The end-to-end run happened, and it is the first since the ownership rule changed. Forty-seven specifications, and no regression. Eight declarations failed that had passed at the baseline, and every one of them is a screenshot that timed out; not one assertion failed anywhere in the run. All five specifications were re-run on their own against the identical build and all five are green at their baseline durations, which is what a harness stall looks like and is not what a behaviour change looks like. The specifications that had depended on the engine answering for them all pass.
One signature moved and it is recorded rather than dismissed: inside a specification that was already failing and already quarantined for a stale selector, a failure that used to read created nothing now reads cannot get past the second step, on exactly the surface the waiting-for-the-scan change touched. It is not a new failure and the diagnosis on that specification is unrelated, but it is the only behavioural signature in the whole suite that moved, and it belongs to whoever repairs that specification.
And the criterion is still not met
Section titled “And the criterion is still not met”The bar’s written clauses now all hold. A second, unrelated framework with nobody clicking anything lands in its own folder as a new collection, because two unrelated frameworks share no identifiers and nothing can pull them together. The bar’s closing sentence does not hold, and it is the one that matters: nothing below the click can choose a collection. That is true of the collection. It is false of the notes.
| Found | Why it blocks |
|---|---|
| A new collection that owns nothing can still take another collection’s notes | Ownership is answered correctly and then ignored one layer down. The index deciding where each note goes is vault-wide and has no notion of collections, so a run that owns nothing still finds every note whose identifier it produces, wherever it lives and whoever it belongs to, and treats it as its own. On replace it renames the other collection’s notes into its folder, rewrites them, and stamps them as its own; on skip it leaves them and writes only the genuinely new rows, splitting one framework across two folders and two collections. Missing notes are counted only against what the run itself owns, and it owned nothing, so both cases report no missing notes. This is the row the fourth attempt named as the next decision, and it is unchanged |
| The guard against this walks you into it | Import the same framework twice with no click. The ownership step correctly says the first import stays separate. A separate check then notices the folder is occupied and tells you to choose another folder for this import. Do exactly that, and every note of the first import is taken by the second, by the row above. There is no second click and no warning, and the screen is still saying the first import stays separate while it happens. A variant needs no typing at all: change your output folder setting between two imports of the same framework and the occupancy check never fires |
| ”Stop on conflict” moves your notes and then refuses to write | The setting whose entire meaning is touch nothing when something is already there is the one that relocates most. The move happens before the refusal is raised, so a run in that mode renames every note it would have conflicted with into its own folder, then reports each one as a conflict and writes nothing. Three lines test “is this not skip” where they should test “is this replace” |
| The bundled-example command still reports a count of errors and none of them | It now names its own collection, so it can no longer overwrite a real import. It still ends by saying how many errors happened without showing any. The rule that a run which closes must show its errors has one exemption left, and the bar says any path |
| A check written for exactly this collision has never run | The mapping importer builds a list of which collection owns which destination, consults it before importing, and nothing ever puts anything in the list. It reads like a live guard and has been empty since the folder layout changed under it. It is not load-bearing now, because qualifying the identifiers closed the collision it was meant to catch, but dead code that looks like a safety check is worse than no check |
The status is therefore: the ownership rule is finished and end-to-end verified, and the branch is still blocked, on the other half. What is left is one data-model decision and three small deterministic fixes, none of which is a search. That is a materially different position from the previous four attempts, each of which ended without knowing where to look next.
The sixth attempt: the boundary enforced on identity, and the address it does not cover (2026-08-30)
Section titled “The sixth attempt: the boundary enforced on identity, and the address it does not cover (2026-08-30)”The fifth attempt ended by naming exactly one decision and three small fixes. The decision was this: the index that decides where a note goes searches the whole vault and does not know collections exist, so a collection that owns nothing still finds every note carrying an identifier it produces, wherever it is and whoever it belongs to. The answer chosen was the one already ratified in August and never applied here: a run only touches notes its own collection owns. Everything else is reported, not adopted.
That is now built. It closes the identity route into another collection’s notes and leaves the address route open, which is the sixth level of the same thing.
What was built
Section titled “What was built”| Fix | What a person sees |
|---|---|
| A write never crosses a collection boundary | Placing a note now consults only the notes the collection doing the writing already owns. The whole-vault view is kept for one purpose: noticing. If a run would write an identifier another collection holds, the row is refused by name, saying which collection holds it and where, and nothing is written for it. Not moved, not merged into, not relabelled. Grouping notes follow the same rule, including under the older form of their identifier, because adopting one by its old name crosses the same boundary |
| A refused row is refused before anything vouches for it | The refusal happens before the row is counted as produced, before its folder is claimed, and before its note is even rendered. A row the run will not write must not be recorded anywhere as a row it wrote, or the missing-note count is computed against a note that does not exist |
| A second release of one framework is created with its own identifiers | When a new collection would write identifiers an existing collection’s notes already carry, it is created with those identifiers qualified by the collection instead. The two releases then coexist by construction rather than meeting as a refusal on every row. The mapping-import window has always done this; the wizard now does it too. Qualifying happens at the front of the identifier rather than at the end, so ordinary notes, grouping notes, and the collection’s home note are all qualified by one rule instead of the leaves being moved out from under the branches |
| Imports created before this change keep their identifiers exactly | Nothing collides, so nothing is qualified, and every existing import writes the identifiers it always wrote. A collection already qualified is never qualified twice |
How it was measured
Section titled “How it was measured”The unit suite is green at 2,370 passing and two skipped, up from 2,286. Fourteen of the new tests drive the refusal end to end through the real import window and read only what a person is shown: the finish message, the summary screen, and whether the window closed. A refusal nobody sees is the same product failure as the silent adoption it replaced, with the sign flipped.
Ten mutations were run, each restoring one guard. Nine turned their own tests red and no others, including the two sharpest: dropping the older-name half of the grouping-note check fails exactly the four tests about it, and making the qualification non-repeatable fails exactly the one test about refreshing an already-qualified collection. Two tests were retired and both were shown to have been passing because of the adoption: re-run in their old form against the new code, one fails with six refusals where it expected none, and the other cannot find the note it reads, because that note only ever existed at that address by having been taken from another collection.
One guard could not be falsified and is reported rather than papered over. The clause that forces the summary screen open when a run records errors is unreachable as a sole cause: a run with errors already fails the missing-note check, which already forces the screen. It is real defence in depth and it is currently riding on a neighbouring rule; if that neighbour ever stops depending on error count, it becomes load-bearing again.
The end-to-end run happened and it is not green. Forty-seven specifications, 25 failing declarations against 8 quarantined at the baseline, and none of the 25 is a timeout: every one is a deterministic assertion. Seventeen are new, and sixteen of them are one shape. Three specifications import the same source twice into one identifier space without ever naming a collection, which is precisely what the new rule refuses. They are test-only entry points and no person can reach them, so this is the harness encoding the behaviour that was deleted, the same thing the fifth attempt found in fifteen unit tests and fixed by making each one name the collection it refreshes. It is nevertheless a red suite, and a red suite is not a verified branch.
The seventeenth is not that, and it is a genuine trade the owner has to settle. Moving the refusal ahead of everything else means a row that is both a boundary crossing and separately invalid now reports only the crossing. One specification asserts the other error. Both placements satisfy the rule as written; the earlier one additionally keeps a refused row out of the produced set, which is worth having. Either the specification’s expectation moves or the refusal does.
And the criterion is still not met
Section titled “And the criterion is still not met”The bar’s closing sentence was restated one level down for this pass: nothing below the click can choose a collection, and nothing below the collection can choose a note. The first half holds. The second half is now true of identity and false of address.
| Found | Why it blocks |
|---|---|
| A write still reaches another collection’s note by address | Placing a note asks the folder before it asks anything else. If a note is already sitting at the address being written, it is used, and nothing reads who owns it. The new rule governs the route through identifiers and this route does not use them: the two notes have different identifiers and the same address. The note is merged into and relabelled as the writing collection’s own, and on grouping notes it happens with nothing reported at all, because that branch does not record what it wrote. At the other collection’s next refresh the note is gone from what it owns, so it is reported missing and written again, taking it back. Nothing stops this repeating |
| Qualifying the identifiers made that route the normal outcome instead of a rare one | The two changes in this pass point in opposite directions. Refusing on a shared identifier catches the collision and reports it. Qualifying the identifiers means there is no longer a shared identifier to catch, while the folder is derived from the file name and does not change, so two releases in one folder now collide on address alone, where nothing is watching. What was caught and named is now uncaught and silent. The mapping-import window does not have this problem, because its notes are named after their identifier, so qualifying the identifier qualifies the address too |
| Choosing “a new collection” on screen turns the qualification off | The qualification is only worked out when the ownership control is never touched. Both ways of asking for a new collection, the dropdown and the button that backs out of a suggested refresh, record the answer as a plain word, and the plain word is returned before the collision is ever considered. So the user who reads the screen and confirms the default gets unqualified identifiers, walks into every identifier the existing collection holds, and is refused on every row: a screen full of collisions and no notes written, for doing the right thing. The one who ignores the screen gets the qualified collection that works. The rule says qualify at creation, without condition |
| The mitigation is on one screen; the refusal is everywhere | Refusing is in the engine, so it applies to every path. Qualifying is in the import window and in the mapping window, and nowhere else. The bundled-example command asks for a new collection with unqualified identifiers, so importing the same sample twice now produces a page of refusals where it used to quietly overwrite. Safe, and not what anyone wants to see. This is the shape of the whole arc inverted: the rule at the shared layer, the escape from it copied onto screens |
The status is therefore: the collection boundary is enforced on identity, verified by a green unit suite and a falsification pass, and the branch is still blocked on the address route, on a mitigation that is off by default when a person uses the screen, and on an end-to-end suite that is red because the harness still imports the way the product no longer allows. None of the three is a search. The first is one ownership read at each of two places, the second is deleting a short-circuit, and the third is naming a collection in three specifications.
The seventh attempt: the last route closed, and the windows that never used it (2026-08-31)
Section titled “The seventh attempt: the last route closed, and the windows that never used it (2026-08-31)”The sixth attempt ended by naming three things and calling none of them a search. All three were done, exactly as written, and they were the right three.
The descent is over. Placing a note consults its identifier, then the folder it would land in, then creates a new file. That is the whole list; there is no fourth thing to ask. The sixth attempt governed the first, and this one governs the second, so every way into an existing note now reads who owns it before touching it.
And the bar is still not met, for a reason that is not one level further down, because there is no further down. It is one level out. The rule is now correct in the part of the product that imports frameworks, and there are windows that write notes without going through it.
What was built
Section titled “What was built”| Fix | What a person sees |
|---|---|
| A write reads who owns the note already sitting where it is about to write | Its own collection’s note is refreshed in place, exactly as before: that is the ordinary re-import and it is untouched. Anything else is refused by name and nothing is written for it. Another collection’s note names the collection that owns it. A note you wrote yourself says it is not Crosswalker’s and asks you to move or rename it. A note from an import made before collections existed says that instead of guessing an owner. Grouping and home notes follow the same rule |
| A refused row is refused before anything vouches for it | The ownership question is now asked before the row is counted as produced, before its folder is claimed, and before its content is checked. A row the run will not write must not appear anywhere as a row it wrote |
| Choosing “a new collection” on screen no longer turns the qualification off | Both ways of asking for a new collection now pass through the collision test on the way out, so confirming the default on screen gives the same working collection as ignoring the screen. Previously it gave the opposite |
| Three end-to-end walkthroughs name the collection they refresh | They imported the same source twice without ever saying which collection they meant, which is what the new rule refuses. Each now reads the stamp off a note it generated and passes it back on the second run, which is the harness standing in for the click a person makes |
How it was measured
Section titled “How it was measured”The fixture is worth more than the count. The sixth attempt’s rule refuses any row whose identifier another collection holds, so a vault with two collections sharing one framework never reaches the folder question at all — its tests pass against a build with no folder check whatsoever. The shape that isolates it is two different frameworks pointed at one folder: no shared identifier, so the identifier route is empty and the folder is the only way in. Sixty-six new tests are written on that fixture, across both overwrite settings and both ways of starting an import, and each asserts the whole refusal sentence rather than a fragment of it, because a fragment lets any part of it quietly disappear.
Seven mutations were run, each neutralising one guard, and every one isolated to its own tests: turning the check off fails forty-two, refusing the ordinary re-import fails forty-one, and letting the grouping notes adopt fails exactly the twenty-four about grouping notes. Three tests are controls by construction — nothing sits at the address, so the check is never consulted — and they exist so that a build which refuses everything cannot pass.
The unit suite is not green: 2,438 passing and two failing. Both failures are real, both are new, and they are described below rather than adjusted. Two others found in the same run were repaired: a test vault that told the engine a note it had just written did not exist, and a test that imported twice without naming a collection. Neither repair weakened an assertion.
The end-to-end run is the best of the arc and still not green. Forty-seven specifications; sixteen failing declarations turned green and not one turned red. The sixteen are exactly the three repaired walkthroughs. Nine failures remain and all nine are byte-identical to the previous run: eight are the long-standing quarantined visual specifications, and the ninth is not.
And the criterion is still not met
Section titled “And the criterion is still not met”| Found | Why it blocks |
|---|---|
| An evidence-link window writes over whatever is sitting at the address it picked | Linking a control to a piece of evidence writes a small note whose location is worked out from the two names. If anything is already there, it is replaced in full, and nothing reads who owns it: not the collection stamp, not the preserve-my-edits rule, not the conflict path. The folder that window writes into is a setting a person can change, so pointing it at a folder that already holds notes is enough. The window then says it updated the existing link for a note that was never a link. This is the code the engine had before this pass, alive on a different window, and it is the one remaining way to lose a note outright |
| The rule for creating a new collection is written three times and two of the copies are wrong | The import window now works out whether a new collection’s identifiers would collide with an existing one. The bundled-example command asks for a new collection without ever asking that question, so loading a sample twice produces a page of refusals. The mapping window asks a different question entirely: it looks at whether the destination folder is empty and treats that as an answer about identifiers. Drag that folder somewhere else in Obsidian and the next import of the same pair sees an empty folder, creates an unqualified collection, and meets the moved one as a refusal on every row. A folder is an address, and the whole arc is about addresses not naming owners |
| A note whose properties cannot be read is reported as not being Crosswalker’s | If a note’s properties are damaged, nothing can read the collection stamp off it, and the new check has only three answers available. It picks this is not Crosswalker’s, and tells you to move or rename your own imported note. Nothing is written either way and no data is lost, but the diagnosis is wrong and the instruction is alarming. It is also the two failing unit tests, which say in as many words that unreadable properties are a conflict and never anything else. The check needs a fourth answer — I could not read it — routed to the conflict path that already handles this correctly. This is absence read as fact one more time: unreadable is not the same as unstamped |
| A grouping note the run itself moved is not recorded as its own | The run keeps a list of the notes it has created so it cannot refuse itself, and a note it relocated is not added to that list. A later grouping note landing on that address would be refused as somebody else’s, on a run that had just written it. It needs a layout where two grouping levels overlap, so it is unlikely, and it is one line |
| Two rows of one import can still overwrite each other silently | The check that stops two rows landing in the same place reserves the address the row was aiming at, not the address it is actually written to, and those differ for a note that becomes its folder’s home note. The second row then finds the first row’s note, sees its own collection’s stamp on it, and correctly refreshes it in place: one concept written over another, both counted as created, nothing reported. It needs a concept named the same as the folder above it, and the fix is to reserve the address that is actually used |
| The end-to-end suite is red outside the quarantine | One walkthrough fails deterministically, on its own, in two seconds, and it is a fourth instance of exactly what the three repaired walkthroughs were: it generates over one identifier space twice without naming a collection. The second run is refused as a boundary crossing, so the specific error it was written to check for never happens. Either it gets the same repair the other three got, or the check it guards is unreachable for a source whose identifiers already exist elsewhere |
The status is therefore: the last route into a note is closed and verified, sixteen walkthroughs recovered with none lost, and the branch is still blocked on one window that can still overwrite a note outright, two copies of the new-collection rule that were never right, a misdiagnosis of a damaged note that two unit tests are correctly failing on, and one walkthrough outside the quarantine. None of them is the original defect and none of them is a search. Four are small and deterministic; the fifth is the owner’s call on what to say about a note nobody can read; and the sixth is the gate that makes the seventh pass the last one.
The eighth attempt: the windows given the rule, and the key that was never an identity (2026-08-31)
Section titled “The eighth attempt: the windows given the rule, and the key that was never an identity (2026-08-31)”The seventh attempt ended by naming five items and one gate, and by saying that none of them was a search. All five were built, exactly as written, and they were the right five. The two unit tests that had been left correctly failing now pass without being touched. The walkthrough that was failing outside the quarantine now passes, and the diagnosis was recorded rather than patched around: the walkthrough had been relying on the adoption the third attempt deleted, so the strict check it guards was never reached; naming the collection puts it back on the check, and the check rejects.
And the bar is still not met. Not one level down, because there is nothing below the folder, and not one level out again. It is in the same place, one turn further along. The rules were carried out to the windows, and the windows were found to be holding a second copy of the original mistake. The importer knows which note is which because each note records what produced it. The window that links evidence works out which note is which by computing a name from two file names — and two different notes can compute the same name.
What was built
Section titled “What was built”| Fix | What a person sees |
|---|---|
| The evidence-link window asks who owns a note before replacing it | Linking the same control to the same evidence again updates that link, as before. Anything else at that location is refused by name and nothing is written: another collection’s note names its owner, your own note says it is not Crosswalker’s, and a note nobody can read is asked to be repaired |
| The coverage-report writer asks the same question | Regenerating a report replaces the report. A note of yours that happens to share that name and folder is no longer destroyed without warning. It was, on every regenerate, and nobody had looked at that writer |
| A damaged note is a fourth answer rather than a wrong one of three | I could not read this note’s properties is now something the ownership check can say. It is routed to the conflict path, which leaves the file untouched and lets the run continue, and the wording asks you to repair the note and import again. It never says the note is not Crosswalker’s and never asks you to move or rename it |
| The rule for creating a new collection has one implementation | The import window, the mapping window, and the bundled-example command call the same function. The mapping window had been answering an identifier question by asking whether a folder was empty, so dragging that folder elsewhere was enough to make the next import of the same pair collide with the one you moved |
| A grouping note the run itself relocated is one of its own | A run records a note it moved, not only a note it created, so a later grouping note landing on that address is no longer refused as somebody else’s by the run that had just written it |
| The bundled-example command shows its errors | It states how many notes it wrote, how many it refused, and the first few reasons, instead of a bare count. It also stopped printing the list of created files where the number of them belonged |
How it was measured
Section titled “How it was measured”Both suites are green for the first time in the arc. The unit suite is 2,483 passing with two skipped, including the two that were deliberately red as the previous pass’s contract, and they pass unmodified. Forty-one new tests were written across six files and falsified with twenty-eight mutations, every one of which killed at least one named test; three assertions that no single-guard mutation can kill are stated as such rather than left to look load-bearing.
The end-to-end suite is green outside the quarantine: 167 passing, 8 failing, and all eight are quarantined visual specifications whose failure sites and messages are byte-identical to the previous run. No declaration was added, lost, or moved into or out of skip.
The run itself is worth recording, because it produced eleven failures that read exactly like regressions and were not. The machine’s monitors went to sleep during an unattended run. The compositor then stops producing frames, the browser cannot capture one, and every specification that takes a screenshot fails after three minutes with a timeout and no assertion frame. The session reported itself active and unlocked throughout; the only place the truth was visible was the display power state in the kernel’s device tree. Waking the display made all six affected specification files pass in full, individually, with fresh captures. A test harness that cannot see a screen fails in the shape of a product defect, and the next runner is told where to look before starting.
And the criterion is still not met
Section titled “And the criterion is still not met”| Found | Why it blocks |
|---|---|
| The name of an evidence link is computed from two file names, and the new check trusts it | The window works out both the location and the identifier of a link from the two notes’ file names. Two controls that share a file name therefore share an identifier, and two releases of one framework are precisely two controls that share a file name. Link evidence to a control in the first release, approve it, record the reviewer and the baseline, write your notes; link the same evidence to the same control in the second release and the check sees a matching identifier, concludes it is the same link, and replaces the file whole. The notice says it updated the link. The attestation is gone, the first release’s coverage silently loses a control, and nothing is reported, because the identifier never changed and there is therefore no duplicate for anything to notice. The check that was added is correct; the thing it checks is not an identity. A narrower form needs only two concept notes with the same file name in different folders, which the ordinary folder-and-file layout produces routinely |
| Two notes can come to hold one identifier, and every later import then fails | The same window’s other branch, the one that creates a link rather than replacing one, consults nothing. Drag an evidence link elsewhere in Obsidian and the next link for that pair finds nothing where it looked and writes a second note carrying the same identifier. From then on every import reports an ambiguous identifier and refuses to resolve it, with nothing on any screen explaining where the second one came from. The same state is reachable from two controls whose file names differ only in punctuation, because the rule that makes a name safe for a file and the rule that makes it safe for an identifier collapse different characters |
| The single qualification rule can read an unfinished index as an empty vault | The rule now exists once, which was the point, and the one implementation asks the vault what collections exist. That question is answered from Obsidian’s index, and the two command-palette paths that ask it do not first wait for the index to finish. During startup on a large vault the answer is fewer collections than there are, so the rule mints unqualified identifiers into a space that is already occupied, and the ownership rule then correctly refuses every row of the import. That is the same symptom the rule was written to eliminate, reached through cache lag instead. This is absence read as fact for the seventh time, inside the function introduced to be the one place that cannot be wrong |
| The one write that records an audit fact records it by address | Re-baselining a reviewed link takes its list of notes out of a previously generated report, joins that list to the stored projection by file location, and writes the new review baseline to whatever note now sits at each location. The link’s own identifier is available and is never compared. A stale report plus a note replaced at that location stamps an attestation baseline onto a note nothing ever identified. It is also the reason the sweep missed it: it writes through a different mechanism than the two the sweep searched for, so a search by call name could not see it |
| A damaged generated report is still told it is not Crosswalker’s | The fourth answer was added to the ownership check in the importer, and the report writer added this pass does not use that check. It asks a helper that collapses unreadable into nothing there, so a generated report whose properties a person has damaged is told that a note Crosswalker did not generate is in the way, and asked to move or rename it. That is the exact false cause and the exact banned instruction the fourth answer exists to remove, at a site added in the same pass |
The status is therefore: both suites are green, the rules reached every window, and the branch is still blocked on an identity that is computed from file names instead of read from notes, on the one write that records an audit fact by location, on a single qualification rule that can still be asked during indexing, and on one damaged-note message that did not get the fourth answer. The first is the largest, is reachable through the release-isolation feature this arc exists to make safe, and is the only remaining way to lose a note.
The ninth attempt: the windows given an identity, and the same defect found where they copied it from (2026-08-31)
Section titled “The ninth attempt: the windows given an identity, and the same defect found where they copied it from (2026-08-31)”The eighth attempt ended by naming four items, all of them in the windows beside the importer, and by saying the largest was an identity computed from two file names. All four were built as written, and they were the right four.
And the bar is still not met. Not one level down and not one level out. This time it is at the beginning. Nine passes have moved down and then outward, and each one attacked the place the previous review had pointed at. This pass fixed the two derivations in the evidence window; the identical defect was sitting two modules away in the importer, where it has been since the importer was written. The identifier of an imported control is its file name, run through a routine whose entire job is to make a string legal as a file name. That routine turns a slash into a hyphen, and a slash is a perfectly legal character in a control identifier. Two identifiers a source declares as different become one, both notes are written anyway because nothing checks within a single run, and from the next import onwards every run in that vault stops on an ambiguity naming two files and no cause.
What was built
Section titled “What was built”| Fix | What a person sees |
|---|---|
| An evidence link is named after the control it is about | Two releases of one framework hold two controls with the same file name. Linking evidence against the second used to replace the first release’s link in full while reporting an update, taking the approval, the reviewer, the date, the review baseline, and anything written by hand. They are now two links and both survive. A fixed hash of the exact pair is part of the name, so no two pairs can present one name however the text is cleaned up |
| Links written before the change are still recognised | The old form of the name is looked for as well, and a note found under it is updated where it sits rather than duplicated. It is adopted only when the note itself says it is about this control and this evidence, because the old name was never unique and several notes can legitimately hold it |
| The window looks for the link before it creates one | Move a link note anywhere in the vault and the next link for that pair updates it where it now is, and the notice names where. Previously it found nothing where it expected and wrote a second note with the same identifier, after which every import reported an ambiguity that no screen explained |
| The collection-creating rule will not answer while the vault is indexing | It counts unread files, waits, and counts again, then refuses with the indexing message. The wait is not taken as proof; the second count is. The refusal lives inside the rule, so the two command-palette paths cannot skip it, which is how the first version of this precondition ended up in one caller out of three |
| The one write that records an attestation fact checks it is writing to that link | Re-baselining used to take a list of locations out of an old report and write the new baseline to whatever note now sat at each. It now compares each note’s own identifier before the first write and refuses the whole selection by name if any disagrees, naming both links, or asking for a note to be repaired when it cannot be read |
| A damaged coverage report is asked to be repaired rather than disowned | The fourth answer added by the seventh pass reached one more writer. A report whose properties have been damaged is no longer told that a note Crosswalker did not generate is in the way and asked to move it |
The sweep that found the last of those was widened from two ways of writing a note to five, across the whole plugin. Eleven writers were examined and named: six are closed, and five are deferred with a written reason rather than a shrug. One of the deferrals turned out to be wrong and is a finding below.
How it was measured, and where the evidence stops
Section titled “How it was measured, and where the evidence stops”Fifty-nine tests were written across six files, and every one of thirty-three sabotage checks killed at least one of them. One sabotage initially survived, because removing the branch it targeted left a refusal landing for the neighbouring reason; the test was strengthened to assert the wording, since this note carries no identity and this note is a different link are different situations for the person reading them. Three test files that the change had broken were repaired rather than deleted, and the reason one of them broke is worth recording: the project’s test configuration transpiles without type-checking, so a function called with the wrong number of arguments fails at run time rather than at build time.
Neither suite was run. Partway through, the machine’s disk filled to capacity, from a test harness in an unrelated project on the same machine that was creating scratch folders at roughly twenty-four a second and never removing them. Every command then failed before it started. The end-to-end suite copies a vault per worker and writes its log to the same disk, so it could not have run; more to the point, a run started in that state fails in the shape of the defect under test. Mass setup failures arrive as timeouts and missing files across every walkthrough, which is the same shape a change to how evidence links are named would produce, and the whole question this pass had to answer was scheme migration against regression. Writing no result was better than writing an unreadable one. The last real measurement therefore stands and is from the pass before: the unit suite green at 2,483, and the end-to-end suite at 167 passing with 8 failing, all quarantined.
And the criterion is still not met
Section titled “And the criterion is still not met”| Found | Why it blocks |
|---|---|
| The name of an imported control is its file name | The importer builds a control’s identifier by taking whatever the recipe’s file-name template produces and running it through the routine that makes a string safe to use as a file name. That routine replaces a slash, collapses runs of whitespace, strips a leading dot and a trailing .md. All four are legal in a control identifier, and the first is ordinary: a source with A.9.2/1 and A.9.2-1 in it declares two controls and gets one identifier. The two notes land at two different addresses, because the address is built from the raw values, so both are written; nothing checks for a collision within a single run. From the next import onwards every run in that vault reports an ambiguous identifier, refuses to resolve it, names two files, and gives no cause. Supplying the identifier explicitly does not help: the ordinary import path never reads it, and the recipe path reads it and then puts it through the file-name routine anyway. It has been there since the importer was written, and no bundled framework shows it, because their identifiers happen to contain only letters, digits, dots and hyphens |
| The name of a crosswalk mapping is its two endpoints, cleaned up for a file system | The same defect with a stronger rule and one more head. Every character that is not a letter, digit, underscore or hyphen is replaced, so a dot and a parenthesis both vanish and A.9.2.1 and A-9-2-1 become one endpoint. The two halves are then joined with a hyphen, and a hyphen can occur inside either half, so a different pair can produce the same joined name by moving the boundary. Reconciling the same control written two ways is the ordinary reason to build a crosswalk, so one well-formed mapping file reaches it. The evidence window’s new name is built to be immune to exactly these two heads, one module away |
| Neither suite was run against the change | Sixty-eight tests pass individually and none of them is the suite. The end-to-end suite is the only thing that can separate the effect of renaming evidence links from a regression, and it is also the only thing that can show that the eight quarantined failures are still the same eight. Until it runs, the four fixes above are tested and not verified, which by this arc’s own bar is not shipped |
| The reset tool decides what to delete by searching a note’s text | Found while checking a deferral rather than by the sweep. Reset imports falls back, when it cannot read a note’s properties, to searching the whole note for a line beginning with Crosswalker’s marker. So a note of a person’s own that quotes or documents that marker is offered for deletion, and a generated note whose properties are damaged is deleted rather than skipped. The reason given for deferring it, that it fails in the safe direction, is wrong in both directions. It is a maintenance command and deletions go to the configured trash, which is why it is listed here rather than higher |
The status is therefore: the windows now hold identities worth refusing on, the sweep is written down rather than assumed, and the branch is still blocked on the same defect at the point every rule reads from, on the crosswalk name that shares it, and on two suites that could not run. None of the three is a search. The first two are one change each and the third is a disk.
5. The same family, one layer down: a reset that reset nothing
Section titled “5. The same family, one layer down: a reset that reset nothing”Also on 2026-08-28, and the reason this log covers three things rather than one.
Maintenance: reset search data exists to throw away Crosswalker’s derived search data so it can be rebuilt from the canonical notes. It was deleting nothing at all. The index is opened through one storage mechanism and the delete was attempted through a different one, behind a typeof guard that was never true. The guard was false, the function returned having done nothing, and the command still showed Fast query index cleared.
Every row the reset was supposed to destroy survived, and the next query answered out of it. Nothing downstream ever complained, because losing this data is safe by design. The index rebuilds from the notes, so an index that was never cleared behaves almost exactly like one that was cleared and immediately rebuilt. The one observable difference is stale rows, and only if you go looking.
The reset now deletes where the file actually lives, removes any partial write left beside it, closes the live connection first, then re-reads and throws if anything survived, so the command reports failure instead of a success it cannot see. The message now distinguishes three genuinely different outcomes: something was deleted, there was nothing to delete, or the data was only ever held in memory this session.
The E2E test that covered this command is worth reading as an artifact. It projected rows first, with a comment saying “so we have data to verify is gone after clear”, and then never checked that the data was gone. It asserted that the handle was null and that a later reprojection succeeded, with a further comment excusing the row counts as possibly zero. The test watched the mechanism and not the outcome, so it passed for months over a command that did nothing.
This is the same family as the closure cache fix and the cache-lag bugs: absence read as fine. A missing cache entry read as “no result.” A not-yet-indexed file read as “no frontmatter.” A delete that found nothing read as “already clean.” In each case the code observed nothing and concluded that everything was in order, when the correct conclusion was I cannot tell.
It had also been flagged. Challenge 45 named the purge boundary as unproven, noted that the clear was best-effort and silently did nothing when the function was unavailable, and told pilots not to treat the index as a confidentiality boundary. The finding was recorded and correct. It took until today to become a fix.
6. What changed
Section titled “6. What changed”| Area | Change |
|---|---|
| Installed-framework discovery | Derived from provenance plus CURIE prefix. Path is no longer an input |
| Coverage report | Refreshes the query index before reading it, and reports a refresh failure instead of an empty result |
| Command palette | Grouped by what a person is trying to do: Start here, Evidence, Explore data, Import and export, Maintenance, Developer tools. Internal storage vocabulary removed from every command name |
| First-run coverage | tests/e2e/first-run.spec.ts. Empty vault, real wizard, real 12-row NIST slice |
| First-run test wiring | The shared configuration excludes first-run.spec.ts, which its own configuration then clears. Run against the seeded vault the spec tested the opposite of its purpose, and failed for the one reason that is not a defect |
| Living pages | Quick start and command reference re-verified against the empty-vault run, markers dated |
| Maintenance reset | Deletes the index where it actually lives, verifies the deletion, and fails loudly when it cannot |
| Documentation links | A new check:links CI gate, described below |
| Migration safety for an existing vault | Attempted nine times and not landed. Each attempt was measured against a bar written down in advance, and each ended one level below the last: the folder a refresh writes into, the label that matched a source to a collection, the engine branch that adopted whichever collection shared the folder, the vault-wide search that placed each note, and finally the folder that note was about to land in. All five are settled, and the fifth was the last of them, because placing a note asks nothing else. The eighth carried those rules out to the windows beside the importer and the ninth gave those windows an identity worth refusing on. What remains is not a sixth level: it is the same mistake at the origin, where the identifier of an imported control is computed from the name of the file it lands in, which is the value every rule above compares and stamps. Recorded as blocked, not as shipped |
The link gate is its own small instance of the same theme. Astro builds and deploys a page full of broken internal links without complaint, and Starlight does not validate them either, so the failure was silent by construction: green build, successful deploy, and the only signal a reader landing on a 404 that nobody hears about. A sweep found 68 files pointing at challenge briefs that had been archived out from under them, across eighteen briefs and months of archiving. bun run check:links now resolves all 4,947 internal links across the site, README, ROADMAP, and both agent-instruction files, in about a second with no build, no browser, and no network. It deliberately does not check heading anchors, because a gate that cries wolf gets waved through when it is finally right.
7. Open at the time of writing
Section titled “7. Open at the time of writing”Closed by adversarial review, in this same session
Section titled “Closed by adversarial review, in this same session”The reset fix was reviewed by an agent whose only instruction was to prove it wrong, and it found two ways the repaired command still deleted nothing while saying something reassuring. Both are recorded because the pattern is the point: the fix removed the original silent no-op and immediately grew two more behind it. Neither was reachable on the default path, which is exactly what would have carried them through a manual test.
| Found | Why it mattered | Resolution |
|---|---|---|
| The pool key was rebuilt by hand and escaped differently from the storage layer | A Query index file containing a space, such as Vault Notes/.crosswalker.sqlite, matched nothing, deleted nothing, threw nothing, and reported “already empty.” The setting has a folder picker attached, so spaces are ordinary | Key is now derived by the same expression the storage layer uses, so the two cannot disagree. Regression test asserts the escaped form and fails on the hand-rolled version |
| A storage-pool that refused to open was read as “nothing was persisted” | The pool also refuses while a second vault window holds it, and caches that refusal for the session. The rows are intact on disk and the user is told they were discarded | Absence must be earned: “nothing persisted” is claimed only when this session demonstrably ran in memory. Any other unreachable pool is a reported failure |
Still open
Section titled “Still open”| Item | State |
|---|---|
| The seeded-vault assumption elsewhere | Asked, answered, and the first move executed. The audit found sixteen assumptions across 48 specs. Its top finding — the configured output folder is never written to — was closed by pointing the seed at Frameworks, which cost one new test failure and revealed a live destination defect. The remaining fifteen are unfixed |
| The destination defect the experiment revealed | Fixed 2026-08-29, verified, with named residual risk. The guard now reads a recorded choice instead of an empty field, the recognised-recipe reconciliation is decided (curated folder wins, nested inside the configured root), and a real run confirms the per-import folder. The residual items below are what the adversarial review found; delivery detail is in CHANGELOG.md [Unreleased]. The follow-up making it safe for an existing vault did not clear its bar and is not shippable — see the next row and section 4 |
| A vault that already imported into the shared folder | Attempted eight times, still open, and no longer the largest remaining item. Its next import lands in the new per-import folder, does not recognise the earlier collection, and mints a new one: split on skip, moved on replace, deleted in neither. Every rule under it is settled and implemented, and as of 2026-08-31 it is implemented on every window that imports rather than only in the importer: a refresh never changes a collection’s folder, a collection’s label is pinned when it is created, nothing below a click can choose a collection, and a write reaches an existing note only when the collection doing the writing owns it, by identifier and by address. What blocked as of 2026-08-31 is no longer one turn further along but at the origin: the identifier of an imported control is computed from the name of the file it lands in, and that identifier is what every rule above compares, refuses on, and stamps. See its own row below |
| The ordinary wizard labels every import with the same placeholder | Closed 2026-08-30. The class it belongs to is not. The ordinary path now labels an import with the name of the file it came from, and matching ignores the placeholder labels. A second unrelated framework through that path now defaults to a new collection, falsified against the previous behaviour. The three rows below are the same defect through labels that are not placeholders |
| A genuine ambiguity error ends the run on the success path | Closed 2026-08-30. Any recorded error now forces the summary screen open with the collision named on it, and the finish message points at it. The rule is that a run which closes must show its errors. It is not yet applied to the mapping-import surface, or to a run whose only anomaly is a conflict; both are separate rows below |
| The indexing precondition can report success without having run | Open, and now smaller again. A forced check that finds one already in flight still returns passed whenever nothing has been refused yet, which is the state during the wait it exists to enforce. As of 2026-08-30 it guards all four entry points rather than one, and no caller acts on that answer without separately refusing an unfinished scan, so it is a misleading return value rather than a reachable defect |
| The mapping-import window still guesses ownership, and its default is to overwrite | Closed 2026-08-30. It now lists every collection at that pair’s folder, opens on keep this release as a new collection, and reaches a refresh only through the button that names one. A new collection is written in skip mode and only a chosen refresh replaces anything, and a second release is qualified so it cannot collide with the first on a shared identifier. What it did before: It is the surface the general-purpose mapping recipe uses for every pair of frameworks. If one collection already sits in the folder for that pair it is adopted silently, with no control on the screen and no click, and that window writes in replace mode, so an earlier mapping is rewritten in place and the rows it no longer covers become missing notes nothing there reports. The folder for a pair of frameworks is deterministic; who owns what is inside it is not |
| A scan that has not started reads the same as a scan that found nothing | Closed 2026-08-30. The scan is a three-state answer, all four ways into the later steps wait for it, and the three places that decide anything refuse with the indexing message rather than reading an unfinished scan as an empty vault. What it did before: The list the wizard defends itself with is empty both when the vault holds no collections and when the scan has not finished. Two ways into the later steps do not wait for it, a recognised source and a resumed draft, and the button that starts the import does not check. In that window the ownership step draws no control at all, so there is nothing to click, and the import falls through to the older rule underneath, which adopts whichever collection shares the folder. Timing-gated; the missing check is not. The same shape as reading an unfinished index as an empty one |
| The bundled-example command has no ownership control at all | Closed 2026-08-31. It asks the one qualification rule for its collection rather than carrying a literal, so loading the same sample twice no longer produces a page of refusals, and it now states how many notes it wrote, how many it refused, and the first few reasons. It was also interpolating the list of created files where the number of them belonged. Its remaining exposure is shared with the mapping window and is the indexing row below, not this one |
| A write crosses the collection boundary even when ownership is answered correctly | Half closed 2026-08-30, on the identifier route. Placing a note now consults only the notes the collection doing the writing owns. A run that would write an identifier another collection holds refuses that row by name, saying which collection holds it and where, and writes nothing for it: not moved, not merged into, not relabelled. Grouping notes follow the same rule, including under the older form of their identifier, and the whole-vault view is kept only for noticing. The decision taken was the one ratified in August and never applied here, that a run only touches what its own collection owns. The address route is the row below |
| A write still crosses the boundary by address | Closed 2026-08-31 in the importer, and it was the last route. Placing a note reads the collection stamp off whatever is already at that address. Its own collection’s note is refreshed in place, which is the ordinary re-import and is unchanged. Anything else is refused by name and nothing is written for it, with three distinct answers: another collection’s note names its owner, a note that is not Crosswalker’s says so, and a note from before collections existed says that rather than inventing an owner. Grouping and home notes follow the same rule. Sixty-six tests on a fixture the identifier rule cannot reach, seven mutations each isolating. The window that links evidence does not go through any of this: see its own row |
| Qualifying the identifiers made the address route the normal outcome | Closed 2026-08-31 as a silent outcome; open as a folder question. Two releases in one folder still land in one folder, because the folder is derived from the name of the file. They now collide loudly: the second run refuses every note it would write over and names the collection that owns it, instead of taking them. Uncaught and silent has become caught and reported, which was the whole complaint. Making the two coexist rather than collide means the folder can no longer be derived from the file name alone, which is the same-name-source row further down |
| Choosing a new collection on screen turns the qualification off | Closed 2026-08-31. Both ways of asking for a new collection now go through the collision test on the way out, so confirming the default on screen gives the same working collection as ignoring the screen. It was one short-circuit and it was returning the plain word before the test could run |
| The same rule is written once per window instead of once | Closed 2026-08-31 as a duplication, and it is what let the next defect through. The qualification rule now has one implementation beside the mint, and the import window, the mapping window, and the bundled-example command all call it; the mapping window’s folder-emptiness copy is deleted. The write itself went through the same treatment: the evidence-link window and the coverage-report writer now consult ownership, and the sweep that found the report writer is recorded. The gate that would stop a seventh copy is still Part B work. What the collapse revealed is the row below: a rule with one implementation can still be wrong if the key it is implemented over is not an identity |
| The evidence-link window replaces whatever sits at the address it picked | Closed 2026-08-31 on the ninth attempt, tested and not yet suite-verified. The link’s identifier is now built from the control’s own recorded identifier plus a fixed hash of the exact pair, so two releases of one framework produce two links and both survive, and no cleaning-up rule can collapse two pairs into one name. The branch that creates a link consults identity across the vault first, so a link moved in Obsidian is updated where it sits instead of duplicated. Links written under the old name are recognised and adopted, but only when the note itself says it is about this control and this evidence, because the old name was never unique. What it did before, when it was the largest remaining item: the eighth pass had given the window an ownership read and three refusals plus the fourth for an unreadable note, and the identity it refused on was computed from the two notes’ file names rather than read off the notes, so two controls sharing a file name shared an identifier. That is two releases of one framework, exactly the case release isolation exists to support. Linking evidence to the second release passed the check and replaced the first release’s link in full, taking its approval, reviewer, date, review baseline, and anything written by hand, while reporting an update. The branch that creates a link consulted nothing, so moving a link in Obsidian caused a second note with the same identifier and every later import to report an ambiguity nothing explained. The check was correct; the key was not an identity |
| A note whose properties cannot be read is reported as not being Crosswalker’s | Closed 2026-08-31 in the importer, in both windows that use its check, and on the ninth attempt in the one writer that had its own. The ownership read has a fourth answer, I could not read it, routed to the conflict path that leaves the file untouched and lets the run continue, and its wording asks you to repair the note and import again rather than claiming the note is not Crosswalker’s or asking you to move it. The two unit tests that asserted this contract now pass unmodified. Fully closed 2026-08-31 on the ninth attempt. The coverage-report writer had asked a different helper, one that collapses unreadable into nothing-there, so a generated report with damaged properties was still told a note Crosswalker did not generate was in the way. That helper now has the same three answers as the importer’s, and the two-state wrapper left in place for the callers that only read a key is documented as forbidden to any caller that tells a person a cause |
| The name of an imported control is its file name | New 2026-08-31, blocking, and it is the same defect the four rows above just closed, at the point they all read from. The importer builds a control’s identifier from whatever the recipe’s file-name template produces, run through the routine that makes a string safe as a file name. That routine replaces a slash, collapses whitespace runs, and strips a leading dot and a trailing .md, and all four are legal in a control identifier. A source declaring A.9.2/1 and A.9.2-1 gets one identifier for two controls; both notes are written, because nothing checks for a collision inside a single run, and every later import in that vault then refuses to resolve an ambiguity, naming two files and no cause. Supplying the identifier explicitly does not help: the ordinary import path never reads it and the recipe path puts it through the same routine. Present since the importer was written; invisible because no bundled framework has a slash, a double space, or a leading dot in an identifier |
| The name of a crosswalk mapping is its two endpoints cleaned up for a file system | New 2026-08-31, blocking, same class, one module over. Every character that is not a letter, digit, underscore or hyphen is replaced, so a dot and a parenthesis both vanish and two differently written forms of one control become one endpoint. The two halves are then joined with a hyphen that can also occur inside either half, so a different pair can produce the same name by shifting the boundary. Reconciling the same control written two ways is the ordinary reason a crosswalk exists, so one well-formed mapping file reaches it. The comment on that function says the cleaning keeps the result safe for a file system, which is the arc’s thesis stated as a justification |
| Neither suite has been run against the ninth pass’s changes | New 2026-08-31, blocking. Sixty-eight unit tests pass individually, fifty-nine of them new, and thirty-three sabotage checks each kill at least one. None of that is the suite. Partway through the pass the machine’s disk filled to capacity from a test harness in an unrelated project creating scratch folders at roughly twenty-four a second and never removing them, and every command failed before starting. A run in that state was refused deliberately rather than attempted: mass setup failures arrive as timeouts and missing files across every walkthrough, which is the same shape a change to how evidence links are named would produce, and separating those two was the entire question the run had to answer. An unreadable result is worse than no result |
| The reset tool decides what to delete by searching a note’s text | New 2026-08-31, found while checking a deferral rather than by the sweep. When Reset imports cannot read a note’s properties it falls back to searching the whole note for a line beginning with Crosswalker’s marker. A note of a person’s own that quotes or documents that marker is therefore offered for deletion, and a generated note with damaged properties is deleted rather than skipped. The written reason for deferring it, that it fails in the safe direction, is wrong in both directions. It is a maintenance command and deletions go to the configured trash, which is why it sits below the three rows above |
| A sweep by call name cannot find the class | New 2026-08-31, and it is the argument for the gate rather than a tenth sweep. Three sweeps have now been run by searching for the names of the functions that write, and each found real instances and missed the next class: one missed the writers that edit properties in place, one missed the two routines that compute identifiers, and the widened one found six sites and deferred five, one of them wrongly. The property that has to hold is not a list of call sites. It is that nothing computes an identity from a path or a file name, which is a mechanical check of the same shape as the internal-link gate that sixty-eight broken links justified |
| A grouping note the run itself moved can be refused by the same run | Closed 2026-08-31. A relocated note is recorded as one the run produced, at the point of the rename, so both grouping loops get it from one place rather than two. Verified on a real two-run ordering in which the relocation actually happens, not a stub |
| Two rows of one import can overwrite each other with nothing reported | Open 2026-08-31, examined, and it is not the two-line fix it was recorded as. The check that stops two rows landing in the same place tests the address the row rendered, which is the right key for a row-against-row question, and reserves that same address. Only the reservation needs to become the address actually written. Substituting one for the other breaks the row-against-row check, so it needs two sets rather than one substitution, and it is the next slice with that framing |
| ”Stop on conflict” moves your notes and then refuses to write | New 2026-08-30, blocking, and three lines to fix. The setting whose entire meaning is touch nothing when something is already there is the one that relocates most. The move happens before the refusal is raised, so a run in that mode renames every note it would have conflicted with into its own folder, then reports each one as a conflict and writes nothing. Three places ask “is this not skip” where they should ask “is this replace” |
| A check written for exactly the collision above has never run | New 2026-08-30, not load-bearing, delete or wire it. The mapping importer builds a list of which collection owns which destination, consults it before importing, and nothing ever puts anything in the list; it has been empty since the folder layout changed under it. Qualifying the identifiers closed the collision it was meant to catch, so nothing depends on it. Dead code that reads as a safety check is worse than no check |
| The end-to-end suite is red on the branch | Closed 2026-08-31. Forty-seven specifications: 167 passing, 8 failing, and all eight are the long-standing quarantined visual specifications with failure sites and messages byte-identical to the previous run. No declaration was added, lost, or moved into or out of skip. The walkthrough that had been failing outside the quarantine now passes, and its diagnosis was recorded rather than patched around: its probe relied on the engine adoption the third attempt deleted, so the strict check it guards was never reached. Naming the collection puts it back on the check and the check rejects. Recorded for the next runner: eleven further failures in that run were the machine’s monitors going to sleep. With no frames being produced, every screenshotting specification fails after three minutes with a timeout and no assertion frame, which reads exactly like a regression; the session reports itself active throughout and the only visible signal is the display power state in the kernel’s device tree. All six affected specification files pass in full once the display is awake |
| The unit suite is red on the branch | Closed 2026-08-31. 2,483 passing, two skipped, none failing. The two tests that were correctly red pass unmodified. Forty-one new tests were added across six files and falsified with twenty-eight mutations, each killing at least one named test; the three assertions no single-guard mutation can kill are stated as preconditions rather than left to look load-bearing |
| The one qualification rule can read an unfinished index as an empty vault | Closed 2026-08-31 on the ninth attempt, tested and not yet suite-verified. The rule counts unread files, waits on the index, and counts again before answering, then refuses with the indexing message. The wait is not taken as proof; the second count is. The precondition is inside the rule and runs above every shortcut branch, so no caller can forget it, which is the whole point: the first version of this precondition lived in one caller out of three. What it did before, and it was absence read as fact for the seventh time: Collapsing the three copies of the qualification rule into one put the vault-wide question in a single function, and that function asks Obsidian’s index. The two command-palette paths that call it, the mapping import and the bundled-example command, do not first wait for the index to finish, and the wizard’s warm-index precondition lives in the wizard rather than in the rule. During startup on a large vault the answer is fewer collections than there are, so the rule mints unqualified identifiers into an occupied space and the ownership rule then correctly refuses every row. That is the symptom the rule was written to remove, reached through cache lag instead. The precondition belongs inside the rule, where a caller cannot forget it |
| The one write that records an audit fact records it by address | Closed 2026-08-31 on the ninth attempt, tested and not yet suite-verified. The link’s own identifier is now selected alongside the location, and each note is read and compared before the first write. A disagreement refuses the whole selection through the command’s existing refuse-all path and names both links; a note that cannot be read is asked to be repaired rather than called somebody else’s; an entry carrying no identifier at all says that rather than being reported as the wrong link. What it did before, and the sweep could not have found it by searching: Re-baselining a reviewed link takes its notes out of a previously generated report, joins that list to the stored projection on file location, and writes the new review baseline to whatever note now sits at each location. The link’s own identifier is in the projection and is never compared. A stale report plus a note replaced at that location stamps an attestation baseline onto a note nothing identified. It also writes through a different mechanism than the two the ownership sweep searched for, which is why a sweep by call name missed it, and is the argument for the gate rather than a ninth sweep |
| The branch had no end-to-end run between the ownership rule changing and the pass before this one | Closed 2026-08-30. Forty-seven specifications, no regression: the eight declarations that failed and had passed at the baseline are all screenshot timeouts, no assertion failed anywhere in the run, and all five of those specifications are green on their own at their baseline durations. The unit suite is green as a whole at 2,286 passing. Why it had been outstanding: the host was carrying an unrelated foreground workload at three times the pressure threshold with 7 GB swapped, and starting 47 headed sessions on top of that is the arrangement that locked the machine on 2026-08-21. Refusing was correct. A static pass in its place found that no end-to-end test touches any of the four changes, so the run is a precondition for shipping and not a source of confidence about them. The unit suite has also not been re-run green as a whole since three stale assertions were widened |
| A collection’s label is not pinned when the collection is created | Closed 2026-08-30, and it was the right call. The label is stamped when the collection is created and a refresh mints under the collection’s own label. A collection created before this change is pinned on its first refresh to whatever its notes already carry. Two blocking rows below closed as a consequence rather than by a separate fix. Residual: a collection whose notes carry two or more labels pins nothing and keeps the old behaviour, which is reachable in exactly the vaults damaged by the original defect |
| Several bundled imports share one label | Closed 2026-08-30 on the ordinary wizard, by removing the match rather than fixing the labels. They still share labels. A shared label can now only draw a suggestion, and only when exactly one collection carries it, so two unrelated mapping imports land as two collections with nobody clicking anything. The guess it defeated survives on the mapping-import window, which is where the general-purpose mapping recipe actually runs — see the row below |
| A saved configuration reused across two frameworks is read as ownership | Closed 2026-08-30. Nothing derived from the source decides ownership on the ordinary wizard any more, so a configuration identifier cannot promote itself to one. Falsified against the previous behaviour: one saved configuration applied to two frameworks now produces two collections |
| Two source files with the same name still collide | Closed 2026-08-30 for ownership, open for foldering. Two exports both called controls.csv no longer resolve to one collection, because the name no longer decides anything. The destination folder is still derived from the file name, so the two still land in one folder unless a destination is typed. That half is the row on renaming a source, below |
| A collection imported before this change can no longer be refreshed at all | Closed 2026-08-30. The ownership step lists every collection in the vault rather than only the matches, each with its folder, note count, and source, so a collection that matches nothing is still there to pick. The control the previous fix assumed was present now exists |
| Explicitly refreshing such a collection reports every note in it as missing | Closed 2026-08-30 by the pinned label. The refresh mints under the label the notes already carry, so the owned set and the produced set are the same set again, and nothing is restamped |
| ”No orphans” is printed when nothing was checked | Closed 2026-08-30. Whether the check ran is now recorded separately from what it found, set to not run before the work starts so a failure part-way cannot read as checked. The finish message says orphans not checked and forces the summary screen, which prints the same words where the number would be |
| A conflicts-only run closes over its own screen | New, one term. A “notes left unchanged” warning tells you to see the summary screen, and that screen is not drawn for it, although it has a section built for exactly that list. Errors were added to the condition that opens the screen; conflicts were not |
The mapping-import window prints [object Object] in place of the error | Closed 2026-08-30. Each error is named, the window stays open on a failed run, and the errors are listed on a screen. One exit remains on that window, for an unreadable source file, which still reports through the truncating notice the rest of it stopped using |
| Home and grouping notes are still matched by location | Answered by the reframing, not yet shippable. They reconcile through the identity index like every other note, and under the rule that a refresh never changes a folder the alias keeping the old identifier alive is always reconstructed against the folder it was written under. The conditional part is gone. What remains is the collision half below |
| May a grouping note’s identity be independent of its folder? | Half answered. Under the new rule the survive-a-move half no longer needs an answer, because nothing moves without being asked. The collide half is still live: two imports of one framework into two folders claim the same grouping identifiers. Still an owner call |
| Two of the blocking findings are small and deterministic | Both closed on the blocked branch, unreleased. The enrichment phase is now told which overwrite setting is in force, and a suppressed move reports the folder the note is in rather than the one it would have gone to. Neither ships while the branch is blocked |
| Moves and orphans are recorded and never shown | Closed on the finish message, and still wrong in one case. Both counts now ride on the notice for every run, so a clean refresh states its zeros instead of closing silently. But the orphan count is read from a field the engine leaves unset both when there are none and when the check never ran, so a suppressed check still reports “No orphans.” See the row below |
| Two renderings of one framework still share a curated folder | Open, and it is the original defect in miniature. The hierarchical and flat NIST CSF exports both resolve to NIST CSF 2.0, so the second is offered as a refresh of the first. Either the curated folders distinguish the rendering, or the ambiguity is put to the user |
| Switching source file keeps a typed destination | Open, one-line fix. Both source-reset sites clear the curated destination and neither clears the typed one, so picking a source, typing a destination, then switching sources sends the second into the folder belonging to the first |
| The seed change itself | Applied, not yet committed, and one half of it is now moot. visual-workbench.spec.ts completes a generation for the first time and needs no ownership answer, because the destination fix removed the ambiguity it could not express. visual-workspace-flow.spec.ts still matches the identifier rather than the rendered title and is still failing on that string |
tests/e2e/first-run.spec.ts has an intermittent startup hang | On two consecutive runs of the same command on an idle host, one passed in 5.1s and one hung for 2m38s with an unresponsive renderer, failing on the first screenshot. The configuration fix is verified; the spec’s stability is not, on one green run |
tests/e2e/first-run-vault/.obsidian/plugins/ is untracked build residue and is not gitignored | A clean run deletes it, a terminated one strands it. Nothing under that subdirectory is tracked, so ignoring it untracks nothing |
reference/settings.mdx says the index file lives “relative to the vault root” | It does not. It lives in browser-managed storage outside the vault. The settings UI string says the same thing. A copy fix on both surfaces, not scheduled |
Where this went next (added 2026-09-04)
Section titled “Where this went next (added 2026-09-04)”Nothing above is amended. The arc continued past the ninth attempt recorded here and reached a narrowed closure of Part A on 2026-09-04, at the twenty-third attempt: every check that was actually run passes, the seven quarantined visual walkthroughs still fail, four large-corpus walkthroughs never ran, and the branch is unmerged. See Part A of the per-import-folder work closes on a narrowed bar for the measurement, what changed for a user, and what is still open.
Related
Section titled “Related”- Part A closes on a narrowed bar — where this arc landed, and what it does not claim
- Re-import is identity reconciliation — the first path-inference defect
- Import-set ownership — the second, and where “paths are guesses, not facts” was first written down
- Closure cache fix, and what it means — the same absence-read-as-fine family, in the query layer
- Challenge 44 E2E testing strategy synthesis — the seed vault whose limits this session found
- Challenge 45 org-rollout readiness — flagged the unproven purge boundary before it was fixed
- Org-rollout readiness synthesis — what was adopted from that audit
- Quick start and the command reference — the two living pages re-verified here
- v0.1.5 Tier 2 sidecar milestone — where the reset command shipped, with a checkbox asserting a deletion that did not happen
CHANGELOG.md[Unreleased]— the delivery detail for the destination fix, in user-facing language- UI lexicon — the command-naming rules the palette regrouping satisfies
- v0.1 milestone track — where explicit relocation, the additive migration path this reframing makes possible, has to land as its own piece of work