From c760354f7cc32622bd6312084442e5ea6769eb3a Mon Sep 17 00:00:00 2001 From: Andrew Mikofalvy <5668128+amikofalvy@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:11:13 +0000 Subject: [PATCH] Fix false missing-file links: NFC/NFD and more (PRD-8896) (#5691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add target identity and namespace primitives to core PRD-8896: every link surface compares target spellings by raw bytes, so an NFC href to an NFD file on disk reads as dead, the graph splits one document into two nodes, and macOS and Linux disagree. This adds the one rule and the one structure the surfaces will resolve through. TARGET_IDENTITY is the single-point policy table: NFC for every kind, case-sensitive for documents and folders, case-insensitive for the leaf segment of files. The unicode field admits only the literal NFC, so NFKC folding is unrepresentable. identityKey applies the folder rule to ancestor segments and the kind rule to the leaf, which is byte-identical to how the cloud catalog resolves folders and asset leaves. TargetNamespace keeps an exact Set of raw names plus identity buckets sorted by code point. resolve returns the raw existing name: an exact spelling always wins, an equivalent spelling lands on its twin, and a third spelling of two Linux twins is ambiguous but still names the lowest-code-point candidate deterministically. The namespace satisfies ReadonlySet with exact membership so the existing pages: ReadonlySet declarations keep compiling; resolveName resolves through a namespace and exactly through a plain set. dependencySlug is the invalidation key for incremental link graphs: the wiki slug, falling back to the file identity key when the slug is empty. It is property-tested to be coarser than every identity key, so any add or delete of an equivalent spelling reaches the sources that depend on it. Both modules ship as core subpaths for the app's import rule. * Pin the dead-link false positive across NFC and NFD spellings PRD-8896: on macOS the file watcher hands the graph NFD document names while authors and git write NFC hrefs. The graph compares the two by raw bytes, so a markdown link to an existing document is reported dead, its backlink lands on a phantom NFC node, and the editor paints it red. These tests pin the behaviour the next commit restores: an NFC markdown link to an NFD document is not dead and its backlink lands on the real node, the link goes dead when the document is deleted and heals when it is re-added or appears later, and the NFD-to-NFC direction behaves the same way. A seeded property test over an alphabet of NFC, NFD, ASCII and case-variant names checks that an incrementally maintained graph equals a fresh rebuild after random adds and deletes, which is what guards the invalidation bookkeeping the fix adds for markdown sources. * Resolve link targets by identity in the graph and the wiki resolver PRD-8896: the dead-link checker, the backlink graph and the wiki-link resolver all asked a raw Set whether a target spelling exists. On macOS the watcher reports NFD names while authors and git write NFC, so a markdown link to an existing document was reported dead and its backlink sat on a phantom node spelled like the href. The wiki resolver's exact tiers now resolve through resolveName, which answers by identity when given a target namespace and exactly otherwise, and returns the raw existing page name. Identity runs before the slug bucket, so a wiki link to Rene with an accent lands on that page rather than on an accent-stripped sibling the slug winner would have picked. Asset lookups compare NFC and leaf case per segment instead of lowercasing the whole path, so ancestor folder case is now significant for wiki asset targets, matching the cloud catalog, Linux and the generated indexes. The server's project lookup builds a document namespace for pages. In the graph, the cached document lookup keeps a mutable namespace in step with inventory changes. resolveSource canonicalizes markdown and jsx targets through it, so an NFC href lands on the NFD document's node, and registers those sources under dependencySlug in the same bucket the wiki sources use, so a document that appears, disappears or gains a twin later re-resolves them. getDeadLinks and computeBrokenOutboundLinks answer existence through document and folder namespaces. The two key-registration loops become typed helpers because the path buckets are now keyed by the branded slug while the folder bucket stays raw. * Resolve local link targets by identity in the problems plane PRD-8896. The local-target inventory used exact Set membership, so a link spelled in NFC to a document, file or folder stored in NFD (the spelling macOS reports) was assessed as missing and surfaced as a dead-link finding in the validation audit and in write advisories. The inventory now answers through target namespaces and returns the existing raw name, which becomes the assessment's resolvedTarget. Reverse dependency maps are keyed by identity key so a create, delete or watcher event in either spelling reaches the sources that depend on it. The periodic disk sweep keeps statting the raw paths each source depends on rather than the folded keys, which are not disk paths on a case-sensitive filesystem. Behavioral pins: index heal/break in both directions for documents, files and folders, a rebuild plus disk sweep over NFD and mixed-case file names, the audit path producing no finding, and write advisories staying quiet. * Resolve by identity in backlinks, renames, the file oracle and generated indexes PRD-8896. Four more server surfaces compared link targets by exact string, so a spelling that differs only in Unicode normalization from the stored name missed: - getBacklinks and getBacklinkCount now canonicalize the requested docName through the document namespace, so a backlinks query spelled in NFC answers for the NFD document the graph indexed. - The graph's file oracle and the agent-write linked-file probe answer through a file namespace instead of exact Set membership. - Managed renames compare each markdown link and JSX src-ref against the renamed document through the before-rename lookup, so a link spelled in NFC follows an NFD document that moves. Callers without a lookup keep the exact comparison. - Generated folder indexes emit each href in NFC when that spelling resolves back to the same sibling, and keep the raw spelling when a distinct sibling already owns the composed form. Rows are never deduplicated. * Resolve editor link and navigation targets by identity PRD-8896. The editor's page list now publishes document, folder and file namespaces in each snapshot, so link decoration and navigation resolve a target spelled in NFC to a page or folder stored in NFD and navigate to the stored name. The whole-path case-insensitive asset match is replaced by the file rule: the file name stays case-insensitive, parent folder names are now case-sensitive. Plain Set inventories keep exact matching, so callers that never publish a namespace are unchanged. The two link dialogs accept ReadonlySet inputs since they only read the inventories. Changeset notes the normalization fix and the folder-case change. * Pin angle-bracket link destinations to the bare url for PRD-8896 A markdown link whose destination is written in angle brackets and has no whitespace, such as `[René]()`, parses with the `<` and `>` still inside the url, so the editor renders a broken link and "Create page" mints a document literally named ``. The same shape breaks `![alt](<...>)` images and `[label]: <...>` reference definitions. These tests pin the CommonMark contract: the url is the text between the brackets, backslash-escaped `\>` and `\<` are part of it, the source still round-trips byte for byte, and every mid-typing or resource-failing shape stays guarded so remark-mdx never sees a live `<` it cannot parse. They fail against the current guard, which only exempts a destination that contains whitespace. * Parse angle-bracket destinations without whitespace for PRD-8896 `protectFromMdx` guards every `<` that could crash remark-mdx with a private-use sentinel before micromark runs. Its one exemption for the `<` opening a `[text]()` destination only fired when the destination contained whitespace, and it ran in the final catch-all pass, after the autolink, HTML-close-tag and lowercase-tag passes had already rewritten shapes like ``, `` and ``. Every no-space destination therefore reached micromark as a plain destination full of sentinels, and `restoreFromMdx` put the literal `<` and `>` back into the url. The guard now indexes every angle destination that provably forms a CommonMark link, image or reference definition before any pass runs, and every pass leaves those two bytes alone. The scanner mirrors micromark's enclosed-destination tokenizer (backslash escapes `<`, `>` and `\`; no line ending or unescaped `<`), checks the rest of the resource (an immediate `)`, or a space, a quoted or parenthesised title and `)`), and for definitions requires the `[label]:` line to start a content block (document start, a blank line before it, or the line after an accepted definition) with the destination on the same line, since a live `<` at a line start is an MDX JSX-flow interrupt. Every rejection keeps today's guarded, byte-stable behaviour. The FR-14 backslash guard rewrites `\<` to `U+E102<` first, so the scanner reads that pair as the escape and force-guards the inner `<` in every pass; without it the serializer's `\<` could not be read back and CommonMark examples 201 and 494 lost idempotence. The index also keeps exempt or forced `<` out of the paired-tag opener and closer counts and out of the uppercase close-tag index, which stops `x [a]()` from crashing. Serialization: links, images and definitions share one destination emitter. A `sourceUrlForm` of `angle-bracketed` re-wraps the bare url and escapes `<`, `>` and the backslashes micromark would read as escapes, so `[c](b>)` and `[c]()` round-trip byte for byte. Images and `linkRefDef` nodes now carry `sourceUrlForm` (position-slice reads it from the source, index.ts threads it both ways) because their urls no longer keep the brackets. The bracket-preserving branch stays: a guard-rejected shape such as a nested-bracket label still forms a link through the sentinel path with a bracketed url, and wrapping it again would corrupt the source. Schema snapshot and the md-audit anchor catalog are regenerated for the two additive attrs and the moved `@floor` line. * Cover the page list's identity namespaces with a DOM test for PRD-8896 The page list provider is what turns server inventories into identity namespaces for link decoration and navigation, and no test failed when that construction was reverted. This renders the real provider over an NFD-named document and folder and checks that NFC links resolve and navigate to the stored names. * Pin basename wiki embeds resolving outside doc folders (PRD-8896) Failing tests for the first fix of PRD-8896. A wiki embed such as ![[photo.png]] is resolved by the server's basename index. That index is seeded and maintained through the content filter's sibling-asset rule, so a file in a folder that holds no markdown document is never indexed and the embed renders red even though every other inventory knows the file. The index also keys buckets by lowercase only, so an NFC spelling never finds an NFD-named file. The tests expect the seed walk and the watcher to admit assets in folders without a document, and the index to match canonically equivalent spellings. * Resolve basename wiki embeds in folders without a document (PRD-8896) Seed and maintain the basename index with the content filter's isPathIgnored admission, the same gate the watcher's general-file walk, the documents listing and the local-target index already use, instead of the sibling-asset rule that admits an asset only when its folder holds a markdown document. Key the index by the file identity rule (NFC, case-insensitive) so an NFC spelling resolves an NFD-named file. The embed resolver was the only inventory still bound to the sibling rule, which is why ![[photo.png]] rendered red whenever the file lived in a folder with no document while [[photo.png]] resolved. * Pin missing wiki asset targets reaching the Problems pane (PRD-8896) Failing tests for the second fix of PRD-8896. A wiki link or embed to a non-markdown file, [[x.png]] or ![[x.png]], is classified by the local-target plane but never projected onto its surfaces, so a target that does not exist is red in the editor and silent in /api/audit, the Problems pane and /api/forward-links, while the markdown form of the same missing file is reported. The tests expect file-shaped wiki occurrences to project with a wiki-link or wiki-embed source form, resolve against the vault root, fall back to the vault-wide basename rule the embed renderer uses, and heal through the index as files come and go. Document-shaped wiki links stay graph edges and stay out. The wire unions gain the two wiki source forms and a basename resolution method, and the inventory gains an optional basename resolver; both are type-level only here so the branch keeps compiling. * Report missing wiki asset targets in the Problems pane (PRD-8896) Project file-shaped wiki occurrences, [[x.png]] and ![[x.png]], onto the local-target surfaces: the Links panel's local files, the localTargets rows of /api/forward-links and the dead-link diagnostics of /api/audit, which is what the Problems pane shows. Document-shaped wiki links stay graph edges and stay out, so nothing is reported twice. A wiki asset target resolves against the vault root, never the source folder, and a bare basename falls back to the vault-wide nearest-file rule the embed renderer uses, through a basename index the local-target index keeps beside its file namespace. A missing basename embed records a reverse edge keyed by the file basename, so it heals when a matching file appears anywhere and breaks when it goes. Surfaces whose inventory has no basename oracle (agent-write advisories, the extension-twin physical scope) report path-form wiki targets and skip basename-form ones rather than guess. * Pin the browser URL a Cmd-click hands the dispatcher for non-raw files (PRD-8896) A Cmd-click on a linked file that the server does not stream raw, such as a .py script or an extensionless Makefile, must give the asset dispatcher the in-app asset route as its browser fallback URL. Today it passes the raw href, which the server refuses by design, so the new tab shows a 404 or the empty app shell instead of the file. This test fails until the helper is fixed. * Open non-raw files in the app tab on Cmd-click instead of a dead URL (PRD-8896) The server streams a content file raw only when its extension is in ASSET_EXTENSIONS (the allowlist #765 made explicit, with extensionless and unknown extensions falling through to the SPA shell or a 404 on purpose). The editor's Cmd-click path ignored that and handed the raw href to the browser fallback, so a linked .py script or an extensionless Makefile opened a tab with a 404 or an empty app shell. Hand the dispatcher the in-app asset route for those files instead; raw-servable files keep their raw URL and the desktop OS hand-off is unchanged. * Pin that a link to a tracked extensionless file opens the file (PRD-8896) A markdown link such as [makefile](docs/Makefile) classifies as a page link because the path has no extension, yet the editor paints it resolved because the file exists in the project. Clicking it today returns false: a plain click only opens the link popover and a Cmd-click does nothing. These tests pin that such a link opens the file preview, in place or in a new app tab, while a missing path still falls through to create and a real page still opens. * Open a linked extensionless file instead of treating it as a missing page (PRD-8896) classifyMarkdownHref reads an extensionless href such as docs/Makefile as a page link, so the click handler asked the page index, found no page, and returned false: a plain click opened the link popover and a Cmd-click did nothing, even though the editor paints the link resolved because the file exists. Mirror the resolution-state fallback in the click handler: when the page intent is create but the href names a tracked file, activate it as an asset so it opens in the file preview, in place or in a new app tab. * Pin an excluded reason for ignored link targets (PRD-8896) A link to a file that exists on disk but is excluded by .gitignore or .okignore is reported as no-such-file on every surface: the Problems pane and /api/audit, the agent write brokenLinks advisory, and the forward-links panel. The sidebar shows the same file, so the message tells the user something false and gives them nothing to act on. These tests pin the honest shape: an excluded reason next to no-such-doc, no-such-file and unresolvable, carried by the local-target assessment, the write advisory, the audit message, the Problems pane copy and the Links panel badge, with genuinely missing files still reported as no-such-file. They fail until the fix lands. * Report ignored link targets as excluded instead of missing (PRD-8896) A link to a file that exists on disk but is kept out of the project by .gitignore or .okignore used to be reported as no-such-file: Link target "ignored/ig.png" does not resolve to an existing file. The sidebar lists the same file, so the message was false and offered no remedy. The target stays refused; precedent #55 and the serve-side refusal from 69f9c23 are unchanged. Only the diagnosis is corrected. A new brokenLinks reason, excluded, sits next to no-such-doc, no-such-file and unresolvable. It is minted only on a miss, from the existing ContentFilter predicate (isPathIgnored) plus one stat of the target. Built-in exclusions a "!" rule cannot lift (.git, node_modules, secret-bearing files) keep reporting no-such-file, because a re-include hint would be wrong for them. Surfaces: the local-target assessment behind /api/audit, the Problems pane and /api/forward-links; the graph-plane broken-link scan behind the agent write, patch, batch and frontmatter advisories; the MCP brokenLinks description; the Problems pane and Links panel copy (five new strings, translated in every locale); the docs and the project skill's linking reference. /api/dead-links carries doc edges only and needs no change. Known limits: a doc link into an ignored folder still reports no-such-doc, and a file created under an ignored folder after its link was indexed is reassessed only when the source document changes. * Pin the excluded reason for wiki asset targets (PRD-8896) A path-form wiki embed or wiki link to a file that exists but is excluded by ignore rules reports no-such-file, the same false message the markdown forms used to give. * Report excluded wiki asset targets as excluded (PRD-8896) * Pin asset serving through tracked-file identity (PRD-8896) On Linux a file name's bytes are its identity, so an image linked by its NFC spelling, or with a different leaf case, 404s when the file on disk is NFD or differently cased, even though the editor and the Problems pane resolve the link to that file. These tests pin the fallback: when the exact path misses, the content middleware and /api/asset serve the tracked file the server's file namespace names, typed and dispositioned by the resolved file, and never an ignored, escaping or private-state target. * Serve linked assets through tracked-file identity on Linux (PRD-8896) The editor and the Problems pane resolve an asset link by file identity (NFC, with a case-insensitive file name), but serving stat'ed the exact requested bytes. On Linux an NFC link to an NFD-named image therefore showed as resolved while the image 404ed, and so did a link whose file name differed only in case. When the exact path misses, the content middleware and /api/asset now resolve the requested content-relative path through the server's tracked-file namespace, built from the watcher inventory and cached per index generation, and serve that raw file. Parent folders stay case-sensitive, matching the editor's markdown rule, so a wrong-case folder still 404s where the editor shows it red. Only tracked, non-ignored files with a servable asset extension are considered. The resolved path passes the same containment and private-state checks as an exact hit, and the disposition, CSP and content type are taken from the resolved file. The SPA fall-through still sees the requested URL. * Pin case-insensitive wiki asset folders again (PRD-8896) Failing tests for the wiki asset folder-case regression. Before this branch a wiki link or embed to a file matched the file by case across the whole path, so [[pics/deep/x.png]] found Pics/Deep/x.png. The identity change made ancestor folders case-sensitive for wiki targets, so on a case-sensitive file system the editor turns such a link red and the Problems pane and agent-write advisories report it missing. The tests expect the wiki resolver, the indexed asset keys, the local-target index and the write advisories to match a wiki asset by NFC and by case across the whole path, to prefer an identity match over a folder-case match, to pick one deterministic winner among folder-case twins, and to heal and break as the file comes and goes. Markdown asset links keep ancestor folder case significant. * Match wiki asset targets by case across the whole path again (PRD-8896) A wiki link or embed to a file matched case-insensitively across the whole path before this branch; the identity change kept that for the file name only, so on Linux [[pics/deep/x.png]] to Pics/Deep/x.png turned red in the editor and showed up in the Problems pane and in agent-write advisories. Wiki asset targets now resolve by identity first, then by NFC and case across the whole path, with the lowest code-point spelling winning among folder-case twins. Core gains wikiAssetPathKey and a prebuilt createWikiAssetResolver that agrees with the per-call scan. The local-target inventory gains an optional resolveWikiFile, which the index keeps beside its file namespace and the agent-write routes build lazily from the tracked-file index, and the index keys a path-form wiki file dependency by the folded path so the target heals and breaks as files come and go. Markdown asset links keep ancestor folder case significant. * Pin image existence to the branch's file identity rule (PRD-8896) The image existence test still expected a wrong-folder-case src to exist and passed plain Sets, which resolve exactly, so it has failed since the editor moved to identity namespaces. It now builds the inventory as a file namespace, the way the page list does, and asserts the markdown asset contract: a file-name case mismatch exists, a parent folder case mismatch is missing, and NFC and NFD spellings exist both ways. * Pin physical-scope file and folder targets to the tracked inventory (PRD-8896) Failing test for the physical audit scope, the path /api/audit takes for a file with a same-stem .md or .mdx sibling. It checks file and folder targets with exact stat calls behind a freshly built content filter, which admits a non-markdown file only when its folder holds a markdown document. A link to an existing image in a folder of images is therefore reported missing even with the exact spelling, on macOS and Linux alike, while the index path, which reads the watcher's tracked files, reports nothing. On Linux the exact stat also misses NFC and NFD spellings of folders. The test audits the same links once through the physical scope and once through the index path and expects the same findings: only the folder-case file and folder links are dead, and a folder-case wiki embed resolves as wiki asset targets do. * Resolve physical-scope file and folder targets through the tracked inventory (PRD-8896) The physical audit scope now checks file and folder targets against the tracked-file inventory the rest of the problems plane uses: files through a file namespace, folders through a folder namespace of tracked folders and document ancestors, and wiki asset targets through the wiki resolver. That removes the false missing findings for existing files in folders without a markdown document, which the scope's fresh content filter excluded, and for NFC and NFD spellings on Linux, and makes the scope report exactly what the index path reports for the same links. Both audit entry points pass the inventory; without one the scope keeps its stat checks. * Pin path-form wiki embeds to the file they resolve to (PRD-8896) Failing test for wiki embeds such as ![[pics/deep/x.png]]. The server resolves an embed target through its basename index only, so a target with a folder never resolves and the embed renders with the literal spelling as its src. The editor's image existence check applies the markdown rule to that src, so a folder-case target shows "Image not found" while the same wiki link is blue and opens the file, and on Linux any other spelling requests a path that is not on disk. The test expects the server to resolve a path-form embed through the tracked files by the wiki asset rule, NFC and case across the whole path, and the parsed embed to carry the resolved raw path as its src. * Render path-form wiki embeds from the file they resolve to (PRD-8896) The server's embed resolver now resolves a wiki embed target that has a folder through the tracked files by the wiki asset rule, identity first and then NFC and case across the whole path, and keeps the basename index for bare file names. The parsed embed carries the resolved raw path as its src, so ![[pics/deep/x.png]] shows Pics/Deep/x.png instead of "Image not found", and on Linux the browser requests the path that is on disk. The tracked-file resolvers share one per-inventory cache, and attachment sizes look up the same resolved path. * Pin the excluded reason in the physical audit scope (PRD-8896) Failing test for the remaining gap between the two audit paths. The index path reports a link to a file that exists but is ignored as excluded, with the .okignore remedy; the physical scope, taken for a file with a same-stem .md or .mdx sibling, has no exclusion probe and reports the same link as no-such-file. The app integration test "physical sibling and indexed sources agree on link target admission" fails for the same reason. The test expects both paths to give the same reasons. * Report ignored targets as excluded in the physical audit scope (PRD-8896) The physical audit scope now gives its inventory the same on-disk exclusion probe the local-target index uses, so a link to a file that exists but is ignored reads as excluded, with the .okignore remedy, on both audit paths. This also turns the app integration test "physical sibling and indexed sources agree on link target admission" green. * Register the linked local-target inventory with the file-index caller guard (PRD-8896) The wiki file resolver for agent-write advisories and the tracked inventory for the physical audit scope each read getAllFilesIndex() directly, which the getAllFilesIndex caller coverage meta test rejects because neither site was allowlisted. Both now go through one helper, linkedLocalTargetInventory, which hands the index to localTargetInventoryFromIndexes. That function splits entries on their kind, the same reason createLinkedFileExists is allowlisted, so the helper joins the allowlist. * Remove the wiki file basename helper left without callers (PRD-8896) The wiki asset fix made the target index call wikiFilePath directly, so knip flagged wikiFileBasename as an unused export in check:drift:guards. * Pin no-such-file for links under built-in skip folders (PRD-8896) A file under build/, output/ and the other built-in skip folders is reported as excluded with a .okignore remedy that cannot re-include it. * Report excluded only when an ignore file is the cause (PRD-8896) Files under built-in skip folders such as build/ and output/ are rejected before .gitignore and .okignore are read, so the .okignore remedy cannot re-include them. They keep reporting no-such-file. * Drop prose comments from the angle-destination tests (PRD-8896) * Pin a shared dependency slug for lunate sigma case variants (PRD-8896) * Fold case before slugging dependency keys (PRD-8896) Lowercasing after NFKD is context-sensitive for final sigma, so two file spellings with one identity could land in different dependency buckets. Slugging the file identity key keeps them together. * Pin definition output for empty and line-ending hrefs (PRD-8896) A linkRefDef with an empty href serializes as `[ref]: `, which no longer parses as a definition, and a line ending inside the href splits the definition in two. The base forced angle form for empty urls and encoded line endings as character references. * Force angle form for empty definition urls and encode line endings (PRD-8896) The definition handler again emits <> for an empty url, and the angle destination formatter writes line endings as character references so a synthesized href cannot break the definition across lines. * Pin self-closing JSX whose single-quoted attribute holds an angle destination (PRD-8896) The self-closing scan stops at the now-unguarded > of the destination inside a single-quoted attribute and flattens the tag to text. The base guarded that > and kept the JSX element. * Skip angle destination closers in the self-closing JSX scan (PRD-8896) A reserved destination > was a guard sentinel before angle destinations stayed live, so the uppercase self-closing scan never stopped on it. It now skips those offsets and keeps the base tag classification. * Pin definition-shaped lines after U+2028 as guarded paragraph text (PRD-8896) The definition index matches line starts with a multiline regex, whose ^ also matches after U+2028. micromark does not break lines there, so the destination < stays live in paragraph text and remark-mdx throws. * Match definition line starts only after CR or LF (PRD-8896) The multiline ^ also matches after U+2028 and U+2029, which micromark treats as ordinary characters. A lookbehind on CR and LF keeps the definition index aligned with the lines micromark actually splits. * Pin angle destinations that a GFM table row splits (PRD-8896) An unescaped pipe inside a link, or a definition line followed by a delimiter row, puts the reserved destination < in a table cell where no link or definition forms, and remark-mdx throws on the live tag. The base guarded those brackets and never threw. * Leave angle destinations unreserved where a GFM table splits them (PRD-8896) An inline link with an unescaped pipe between its label and closing paren, and a definition whose next line is a table delimiter row, can land in a table cell where the link or definition never forms. Those destinations keep their brackets guarded, as the base did. * Pin escaped pipes in table-cell angle destinations (PRD-8896) A table-cell link written as [l]() now parses as a link, but the angle formatter drops the backslash and the next parse splits the cell. The base kept that source as text and round-tripped it unchanged. * Escape pipes in angle destinations written inside table cells (PRD-8896) The angle formatter escapes | and any backslash before it when the serializer is inside a table cell, so the destination stays in one cell. * Pin angle destinations holding escapes the serializer cannot reproduce (PRD-8896) A destination such as now parses as a link whose url drops the backslash, so serialization writes . The base kept the spaced form as text and left the unspaced form as a bracketed url, and an escaped backslash before punctuation lost one backslash on both. * Reserve only angle destinations whose escapes serialize back exactly (PRD-8896) The destination index refuses a backslash escape of any ASCII punctuation other than <, > and backslash, so those destinations stay guarded as on the base. The angle formatter doubles a backslash before any ASCII punctuation, so an escaped backslash survives the round trip. * Pin escaped backslashes before ordinary characters in angle destinations (PRD-8896) Both a\\b and a\b decode to the same url, so the serializer writes the single-backslash form and [l]() loses a backslash. The base kept that source as text. * Refuse escaped backslashes the angle formatter cannot reproduce (PRD-8896) An escaped backslash followed by an ordinary character serializes back as a single backslash, so such destinations stay guarded as on the base. * Regenerate the NG anchor catalog after the test comment cleanup (PRD-8896) * Pin tracked asset names holding ? or # against prefix serving (PRD-8896) A tracked secret?.png next to an ignored extensionless secret must not serve secret through the content route. The /api/asset case resolves file paths directly and stays green. * Refuse tracked asset fallbacks whose URL would look up another file (PRD-8896) The rewritten URL must decode back to exactly the tracked path the ignore and extension checks approved. encodeURI leaves ? and # literal, so a tracked secret?.png rewrote to a URL that served the ignored secret. Names that cannot round-trip now 404. * Pin refusal of tracked asset names no URL can spell (PRD-8896) A lone surrogate made encodeURI throw inside the middleware before the exact-lookup check; it now 404s. Verified red against the previous middleware. * Pin folder-index wiki links across NFC and NFD spellings without a rebuild (PRD-8896) Adding or deleting a folder index or folder note spelled in the other normalization form must re-resolve the wiki links that name its folder, on the incremental path. * Add folder-index spellings to the incremental-equals-rebuild property (PRD-8896) Seed 11 diverges from a fresh rebuild on the current folder dependency keys. * Key folder-index wiki dependencies by folder identity (PRD-8896) Wiki sources were registered under the raw target spelling and looked up with the raw parent folder, so a folder index or folder note added or deleted in the other normalization form never re-resolved the links naming its folder. Both sides now use identityKey('folder'), and the folder-note leaf test compares document keys. * Restore the angle-destination crash-safety rows (PRD-8896) Commit 3691e1ab65 collapsed the mustStayGuarded table, its companion tests and the self-closing describe header into one literal-\n comment line, so none of them ran; da688d18fa then deleted that line. This restores every `](<` and `]: <` shape that still forms no resource, each asserting the destination < stays guarded, the parse does not throw and the bytes round-trip. Shapes micromark still links after the guard rejects them (title without a space, nested paren title, nested label brackets, a four-space indented definition) assert one-pass idempotence, since their bytes were already canonicalized on main. New rows cover a label spanning a line ending and label and title scan caps. Discrimination, each rejection loosened locally (not committed) and the file run: odd backslashes before ], label scan floor, label line ending, label backtick, closed ] before the label, ^ label, ][ label, missing label start, destination line ending, destination scan ceiling, title without a space, anything after the destination, title scan ceiling, unescaped ( in a paren title, ANGLE_DEST_SCAN_CAP raised, whitespace before ), and the definition paragraph-interrupt, ^ label, empty label, junk after destination or title, next-line destination, blockquote and four-space indent rejections each turn at least one restored row red. Allowing < inside the destination is caught by the surviving "an unescaped < inside the destination keeps the < guarded" test. Accepting an unclosed destination at end of input is an equivalent mutant: resourceEnd rejects the same shapes. Without the guarded-< assertion, no-throw and byte-stable alone miss ten of these loosenings, because parse recovery re-guards a crashing tag. Retired rows, kind "asserts something the product no longer promises": - `[a]( y>)` "escaped > inside destination": a backslash-escaped > now stays inside an angle destination and the link forms (CommonMark backslash escapes). Surviving assertion: "backslash-escaped > inside the destination is part of the url". - `[link]( "t")` "title after destination", also dropped from the no-link list: titled angle destinations now form. Surviving assertion: "space-bearing destination with a title forms a link and round-trips". * Pin tabs opened from differently spelled asset links across a page-list refresh (PRD-8896) Activation must open the stored spelling the identity match returned, and the known-target sync must keep a tab whose path is identity-equal to a tracked file. The asset-link case targets resolveStoredAssetActivation, which the fix adds. * Open tracked-file tabs under the stored spelling and keep them on refresh (PRD-8896) Activation built the tab id from the link's own spelling after an identity match, and the known-target sync pruned asset and folder tabs with an exact Set.has, so a link made clickable by the identity rule opened a tab the next page-list refresh closed. resolveAssetHrefPath now returns the stored path that both activation paths open, and the sync checks membership through resolveName. * Pin write advisories for NFD links to an NFC folder holding no document (PRD-8896) * Match linked folders for write advisories by folder identity (PRD-8896) createLinkedFolderExists used an exact Map.has, so an NFD link to an NFC folder holding no document landed in agent-write brokenLinks while the Problems pane resolved it. * Pin a verdict for slash-free wiki asset embeds on every assessment path (PRD-8896) Agent-write advisories and the physical-scope audit skip ![[ghost.png]] silently, while the project-scope audit reports it as a dead link. * Give every local-target inventory a basename resolver (PRD-8896) assessWikiFile returned no verdict for a slash-free wiki asset when the inventory had no basename resolver, which only LocalTargetIndex supplied. The resolver is now required: agent-write advisories and the physical-scope audit build one from the tracked file inventory, and an inventory with no file list reports the target missing. * Declare minor bumps for the additive link-target API (PRD-8896) The excluded broken-link reason and the new core target-identity and target-namespace subpath exports are additive API surface, which the OK pre-1.0 table maps to minor. * Document the link identity rule and asset folder case (PRD-8896) Review links after upgrading now states which names match case-sensitively, that NFC and NFD spellings are one name, the tie-break, and why a Markdown asset link with mismatched folder case can turn broken while a wiki embed still resolves. * Pass write-advisory target capabilities as one named object (PRD-8896) computeWriteAdvisoryLinks took its optional oracles positionally, so call sites passed literal undefined between adjacent (path) => ... callbacks that could be swapped without a type error. Behavior is unchanged. * Name the angle-destination escape tests as current, believed-incomplete behavior (PRD-8896) These pin outcomes that differ from CommonMark: a spaced destination with an unreproducible escape stays text, and an unspaced one keeps its brackets in the url. The names now say so, so a future fix does not read as breaking a contract. Assertions are unchanged. No tracker exists yet for the limitation. * Define the angle-destination escape rule once (PRD-8896) The guard decided which escapes inside <...> a round trip reproduces, and formatAngleDestination decided which escapes it writes. Each kept its own ASCII punctuation regex and its own idea of when a backslash is doubled, so the two could drift apart and turn a guarded shape into one the serializer rewrites. Both now read the same predicates from angle-destination-escapes.ts: which brackets the serializer escapes, when it doubles a backslash, and which escapes therefore survive a round trip. Behavior is unchanged; the core markdown suite (3188 tests) stays green. * Fill folder namespaces from document names in one place (PRD-8896) The backlink index, write advisories, the physical-scope audit and the local-target index each walked every document name's slashes to build the folders that hold a document. The copies differed in loop shape and one built a plain set first, so a change to how folders are derived had to land four times. Core now exports addDocumentFolders, which adds each document's ancestor folders to a folder namespace, and all four call it. Behavior is unchanged; the folder-namespace test pins the helper and the server link suites stay green. * Define the wiki asset fold and its tie-break beside the identity rule (PRD-8896) Wiki embeds match an asset's whole path case-insensitively, and when several stored paths fold together the lowest UTF-16 spelling wins. That rule lived in wiki-link-resolve.ts twice, as a linear scan and as a prebuilt map, each comparing with its own `<`, while the namespace buckets and the slug indexes carried two more private copies of the same comparison. target-identity.ts now owns wikiAssetPathKey and compareSpellings next to TARGET_IDENTITY. The namespace, the slug and basename indexes and the asset resolver all use compareSpellings, and the scan is gone: resolveWikiLinkAssetTarget asks createWikiAssetResolver, which folds lazily on the first miss so an exact hit still costs no scan. The test that compared the prebuilt resolver against the scan now pins what that comparison guaranteed: the answer ignores input order and prefers the lowest folded twin. New tests pin the whole-path fold and the code-unit order. Behavior is unchanged. * Pin bare destinations that start with a guarded < as byte-stable (PRD-8896) While a reference definition is being typed, `[a]:

, so the guard hides the < and the parser reads a bare destination whose url begins with <. Main writes those bytes back unchanged. This branch marks every definition destination that starts with < as angle-bracketed, so the same line comes back as `[a]: <\

`. Inline links had the same shape and already rewrote it on main, as `[l](<

)`. The new rows fail on this branch: four definition shapes and three link shapes. Four enclosed destinations whose url really does start with < (an escaped < or a character reference) keep their angle form and pass today; they pin that a fix must not drop it. * Keep the angle form only for destinations the parser read as enclosed (PRD-8896) Position-slice marked a link, image or definition destination as angle-bracketed whenever its source began with <. When the guard has hidden that < because nothing closes it, the parser reads a bare destination and the url itself starts with <, so the serializer wrapped it again and escaped the <: `[a]:

`. An enclosed destination's url can start with < only through an escaped \< right after the opener; a character reference stays literal in the url. So the angle form is now kept when the url does not start with <, or when the opener is followed by a backslash. Bare destinations are written back bare, which the guard hides again on the next parse. The seven rows pinned in the previous commit now pass, including the three link shapes main already rewrote. Removing the backslash case turns the two escaped-< rows red. * Test that dotted bare wiki names naming a document are not missing files (PRD-8896) [[acp.daemon]] names the document vault/acp.daemon. Since every inventory gained a basename resolver, the wiki asset path reports it as a missing file even though the editor opens the document. * Leave dotted bare wiki names that name a document to the graph (PRD-8896) A missing wiki asset whose name resolves to a document by bare-name rules is a document link, the same promotion the editor applies. The asset assessment now yields no verdict for it, so [[acp.daemon]] is no longer reported as a missing file. * Pin backslash-led bare destinations behind a guarded < as byte-stable (PRD-8896) When nothing closes a destination's <, the guard hides it and the parser reads a bare destination whose url starts with <. Position-slice still marks it angle-bracketed when a backslash follows the <, so the serializer wraps and escapes it: `[l](<\x)` comes back as `[l](<\<\x>)`, and `[l](<\\server\share)` and `[a]: <\