Skip to content

v0.84.0

@amikofalvy amikofalvy tagged this 06 Oct 17:53
* 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<string> with exact membership so the existing
pages: ReadonlySet<string> 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é](</People/Ren%C3%A9.md>)`, parses with
the `<` and `>` still inside the url, so the editor renders a broken
link and "Create page" mints a document literally named
`</People/René.md>`. 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](<dest>)` 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 `<https://x>`, `</div>` and `<foo.md>`.
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
`<mark>x [a](</mark>)` 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](<a\>b>)` and `[c](<a\<b>)` 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](<x\|y z>) 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 <a\*b c> now parses as a link whose url drops the
backslash, so serialization writes <a*b c>. 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](<a\\b c>) 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](</x \> 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](</my uri> "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]: </p/x.md` has no
closing >, 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]: <\</p/x.md>`. Inline links had the same shape and already
rewrote it on main, as `[l](<</p/x.md>)`.

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]: </p/x.md` came back as
`[a]: <\</p/x.md>`.

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]: <\<x` are rewritten the same way.

Nine link, image and definition rows with a backslash after the guarded
< must be byte-stable, and six rows where that backslash starts an
escape must come back bare. A bare destination does not need those
escapes and drops them, so those six are labelled current, believed
incomplete behaviour. All fifteen fail on the current predicate.

Image rows without a backslash, and an enclosed image destination whose
url starts with <, pass today and cover the image call site, which no
row reached before.

* Decide the angle form from the opener the parser consumed (PRD-8896)

Plain CommonMark never reads a destination that starts with < as bare,
so a bare destination starting with < exists only when the guard has
hidden that opener. The guard's restore pass now records it on the link,
image or definition whose url starts with its sentinel, and
position-slice keeps the angle form only for a < the parser consumed as
an opener. The previous predicate guessed from the url and the character
after the <, which misread `[l](<\x)` as enclosed.

`<\<x>` keeps its angle form, and the fifteen rows from the previous
commit pass.

* Test that every write route resolves links to asset-only folders (PRD-8896)

/api/agent-write-md and the batch handler resolve folders through the
folder index, but /api/frontmatter-patch and /api/agent-patch omit
folderExists, so a link to a folder that holds only assets comes back as
broken on those two routes. Both new route tests fail for that reason.

The type test pins that a write route which omits folder resolution does
not compile, and that each capability can be opted out of only by
writing null. It fails today because every capability is optional and
null is not accepted.

* Require every write advisory capability and resolve folders on patches (PRD-8896)

WriteAdvisoryTargets declared each capability optional and read a
missing one as a denial, so a route that forgot folderExists reported
links to asset-only folders as broken without any compile error. Every
capability is now required. A caller that means to go without one
writes null, which keeps the old meaning: no file oracle skips file
findings, and no folder oracle leaves only folders that hold documents.

/api/frontmatter-patch and /api/agent-patch now pass folderExists like
the main write path and the batch handler, so all four routes agree
with the Problems pane. The unit tests spell their opt-outs through one
NO_TARGETS fixture.

* Test that dotted wiki names stay in step with a rebuild (PRD-8896)

[[acp.daemon]] resolves to a missing file until a document such as
vault/acp.daemon claims the name, and then it is a document link. The
index keeps neither verdict current. A missing row records only its
file-basename key, so creating the document leaves the stale row. While
the document exists the assessment drops the occurrence, so no row or
edge is stored and deleting the document re-assesses nothing. Both
directions, through a source event and through a document reconcile,
now compare incremental state with a fresh rebuild and fail, including
the ![[ACP.Daemon]] and [[vault/acp.daemon]] spellings.

The assessment test now expects the claimed name to come back as a
document link that the local-target surfaces do not project, the same
verdict an undotted wiki name gets, instead of no row.

* Keep dotted wiki names current as documents come and go (PRD-8896)

A wiki asset name that a document claims by bare-name rules is now
assessed as a document link, the verdict an undotted wiki name already
gets, instead of yielding no row. The local-target surfaces still do not
project wiki document rows, so nothing new is shown, but the index now
stores every wiki asset occurrence whatever its verdict and filters to
projectable rows only when it is read. A claimed name therefore keeps
its document edges, and deleting the document re-assesses it.

A missing wiki file row also records the slug, basename and folder keys
a matching document emits when it is created or deleted, so creating
the document clears the stale missing row. The file keys now apply to
every wiki asset row, so a file that later takes the name back is
noticed while the row is a document link.

* Scope the identity rule paragraph to the forms it governs (PRD-8896)

The paragraph claimed one identity rule for every link form, and the
next one says a wiki embed such as ![[Images/cat.png]] still matches
its whole path case-insensitively. It now names wiki asset links and
embeds as the exception up front, so the page no longer contradicts
itself.

* Test that a physical-scope audit without an inventory gives basename wiki assets no verdict (PRD-8896)

Before the watcher's seed walk and after it unsubscribes there is no
tracked file inventory. The physical scope then builds its basename
resolver from an empty list, so ![[photo.png]] is reported missing
although media/photo.png exists. Its siblings probe the disk instead.
The new test expects only the slash form ![[media/gone.png]], which the
disk probe can judge, and fails because both basename embeds are
reported.

* Give basename wiki assets no verdict without a file list (PRD-8896)

A local-target inventory now states whether it can search by basename:
the resolver is required, and an inventory with no file list writes
null. A slash-free wiki asset then gets no verdict, as it did before
every inventory had to supply a resolver, instead of a false missing
file. The physical-scope audit passes null while there is no tracked
inventory, and its path-form siblings still probe the disk. Write
advisories pass their own opt-out through the same way.

* Name the angle-destination rows CommonMark forms as believed incomplete (PRD-8896)

The "never form" lists held shapes that CommonMark does form a link or
definition from, so a later guard fix that formed them would read as a
broken contract. Pure micromark, with GFM where footnotes matter, links
[^fn](</a b>), [a][b](</c d>), a label across a line, whitespace on
either side of the angle destination and the three scan-cap rows, and
defines next-line destinations and a blockquoted definition. Those rows
move to lists named as current, believed-incomplete behaviour, as the
escape rows already are. The guard-rejected rows micromark still links
and the bracketed-url test get the same label, and the conservative
rejection test splits the same way.

[[page]](</a b>) stays with the shapes that form no resource, because
OK reads [[page]] as a wiki link before CommonMark could read a
bracketed label, and [^a]: </p/x.md> stays because GFM reads it as a
footnote definition. Every row keeps its guarded, no-throw and
byte-stability assertions.

* Check the wiki asset resolver loop against literal expectations (PRD-8896)

resolveWikiLinkAssetTarget now builds its path matches with
createWikiAssetResolver, so the loop that compared the two ran the same
code on both sides and could not catch a wrong fold or tie-break. Each
target now carries the answer the documented rule gives: an identity
match first, then the whole-path fold with the lowest UTF-16 spelling
winning. Both functions are checked against those answers in both
input orders. Picking the highest folded twin instead now fails the
test.

* Test that the asset gate checks the path sirv serves (PRD-8896)

The exact-path gate decodes the request cut at ? only, while sirv looks
up the request cut at # and then ?, decoded with decodeURI. A raw
request line GET /docs/secret#.png passes the gate as docs/secret#.png
and streams the ignored, extensionless docs/secret, and /.env#.png
streams .env the same way. GET /docs/x%2Fy.png is checked as
docs/x/y.png while sirv serves the ignored docs/x%2Fy.png. Browsers
strip fragments and encode differently, so the tests send raw request
lines over net.connect. All three leak the file today; a request line
both decoders agree on is still served.

* Refuse asset requests whose gate path is not the path sirv serves (PRD-8896)

The ignore and extension checks run on the request decoded with
decodeURIComponent and cut at ?, while sirv serves the request cut at #
and then ? and decoded with decodeURI. When the two paths differ, the
gate approved one file and sirv streamed another. The middleware now
compares them first. On a mismatch neither the exact path nor the
tracked-file fallback is tried, so sirv never runs and an asset
extension gets the fail-closed 404. When they agree, every check has
already run on the path sirv serves. Browsers never send a fragment and
encode file names the way both decoders read alike, so ordinary asset
requests are unchanged.

* Add a changeset for the asset request gate (PRD-8896)

* Test that dot and empty request segments cannot bypass an anchored ignore rule (PRD-8896)

GET /docs/./secret.png, /docs//secret.png and /x/../docs/secret.png pass the gate's ignore check on the unnormalized path while sirv opens the normalized docs/secret.png, so an ignored file is served.

* Refuse asset requests with dot or empty path segments before sirv (PRD-8896)

The ignore check ran on the request path as written while sirv opens the normalized path, so anchored ignore rules missed /docs/./secret.png and its relatives. A request whose path holds an empty, . or .. segment now takes the same fail-closed route as any other gate and sirv disagreement.

* Pin each refused dot-segment asset request and prove the file is servable (PRD-8896)

The dot and empty segment test only checked that no response leaked
the secret, so an empty reply, a 400 or a broken harness passed too.
It now serves docs/secret.png before the anchored rule exists, shows
the same request falls through once the rule is written, pins the 404
status line of every dot and empty segment row, and checks the file is
still on disk afterwards.

* Test that a backslash request path cannot route around an ignore rule (PRD-8896)

GET /x%5C..%5Cdocs%5Csecret.png decodes to a single segment, so the
gate's own segment check passes it and sirv opens whatever the
platform resolves it to. On win32 that is docs\secret.png behind an
anchored rule. The test writes the secret at that spelling, which is
the ignored file on win32 and a literal name on POSIX, so the row fails
on every platform while the gate admits backslashes.

* Refuse asset requests that fail the shared content-path check (PRD-8896)

The asset gate kept its own copy of the segment rule and left out the
backslash and NUL guards, so a %5C spelling of a dot segment passed it
and win32 path handling opened the file an anchored ignore rule names.
The gate now calls isValidRelativeContentPath, the definition every
other content route already uses. Content paths holding a backslash are
already refused by the create, rename and entry routes, so no file the
server can otherwise address stops being servable.

* Name every refused request spelling in the asset gate changeset (PRD-8896)

* Test that a null write advisory capability gives no verdict (PRD-8896)

A null fileExists or resolveFileByBasename already withholds the
verdicts it judges, but a null folderExists reports an asset-only
folder as a missing document, a null fileExcluded reports a file that
may be excluded as missing, and a null resolveWikiFile reports a wiki
asset whose folder case differs as missing. One row per capability now
checks that an oracle knowing nothing reports the link and that null
withholds it. Tests that expect verdicts supply every capability
through a NOTHING_TRACKED fixture instead of relying on null.

* Give every null write advisory capability the same meaning (PRD-8896)

A finding now survives only when every capability its verdict rests on
was supplied. A document verdict needs the folder oracle, since any
missing document link could name a folder that holds only assets. A
missing-file verdict needs the file list and the exclusion probe, and
for a wiki asset the whole-path resolver too. Any finding on a file
target needs the file list. A null capability therefore withholds the
verdicts it judges instead of reporting a target as missing.

* Always prepare write link advisories with the folder oracle (PRD-8896)

Every write route now resolves folder links, so resolveFolderLinks was
true at all four call sites. Since a null folder oracle withholds every
document verdict, a caller passing false would silently lose all
missing-document findings. The option is gone and the prepared advisor
always supplies folderExists.

* Test that hidden local-target rows move neither generation nor stats (PRD-8896)

The index stores wiki asset rows that resolve to a document so their
dependencies stay registered, and hides them from every reader. A
change that touches only those rows still reports a change and bumps
the generation, and getStats counts them, so its occurrence count
disagrees with the rows the index returns.

* Count only returned local-target rows in changes and stats (PRD-8896)

A source event, a removal, a reassessment and a rebuild now report a
change, and move the generation, only when the rows the index returns
change. getStats, the rebuild result and the file occurrence count
measure those rows too. Hidden wiki asset rows are still stored and
still register their dependencies, so later document and file events
reach them.

* State the real scope of the asset request refusal (PRD-8896)

The release note said only ignored files stop being served to raw
requests spelled with these forms. The gate refuses every such request,
whatever the file, so a file with a backslash in its name is no longer
served on macOS or Linux either. The note now says so.

* Decide every write advisory reason explicitly (PRD-8896)

hasVerdict named two reasons and let every other one through, so a
reason added to BROKEN_LINK_REASONS would report even when the
capability that judges it is null, and nothing would fail to compile.
It now switches over each reason with a never default: excluded needs
the file list and the exclusion probe, unresolvable on a file target
needs the file list, and a new reason is a compile error until it is
given a rule. Behaviour is unchanged.

* Test that a Markdown document link gets no verdict without the file list (PRD-8896)

A Markdown link such as [m](./Makefile) can name an extensionless file,
and the assessment only checks for that file before calling the target
a missing document. When the file list or the exclusion probe is null
that check cannot run, so no-such-doc is a guess. The old row pinned the
guess for [g](./g). The new rows expect inline and reference document
links to be withheld when either capability is null, while wiki links
and JSX document refs, which never name a file, keep their verdict.

* Withhold a Markdown document verdict without the file list (PRD-8896)

A null capability now means no verdict for every target it would have
to judge. A Markdown inline or reference link to a missing document can
still name an extensionless file, so its no-such-doc verdict needs the
file list and the exclusion probe as well as the folder oracle. Wiki
links and JSX document refs never name a file and keep needing the
folder oracle alone. isWikiForm is exported so the advisory can tell
the forms apart.

* Test that a rebuild over hidden rows alone stays quiet (PRD-8896)

The setSource, removeSource and reassess paths already have a test that
rows kept only to track dependencies move neither the generation nor
the stats. The rebuild path had none. This one rebuilds a tree whose
only rows are hidden twice and expects zero sources and occurrences
both times and an unchanged generation on the second run. Restoring
sourceAssessments.size at either rebuild site turns it red: the
generation check fails for rebuildOnce and the counts check for
populateFromDisk.

* Drop the asset gate test's read-back of its own fixture (PRD-8896)

The last assertion read back the file the test had just written. A GET
has no write path, so only a broken fixture could turn it red. The
beforeTheRule row already shows the file is served before the ignore
rule exists.

* Test that a write advisory judges an extensionless file link as a file (PRD-8896)

[m](./Makefile) names no document, so the write advisory's broken-link
scan reports it as no-such-doc even when Makefile exists, and a link to
an ignored extensionless file gets no-such-doc instead of excluded. The
local-target assessment already tries the file before calling such a
link a missing document. These tests expect the scan, and the write
advisory with every capability supplied, to do the same: no report for
an existing file, excluded for an ignored one, no-such-doc otherwise.

* Judge an extensionless file link as a file in write advisories (PRD-8896)

The broken-link scan behind the write advisories called any Markdown
link that names no document a missing document, so [m](./Makefile)
reported no-such-doc beside an existing Makefile. It now does what the
local-target assessment does: when no document matches, it resolves
the href as a file path and reports nothing if the file exists and
excluded if an ignore rule keeps it out, using the same file list and
exclusion probe the scan already receives for file links. Otherwise
the folder check and no-such-doc are unchanged.

* Note the write advisory fix in the extensionless file changeset (PRD-8896)

Agent write results stopped reporting a link to an existing
extensionless file as a missing page, so the release note for
extensionless file links now says so.

* Test that a wrong-case .md link is not satisfied by a file check (PRD-8896)

[g](./Guide.md) beside notes/guide misses the case-sensitive document namespace, but the new file-first step asks the file oracle about notes/Guide.md. On a case-insensitive disk the probe answers yes, so the write advisory drops the link while the editor and Problems pane call it missing.

* Never satisfy a missing document link with a file check on its .md path (PRD-8896)

The file-first step for document links now skips hrefs whose path is a Markdown document, in both the broken-link scan and the local-target assessment, matching the graph plane's namesExistingFile. A wrong-case [g](./Guide.md) stays no-such-doc on every host instead of passing a case-insensitive disk probe.

* Test that a link to an ignored .md file still reports excluded (PRD-8896)

[d](./drafts/plan.md) where drafts/plan.md exists but is ignored should report excluded with the re-include hint on both the broken-link scan and the local-target assessment. The .md guard added in the previous commit skips the whole file step, so both planes now call it no-such-doc. The wrong-case [g](./Guide.md) row beside it must keep reporting no-such-doc.

* Withhold only the existence verdict for .md link paths (PRD-8896)

The .md and .mdx guard on the file-first step now skips only the file existence check. The ignored-file check still runs, so a link to an existing but ignored Markdown file reports excluded with the re-include hint again on the broken-link scan and the local-target assessment, while a wrong-case [g](./Guide.md) still reports no-such-doc.

* Test that a .md link with a fragment keeps its graph edge (PRD-8896)

The graph plane decides whether a document link names a file by testing the raw href, so ./Guide.md#intro and ./Help.mdx?v=1 do not count as document files there and a file oracle answering their resolved path drops the edge. The broken-link scan and the local-target assessment test the resolved path, which has the fragment and query stripped, so the planes disagree.

* Decide document-file links through one shared predicate (PRD-8896)

The graph plane, the broken-link scan and the local-target assessment now all ask linkNamesDocumentFile in doc-extensions.ts whether a link names a .md or .mdx file. It judges the resolved project path, so a fragment or query no longer hides the extension on the graph plane, and a new document extension or a change to fragment handling lands in one place.

* Move the write advisory file check beside the local-target inventory (PRD-8896)

createLinkedFileExists in api-extension.ts now delegates to createFileExistsOracle in local-target-inventory.ts, so the tracked-file lookup and its disk fallback can be tested without booting the API extension. Behavior is unchanged.

* Test that the write advisory file check never accepts a directory (PRD-8896)

The disk fallback behind the write advisory's file check uses existsSync, which is true for a directory. A document link such as [d](./Sub) that names an unindexed or wrong-case directory on a case-insensitive host is therefore taken as an existing file and dropped from the advisory, while Linux reports no-such-doc. The fallback should accept regular files only.

* Accept only regular files in the write advis…
Assets 2
Loading