Skip to content

test(conformance): pin section 11's anchors and transclusions - #46

Closed
CameronBrooks11 wants to merge 14 commits into
mainfrom
test/anchor-cases
Closed

CameronBrooks11 wants to merge 14 commits into
mainfrom
test/anchor-cases

Conversation

@CameronBrooks11

@CameronBrooks11 CameronBrooks11 commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Adds 215 conformance cases of the anchor kind under conformance/cases/anchor/, pinning what SPEC.md §11 decides about anchors and transclusion. It also adds their provenance entry to conformance/cases/README.md.

How they were written

The cases come from SPEC.md (§11 above all), docs/adjudications/0003-anchors.md with all three of its addenda, 0001 and 0002, the case-tree README, case-body.schema.json and the tree on main at 66ce7f1, and later at ed2e50e (#47) and f75d069 (#48), the second and third addenda, which this branch has merged in. I did not read any implementation of §11: not #45, not its branch, not any branch but main. The only code I read was the runner, check_tree.py, anchor_cases.py and the block reader reference/blockspec/document.py, to learn the parse.

Each WHY.md quotes the passage that decides its outcome. I cross-checked every status, leaf and span with a throwaway calculation of §11.2 to §11.5, written from the spec and not committed. It ran on the repository that anchor_cases.Repository builds, so placeholders were hashed by git and merges were run by stock git, as the runner does them. Every merge produced the outcome its case states.

Landing plan

main's reference has no resolve, so all 215 anchor cases fail just conform with "the implementation defines no resolve(repo, queries)", and just test fails on the same root cause (test_conformance, plus the mutation probe's baseline). This is the order the README's Landing order decides. The cases are reviewed here. Then #45 rebases onto them and runs against them with no case changed, and that merge lands the cases and the resolver together. This PR then closes as landed through #45. Do not merge this PR on its own.

run_cases.MIN_CASES is still 237. I did not change it, because this PR touches only conformance/cases/. Raise it to 452 when the cases land.

just check, just tree (452 cases, 0 problems), kindkit's validate_case_tree.py and schema_selftest.py all pass.

What the cases cover

record/refuses-an-unknown-leaf-type cannot fail for the T check alone: the leaf-at-nth rule refuses the same record, and its WHY.md says so.

group cases what
status/ 12 RESOLVED, MOVED, EDITED, DETACHED, ORPHANED; a heading that only changes level; CRLF and NFD re-saves
leaf/ 5 leaves in a list item, a block quote, a code block and a table; the | escape keeping a one-column and a two-column table apart
context/ 6 a short prefix that must sit at the leaf's start; a short suffix that may, and must, end at the leaf's end; an edit one code point outside and on the first code point of a full prefix; code-point offsets with a character outside the BMP
record/ 39 the canonical form accepted (every escape, U+0080 escaped, SHA-256) and refused: an anchor that is a JSON array, a quote that overlaps itself, U+0080 written raw, a negative nth, a context of 33 code points, a body the profile refuses, notes file names that are not <ULID>.md (not a ULID, lower case, a letter outside Crockford's set, a first character above 7, .txt), no frontmatter, added or reordered lines, unknown status, trailing space, spacing, key order, other escapes, upper-case hex escape, raw U+007F, \t, unquoted or duplicate key, fractional nth, unknown T, replace on a comment or missing on a suggestion, short contexts away from the edge, empty span, a triple that does not occur or occurs twice, nth past the copies
input/ 7 not-in-profile for a base with a setext heading; document-missing (deleted; renamed without its notes), base-unavailable (after a squash; an id no file has), not-in-profile (raw HTML; committed conflict markers)
correspondence/ 7 the leftmost chain on a two-leaf swap; the 0.15 margin on the base leaf's side; the gap alignment skipping the base element first; similarity exactly 0.5 and just below; a margin of exactly 0.15 (where IEEE 754 doubles get it wrong) and just below
no-pair/, repeat/, near-copy/, refilled-gap/, exact-copy/, crowded-gap/, span/ 18 each §11.4 and §11.5 rule with its controls: twins kept apart by unique neighbours, twins beside a moved leaf, a twin deleted, a copy added, near-copies on EXACT (including a similarity of exactly 0.5, and a verbatim copy, similarity 1, that is not one), MOVED and EDITED, the refill rule, the Q12 narrowing, a crowded gap, two hits, two overlapping hits, and an alignment that detaches
suggestion/ 35 unmappable-offset for a span across or ending right before the spaces a partly consumed tab leaves in a code block (in a list item, and under a fence's own indentation) and for a span ending in a code block's final \n, with a span beginning after the leftover spaces and one ending before the final \n that apply; write-back in a leaf with a tab of list-item indentation (a span right after it, mid-line after it, and before it), and after a marker followed by a tab; unmappable-offset on a list item's lazy line and under a quote's > and tab; unmappable-offset for a \n in replace on a list item's first line, a lazy line, a tab-prefixed line and a last line with no ending (with its control); the gate (RESOLVED, EDITED and heading level apply; MOVED, DETACHED, ORPHANED, refused, accepted, rejected and resolved are stale) and exact write-back: CRLF, a line break as CRLF and as a bare CR, a span across a CRLF break, > and list-item continuation prefixes, no normalisation of the leaf or of replace, and unmappable-offset
reply/ 15 replies whose document is missing or refused, a malformed reply-to before document-missing, and document-missing before a missing parent; reply-to lines with extra spacing or single quotes refused; a lower-case and a non-ULID reply-to refused; replies to a record, to a reply, to an orphaned record; #REF!(reply-to) for a missing parent and for a parent in another notes directory; a bare id and a status line refused
transclusion/ 30 under CommonMark 0.31.2, no transclusion for a link inside link text, a title &#116ransclude (no ;, so not an entity), an image, or prose holding "transclude"), and a transclusion whose title is followed by a line ending before ); a marker name that is also a heading's slug resolving to the marker; a heading slug that matches while no marker does (#REF!(name)); resolution after edits, of a region, after a move with its name; name-rebound by insertion and by a moved marker, and re-pinning; pinned-orphaned, pinned-crowded-gap, pinned-near-copy; #REF!(name); a pinned name that binds nothing; unresolvable-path (/, a scheme, .. above the root); .. and . from a subdirectory; document-missing (absent, case, percent-encoding); base-unavailable; not-in-profile for the target and the pinned blob; a link with another title
pins/ 10 two sorted pins; pins matched by raw path (./policy.md and policy.md with different outcomes); a list item after another key, not refused; then each grammar break refusing every transclusion: order, duplicate, spacing, a pin after a comment line or a blank line, two pins: lines, a pins: line with more after it
merge/ 7 clean merges whose result resolves (other leaves; two lines of the anchored leaf, with a suggestion that applies; a status change beside a re-anchoring; re-pins of two targets); conflicts giving not-in-profile (a document and a transclusion target) and record-inconsistent (two re-anchorings)
order/ 7 §11.10's record order, each case setting up two reasons: grammar and a bad file name before document-missing; document-missing and a refused current text before base-unavailable; base-unavailable before the leaf-at-nth check; a quote that occurs twice and a short context (step 1) before base-unavailable
transclusion-order/ 10 an unbound name (no marker, no slug) after base-unavailable; a pinned-<reason> before name-rebound; §11.9's step order: unresolvable-path before broken pins; broken pins before document-missing; document-missing and a refused target before base-unavailable; a slug-only name before base-unavailable and before the pinned binding; a refused pinned blob before the pinned binding; several pinned-<reason>s reporting the first leaf
referrer/ 3 the referrer-level REFUSED(not-in-profile): raw HTML, a conflict line in a fence, and two adjacent re-pins that conflict under stock git
residual/ 4 R1 to R4, pinned as §11.12 says they resolve. Each WHY.md marks the outcome as a documented residual, not a defect

Questions §11 did not decide

The first version of this PR listed nine. The second addendum (#47) decides all of them, and the cases above pin each decision: a context over 32, the order of input reasons (records and transclusions), unresolvable-path before broken pins, the line breaks that cannot be written back, notes files and reply-to ids that are not ULIDs, a slug-only name, a refused or conflicted referrer, a refused record body, and the unreachable #REF!(name) and EXACT gap clauses, which are now removed. I rechecked every existing case against the new text. No expected outcome changed. Twenty-six WHY.md files quoted wording that the addendum changed, and they now quote the current text.

The two questions this PR later raised, overlapping occurrences and a span across a tab that list indentation partly consumes, are decided by the third addendum (#48), and cases now pin both. Under it I rechecked suggestion/writes-back-after-a-tab-in-an-item-s-indentation: in a paragraph the leftover tab columns are indentation the line loses, so they never enter leaf.text, and the span maps as before.

The open items §12 still lists have no case here: whether creation and the consistency check count under the edge rule, further frontmatter keys, a pin no transclusion uses, frontmatter that hides the pins line, write-back into a table, renderer-style path decoding and empty segments, symbolic links and submodules, and the transclusion details (no path, angle brackets, other title quoting, a link among other text, a transclusion inside a list item or a quote, no pin, self-transclusion). A subfolder in a notes directory is decided but can't be expressed: the case kind names records by file, so no case pins it.

Add 141 cases of the anchor kind under conformance/cases/anchor/,
written from SPEC.md section 11 and adjudication 0003 with its
addendum, without reading any implementation. They cover every
resolution status and reason, the record and pin grammars, the
correspondence thresholds at their exact boundaries, the leftmost
chain, the twin, refill and near-copy rules, context edges, the
suggestion gate and write-back, replies, transclusion, merges with
stock git, and the residuals R1 to R4. Each WHY.md quotes the
passage that decides it, and the README records their provenance.

The reference has no resolve yet, so every anchor case fails
`just conform`. They land together with the resolver, as the
README's landing order says.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
No leaf of base has the refused type with the record's text, so the
leaf-at-nth rule gives the same outcome, and the case cannot go red
for the T check alone. Its WHY.md now says so.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Add seven anchor cases: the 0.15 margin on the base leaf's side, the
gap alignment skipping the base element first, a short suffix that
must end at the leaf's end, a matching heading slug that is not a
transclusion target, a pins line with more after it, a list item
after another frontmatter key that is not refused, and pins matched
by their raw path.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
The second addendum renumbers section 11.9's steps, drops the EXACT
rule's gap clause and widens two rows of section 11.10. Re-quote the
WHY.md passages that changed. No expected outcome changes.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Add 35 anchor cases for 0003's second addendum: an over-long context,
the record and transclusion reason orders through cases that set up
several reasons, unresolvable-path before broken pins, the line breaks
that cannot be written back, notes files that are not ULIDs and
malformed reply-to ids, slug-only names, referrers refused as a whole,
a refused record body, and the checks moved into step 1 of the order.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
An unbound name with an unreadable pin is base-unavailable, since only
a slug-only name is decided before the pinned blob is read. A pinned
refusal is reported before name-rebound, step 3 before step 4.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Add ten anchor cases: U+0080 escaped and raw, a negative nth, two
non-canonical reply-to lines, write-back after and across a tab of
list-item indentation and after a marker followed by a tab, and line
breaks in replace on a list item's lazy line and under a quote's tab.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
The case pinned a span that crosses a tab partly consumed by list
indentation. That mapping is not decided yet, so the case comes out
until it is.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Add a case with two twins in the gap of a moved leaf, one refused as
repeat and one as unaccounted-copy, and two write-back cases whose
span lies in a leaf with a tab-indented line without reaching the tab.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Section 11.8 and 11.10 now render #REF!(reply-to) only once a reply's
document checks pass. Re-quote the three WHY.md files that cited the
old wording. No expected outcome changes.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Add twelve anchor cases: a quote that overlaps itself and overlapping
hits, replies whose document is missing or refused with their place in
the reason order, spans across, before and after the spaces a partly
consumed tab leaves in a code block, including under a fence's own
indentation, and a span ending at a code block's final line ending
with its control.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Add six anchor cases: an anchor that is a JSON array, and referrer
paragraphs that are not a transclusion under CommonMark 0.31.2 (a link
inside link text, an entity with no semicolon in the title, an image,
prose holding the title's characters), plus one whose title is
followed by a line ending before the closing parenthesis, which is.

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
CameronBrooks11 added a commit that referenced this pull request Oct 16, 2026
The reference resolver for `SPEC.md` §11, together with §11's
conformance
cases. The cases are #46's, `test/anchor-cases` at `d389dee` (215 anchor
cases, 452 in all), written from `SPEC.md` by an author who had not read
the
resolver, and merged here with no case changed. Merging this lands both,
and
#46 closes as landed through it. `main`, with 0003's three addenda (#43,
#47,
#48), is merged in too.

**Landing order** (decided). The first anchor case would turn the
required
`reference` check red on `main`, since the reader had no `resolve` until
this PR. So the cases were written blind from `main` on their own branch
and
reviewed there, then merged into this branch and run against it.
`README.md`, `AGENTS.md` and `SPEC.md` now say that the 0001 and 0002
cases
came before the reader, and the 0003 cases after the resolver.

## Against the cases

$ just conform # after merging d389dee, before any change made for it
    0 failure(s) across 452 case(s) in the fixture tree
    $ just conform    # at the head of this branch
    0 failure(s) across 452 case(s) in the fixture tree

No case failed, here or at the earlier merges of `e0f5525` (422 cases)
and
`7375c2c` (446). So no
case is wrong per SPEC, no SPEC question came out of a failure, and the
resolver needed no fix for the cases. `run_cases.MIN_CASES` is 452.

## What this adds

`reference/blockspec/anchor.py`, standard library only, implements §11
on
top of the existing reader:

- **records** (§11.1): the exact frontmatter lines, the canonical JSON
anchor
(key order, spacing, escape set, `B` as 40 or 64 lower-case hex digits
by
object format, `N` as plain decimal), `replace` tied to `kind`, ULID
file
  names and `reply-to`, a body the profile refuses, and the consistency
  rules against `base`, with occurrences counted by start position;
- **correspondence** (§11.3): the leftmost longest chain, the §11.2 LCS
alignment inside each gap with MOVED leaves left out, and EDITED pairing
  with the integer comparisons of the first addendum (`22/30 − 42/72` is
  tested as exactly 0.15);
- **the anchored leaf** (§11.4): every rule and reason, in first-applies
  order;
- **the span** (§11.5): hits counted by start position, the edge rule
and
  the alignment check;
- **input problems** in §11.10's order, replies included (third
addendum);
- **statuses, the suggestion gate and write-back** (§11.6, §11.7):
NFC-to-raw mapping through container prefixes, lost indentation, CRLF
and
  bare CR; a `\n` in `replace` written as the line's ending plus its
container prefixes; and `unmappable-offset` for a span that does not
map,
for a line break that cannot be re-emitted, for a span crossing or
ending
right before a tab's leftover spaces, and for a span ending with a code
  block's final `\n`;
- **transclusion** (§11.9): the pins grammar, path resolution, and steps
0
  to 4, with a referrer the profile refuses reported as
`"REFUSED(not-in-profile)"` and the first pinned leaf in the pinned
blob's
  document order deciding among several `pinned-<reason>`s.

`document.py` gains `resolve(repo, queries)`, the interface the runner
calls. It loads `anchor.py` from its own directory by path and hands it
this
file's reader, so a mutated copy of the reader is the one the resolver
runs.
The reader's own output is unchanged: a script run locally compared
`conform()` from `origin/main` and from this branch, messages included,
for
every `parse` case input, and got `237 inputs, 0 differ`. A copy with
one
line changed gives `237 inputs, 35 differ`.

## Fixed after review

- A JSON array where the grammar wants an object (`anchor: [1]`, a leaf
or
a pin that is an array) raised out of `resolve()`. Objects are now told
from arrays, and every malformed record or pin is `record-inconsistent`.
The review's fuzz (6,000 random records, pins and referrers) now raises
  nothing.
- `[a [b](c.md) d](policy.md#scram "transclude")` was read as a
  transclusion. Link text cannot hold a link (CommonMark 0.31.2, example
  518), so it is no link and no transclusion.
- The self-transclusion walk skipped a whole document when one of its
paragraphs was undecided, so A to B to A went unseen. It now skips only
  that paragraph.
- The claim that "moved leaves stay in gaps" is equivalent was false:
with
  twins beside a move, the mutant swaps `repeat` and `unaccounted-copy`.
  It is an ordinary mutant now, a unit test kills it, and
  `anchor/repeat/twins-beside-a-move` kills it in the gate.
- Shapes CommonMark decides, which were `UNDECIDED`: an image titled
  `transclude` is no link; a paragraph with no inline link is no
  transclusion; a title may be followed by a line ending before `)`; an
entity reference is decoded only with its `;`, so `&#116ransclude` is
not
  `transclude`.

## The mutation gate

`just mutants` gates the resolver as well as the reader: a second table
in
`conformance/mutants.py` over `reference/blockspec/anchor.py`, probed by
the
whole tree. Its probe writes each mutant as `anchor.py` beside a copy of
the reader. Mutants that only turn an `UNDECIDED` into a guess are left
out, since no case can expect one, as are the duplicate-id warning and a
notes-directory subfolder, which no case field can observe. The
resolver's table, as the gate printed it:

    --- the resolver, reference/blockspec/anchor.py
killed escape set misses U+2028 (1 case(s):
anchor/record/accepts-every-escape)
killed escape set misses C1 controls (2 case(s):
anchor/record/accepts-an-escaped-c1-control,
anchor/record/refuses-a-raw-c1-control)
killed negative nth read (1 case(s):
anchor/record/refuses-a-negative-nth)
killed replace not tied to kind (2 case(s):
anchor/record/refuses-a-suggestion-without-replace,
anchor/record/refuses-replace-on-a-comment)
killed canonical bytes not compared (7 case(s):
anchor/order/grammar-before-document-missing,
anchor/record/refuses-a-raw-c1-control,
anchor/record/refuses-a-short-tab-escape)
killed table pipe not escaped (1 case(s):
anchor/leaf/an-escaped-pipe-keeps-two-tables-apart)
killed heading level dropped (2 case(s):
anchor/status/edited-when-a-heading-changes-level,
anchor/suggestion/applies-when-a-heading-changes-level)
killed text not normalised (3 case(s):
anchor/status/resolved-when-the-normalisation-form-changes,
anchor/suggestion/stale-when-the-span-does-not-map,
anchor/suggestion/writes-back-without-normalising)
killed half compares strictly (2 case(s):
anchor/correspondence/a-similarity-of-exactly-one-half-pairs,
anchor/near-copy/exact-with-an-unmatched-leaf-at-one-half)
killed below one allows one (1 case(s):
anchor/near-copy/an-exact-copy-elsewhere-is-not-a-near-copy)
killed margin compares strictly (1 case(s):
anchor/correspondence/a-margin-of-exactly-0-15-pairs)
killed margin in floating point (1 case(s):
anchor/correspondence/a-margin-of-exactly-0-15-pairs)
killed alignment skips the second first (1 case(s):
anchor/correspondence/the-gap-alignment-skips-the-base-leaf-first)
killed chain is the rightmost (1 case(s):
anchor/correspondence/a-swap-keeps-the-leaf-now-first-on-the-chain)
killed chain is not the longest (55 case(s):
anchor/correspondence/a-swap-keeps-the-leaf-now-first-on-the-chain,
anchor/correspondence/the-gap-alignment-skips-the-base-leaf-first,
anchor/exact-copy/a-paired-twin-does-not-count)
killed moved pairs dropped (10 case(s):
anchor/correspondence/a-swap-keeps-the-leaf-now-first-on-the-chain,
anchor/near-copy/moved-with-its-reworded-text-in-place,
anchor/refilled-gap/moved-with-a-new-leaf-in-its-place)
killed moved leaves stay in gaps (1 case(s):
anchor/repeat/twins-beside-a-move)
killed no lcs in gaps (7 case(s):
anchor/exact-copy/a-paired-twin-does-not-count,
anchor/near-copy/an-exact-copy-elsewhere-is-not-a-near-copy,
anchor/no-pair/a-twin-deleted-between-unique-neighbours)
killed edit needs no half (2 case(s):
anchor/correspondence/a-similarity-just-below-one-half-does-not-pair,
anchor/status/orphaned-when-rewritten-beyond-recognition)
killed edit needs no lead over other current (1 case(s):
anchor/correspondence/the-margin-must-hold-on-the-base-leaf-side)
killed edit needs no lead over other base (1 case(s):
anchor/correspondence/a-margin-just-below-0-15-does-not-pair)
killed no unaccounted-copy (3 case(s):
anchor/no-pair/a-twin-deleted-between-unique-neighbours,
anchor/repeat/one-of-two-adjacent-copies-deleted,
anchor/repeat/twins-beside-a-move)
killed repeat only when base repeats (1 case(s):
anchor/repeat/a-copy-added-beside-the-leaf)
killed repeat only when current repeats (2 case(s):
anchor/repeat/one-of-two-adjacent-copies-deleted,
anchor/repeat/twins-beside-a-move)
killed exact near-copy off (3 case(s):
anchor/near-copy/exact-with-an-unmatched-leaf-at-one-half,
anchor/near-copy/exact-with-its-reworded-text-beside-it,
anchor/suggestion/stale-on-a-refusal)
killed near-copy below half (8 case(s):
anchor/near-copy/exact-with-an-unmatched-leaf-just-below-one-half,
anchor/pins/a-pin-matches-its-raw-path,
anchor/refilled-gap/moved-with-a-new-leaf-in-its-place)
killed exact near-copy counts exact copies (1 case(s):
anchor/near-copy/an-exact-copy-elsewhere-is-not-a-near-copy)
killed moved near-copy off (3 case(s):
anchor/near-copy/moved-with-its-reworded-text-in-place,
anchor/transclusion-order/a-pinned-reason-before-name-rebound,
anchor/transclusion/pinned-near-copy)
killed refilled gap off (1 case(s):
anchor/refilled-gap/moved-with-a-new-leaf-in-its-place)
killed exact-copy off (1 case(s):
anchor/exact-copy/edited-with-its-old-text-unmatched)
killed exact-copy counts paired copies (2 case(s):
anchor/correspondence/the-gap-alignment-skips-the-base-leaf-first,
anchor/exact-copy/a-paired-twin-does-not-count)
killed crowded gap off (2 case(s):
anchor/crowded-gap/edited-with-a-new-leaf-beside-it,
anchor/transclusion/pinned-crowded-gap)
killed edited near-copy off (1 case(s):
anchor/near-copy/edited-with-a-variant-elsewhere)
killed empty span allowed (1 case(s):
anchor/record/refuses-an-empty-span)
killed triple may occur twice (3 case(s):
anchor/order/a-quote-twice-before-base-unavailable,
anchor/record/refuses-a-quote-that-occurs-twice,
anchor/record/refuses-a-quote-that-overlaps-itself)
killed short prefix anywhere (1 case(s):
anchor/record/refuses-a-short-prefix-away-from-the-edge)
killed short suffix anywhere (2 case(s):
anchor/order/a-short-context-before-base-unavailable,
anchor/record/refuses-a-short-suffix-away-from-the-edge)
killed occurrences skip overlaps (2 case(s):
anchor/record/refuses-a-quote-that-overlaps-itself,
anchor/span/overlapping-hits-are-ambiguous)
killed no prefix edge rule (1 case(s):
anchor/context/a-short-prefix-must-sit-at-the-leaf-start)
killed no suffix edge rule (1 case(s):
anchor/context/a-short-suffix-must-sit-at-the-leaf-end)
killed two hits not ambiguous (2 case(s):
anchor/span/overlapping-hits-are-ambiguous,
anchor/span/two-hits-are-ambiguous)
killed no alignment check (1 case(s):
anchor/span/the-alignment-detaches-a-rewritten-span)
killed gate ignores status (3 case(s):
anchor/suggestion/stale-when-accepted,
anchor/suggestion/stale-when-rejected,
anchor/suggestion/stale-when-resolved)
killed gate applies on moved (2 case(s):
anchor/residual/r1-a-deleted-leaf-quoted-elsewhere,
anchor/suggestion/stale-on-moved)
killed break written as LF (2 case(s):
anchor/suggestion/writes-a-line-break-as-a-bare-cr,
anchor/suggestion/writes-a-line-break-as-crlf)
killed break written with no prefix (2 case(s):
anchor/suggestion/writes-back-in-a-block-quote,
anchor/suggestion/writes-back-in-a-list-item-continuation-line)
killed synthetic spaces mapped (1 case(s):
anchor/suggestion/writes-back-a-span-beginning-after-leftover-tab-spaces)
killed absolute path allowed (1 case(s):
anchor/transclusion/unresolvable-path-with-a-scheme)
killed scheme path allowed (2 case(s):
anchor/transclusion-order/unresolvable-path-before-broken-pins,
anchor/transclusion/unresolvable-absolute-path)
killed above the root allowed (1 case(s):
anchor/transclusion/unresolvable-path-above-the-root)
killed pins order unchecked (4 case(s):
anchor/pins/refuses-a-path-pinned-twice,
anchor/pins/refuses-pins-out-of-order,
anchor/transclusion-order/broken-pins-before-document-missing)
killed pins after a blank line read (2 case(s):
anchor/pins/refuses-a-pin-after-a-blank-line,
anchor/pins/refuses-a-pin-after-a-comment-line)
killed name ref skipped (1 case(s):
anchor/transclusion/a-name-that-binds-nothing-now)
killed rebound unchecked (3 case(s):
anchor/pins/a-pin-matches-its-raw-path,
anchor/transclusion/name-rebound-by-a-leaf-inserted-after-the-marker,
anchor/transclusion/name-rebound-when-the-marker-moves)
killed reply-to not canonical (1 case(s):
anchor/reply/refuses-a-reply-to-with-extra-spacing)
killed pin not canonical (1 case(s):
anchor/pins/refuses-a-pin-not-in-canonical-form)
killed pins line with trailing text (1 case(s):
anchor/pins/refuses-a-pins-line-with-more-after-it)
killed dot segment kept (2 case(s):
anchor/pins/a-pin-matches-its-raw-path,
anchor/transclusion/resolves-relative-paths-from-a-subdirectory)
killed synthetic char inside span mapped (1 case(s):
anchor/suggestion/unmappable-span-across-a-tab-left-by-fence-indentation)
killed repeat ignores counts (3 case(s):
anchor/repeat/a-copy-added-beside-the-leaf,
anchor/repeat/one-of-two-adjacent-copies-deleted,
anchor/repeat/twins-beside-a-move)
killed over-long context read (2 case(s):
anchor/record/refuses-a-prefix-longer-than-32,
anchor/record/refuses-a-suffix-longer-than-32)
killed reply-to any string (3 case(s):
anchor/reply/a-malformed-reply-to-before-document-missing,
anchor/reply/refuses-a-lower-case-reply-to,
anchor/reply/refuses-a-reply-to-that-is-not-a-ulid)
killed lowercase ulid read (1 case(s):
anchor/record/refuses-a-lower-case-ulid-file-name)
killed reply parent never found (3 case(s):
anchor/reply/a-reply-to-a-record-renders,
anchor/reply/a-reply-to-a-reply-renders,
anchor/reply/a-reply-to-an-orphaned-record-renders)
killed base checked before record-only checks (2 case(s):
anchor/order/a-quote-twice-before-base-unavailable,
anchor/order/a-short-context-before-base-unavailable)
killed record body not checked (1 case(s):
anchor/record/refuses-a-body-the-profile-refuses)
killed document missing ignored for records (3 case(s):
anchor/input/document-missing,
anchor/input/document-renamed-without-its-notes,
anchor/order/document-missing-before-base-unavailable)
killed base read before the document (1 case(s):
anchor/order/current-not-in-profile-before-base-unavailable)
killed lazy bq line repeats a shorter prefix (1 case(s):
anchor/suggestion/unmappable-line-break-on-a-lazy-line)
killed lazy list line repeats a shorter prefix (1 case(s):
anchor/suggestion/unmappable-line-break-on-a-list-item-s-lazy-line)
killed tab after a quote marker repeated (1 case(s):
anchor/suggestion/unmappable-line-break-under-a-quote-marker-and-tab)
equiv item first line repeated (a list item's first line holds its
marker in the prefix region, so the lazy-line check refuses the break
anyway)
killed break with no ending written (1 case(s):
anchor/suggestion/unmappable-line-break-on-a-last-line-with-no-ending)
killed refused referrer enumerated (3 case(s):
anchor/referrer/refused-for-a-conflict-line-in-a-fence,
anchor/referrer/refused-for-conflicting-adjacent-re-pins,
anchor/referrer/refused-for-raw-html)
killed pins before the path (1 case(s):
anchor/transclusion-order/unresolvable-path-before-broken-pins)
killed broken pins ignored (9 case(s):
anchor/pins/refuses-a-path-pinned-twice,
anchor/pins/refuses-a-pin-after-a-blank-line,
anchor/pins/refuses-a-pin-after-a-comment-line)
killed missing target after the pin (1 case(s):
anchor/transclusion-order/document-missing-before-base-unavailable)
killed slug-only name not checked early (2 case(s):
anchor/transclusion-order/a-slug-only-name-before-base-unavailable,
anchor/transclusion-order/a-slug-only-name-before-the-pinned-binding)
killed slug match ignored (2 case(s):
anchor/transclusion-order/a-slug-only-name-before-base-unavailable,
anchor/transclusion-order/a-slug-only-name-before-the-pinned-binding)
killed marker name taken for slug-only (2 case(s):
anchor/merge/re-pins-of-two-targets-merge-cleanly,
anchor/transclusion/a-marker-name-that-is-also-a-slug-resolves)
killed pinned blob not required (2 case(s):
anchor/transclusion-order/an-unbound-name-after-base-unavailable,
anchor/transclusion/base-unavailable)
killed pinned name unchecked (1 case(s):
anchor/transclusion/a-pinned-name-that-binds-nothing)
killed orphaned pinned leaf passes (2 case(s):
anchor/transclusion-order/several-pinned-reasons-report-the-first-leaf,
anchor/transclusion/pinned-orphaned)
killed last pinned leaf decides (1 case(s):
anchor/transclusion-order/several-pinned-reasons-report-the-first-leaf)
killed anchor array taken for an object (1 case(s):
anchor/record/refuses-an-anchor-that-is-an-array)
killed inner link ignored (1 case(s):
anchor/transclusion/a-link-inside-link-text-is-not-one)
killed entity without semicolon decoded (1 case(s):
anchor/transclusion/an-entity-without-a-semicolon-is-not-decoded)
killed image read as a link (1 case(s):
anchor/transclusion/an-image-is-not-one)
killed prose with a title read as a link (1 case(s):
anchor/transclusion/prose-with-the-title-s-characters-is-not-one)
killed line ending after the title refused (1 case(s):
anchor/transclusion/a-line-ending-before-the-closing-parenthesis)
killed code block's final line ending written (1 case(s):
anchor/suggestion/unmappable-span-ending-in-a-code-block-s-final-line-ending)
killed reply's document unchecked (3 case(s):
anchor/reply/document-missing-before-a-missing-parent,
anchor/reply/refused-when-its-document-is-missing,
anchor/reply/refused-when-its-document-is-not-in-profile)
    
    91 killed, 0 survived, 1 equivalent, 0 stale, 0 broken

**No survivors.** The six holes the last run reported, one per review
fix,
are now killed by the cases #46 added for them.

**One equivalence claim**, "item first line repeated": a list item's
first
line holds its marker in the prefix region, so the lazy-line check
refuses a
break there anyway. kindkit flags a claim only when the suite kills the
mutant; it cannot show that one the suite does not kill is equivalent,
so
this rests on the argument.

## Where SPEC.md is still open: refused, not guessed

Each of these returns `{"status": "UNDECIDED", "question": ...}` for
that
record or transclusion, which no case can expect.

**Left open by §12:**

1. Transclusion shapes: a destination with no path, angle brackets,
another
title quoting form or a title with backslash escapes or entity
references
(one that decodes to `transclude` but is not written `"transclude"`), a
`transclude` link in a paragraph that holds other text, a transclusion
in
   a list item or block quote, no pin, and a document that transcludes
   itself, directly or through another.
2. Writing a suggestion back into a table.
3. An empty path segment, and a path naming a file that is not markdown
   (read here as not ending in `.md`).

**Not in §12, and not decided either:**

4. A destination with no `#name`.
5. Link text holding a code span or HTML, either of which can move where
the
   link text ends under CommonMark.
6. A line break written over a span whose first and last lines end or
are
prefixed differently: §11.7 writes each `\n` as the ending of "the line
   it breaks", and such a span has two candidates.
7. Where the open transclusion questions sit in step 1's order: the
empty
segment and self-transclusion after the path resolves, the non-markdown
   target before the pins, and no pin at step 1.6.
8. Inputs no case should produce: a record or referrer that is not in
the
   tree, and a record path in no notes directory.

**Cheap to decide but left `UNDECIDED`** (CommonMark settles them, at
the
cost of a full inline parser): prose that holds `"transclude")`
alongside a
real inline link elsewhere in the same paragraph.

**Readings taken, not refused**, worth confirming:

- When the span ends at a line break, write-back maps its end past the
next
line's prefixes, so `> a\n> b` with `a\n` replaced by `x` gives `> xb`.
- A paragraph holding `"transclude")` in a link-like shape is treated as
a
possible transclusion and refused as undecided where it is not exactly
one
  link. That refuses more than needed, and never resolves wrongly.

## Gates

    $ just check
    All checks passed!
    $ just test
    181 passed in 13.02s
    $ just conform
    0 failure(s) across 452 case(s) in the fixture tree
    $ just tree
    452 cases: 160 accept, 77 refuse, 215 anchor
    0 problem(s)
    $ just mutants
    --- the reader, reference/blockspec/document.py
    91 killed, 0 survived, 0 equivalent, 0 stale, 0 broken
    --- the resolver, reference/blockspec/anchor.py
    91 killed, 0 survived, 1 equivalent, 0 stale, 0 broken

## Break-checks of the unit tests

`tests/test_anchor.py` was break-checked by a sweep run locally (not
committed). It hashes the file before and after each mutation, reports
BROKEN when one does not land, runs the tests, and restores the file.
The
latest run gave `105 killed, 1 not`, the one being the equivalent mutant
above. The hook and the gate's scratch location: `3 killed, 0 not`.

## Not covered

- The thresholds 0.5 and 0.15 are as §11 states them; nothing here
  measures them on real history (0003 §12).
- Re-anchoring (§11.1) and the tool behaviours of §11.11 are not
  implemented: the format does not require them.

---------

Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
@CameronBrooks11

Copy link
Copy Markdown
Member Author

Landed through #45, which merged this branch's final head (d389dee) together with the resolver, as the landing order in conformance/cases/README.md describes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant