diff --git a/docs/releases/0.2.0.md b/docs/releases/0.2.0.md index 9193606..ba67b2f 100644 --- a/docs/releases/0.2.0.md +++ b/docs/releases/0.2.0.md @@ -30,7 +30,7 @@ - **Match rules:** explicit-type create match-rule enforcement scoped to Level 2+. - **Datetime portability:** `settings.timezone` added; applies to `now()`/`today()` and naive comparisons. - **Expressions:** `reduce()` requires explicit `init`; list methods `value/index/acc` shadow frontmatter fields. -- **Links:** `ambiguous_link` is an error on ID ambiguity, warning after tiebreakers. +- **Links:** `ambiguous_link` fails closed for duplicate ID or filename matches; providers do not choose an arbitrary target. - **Non-scalar ordering:** `order_by` on lists/objects now specifies deterministic ordering rules. ## Removed / Deprecated diff --git a/tests/level-4/links-non-markdown.yaml b/tests/level-4/links-non-markdown.yaml index 44c384b..f6b9e51 100644 --- a/tests/level-4/links-non-markdown.yaml +++ b/tests/level-4/links-non-markdown.yaml @@ -349,10 +349,10 @@ groups: field: "assignee" # ============================================================================= - # Group 6: Tiebreaker resolution order (§8.4) + # Group 6: Ambiguous simple-name resolution fails closed (§8.4) # ============================================================================= - - name: "simple name link tiebreaker resolution" + - name: "ambiguous simple name link resolution" spec_ref: "§8.4" setup: @@ -370,7 +370,7 @@ groups: --- tests: - - name: "tiebreaker prefers same directory" + - name: "same-directory duplicate does not create an implicit preference" setup: files: tasks/source.md: | @@ -394,9 +394,9 @@ groups: path: "tasks/source.md" field: "ref" expect: - resolved_path: "tasks/readme.md" + resolved_path: null - - name: "tiebreaker prefers shortest path when no same-dir match" + - name: "shorter duplicate path does not create an implicit preference" setup: files: tasks/source.md: | @@ -420,9 +420,9 @@ groups: path: "tasks/source.md" field: "ref" expect: - resolved_path: "docs/readme.md" + resolved_path: null - - name: "tiebreaker uses alphabetical as final fallback" + - name: "lexical order does not create an implicit preference" setup: files: root-source.md: | @@ -446,7 +446,7 @@ groups: path: "root-source.md" field: "ref" expect: - resolved_path: "aaa/target.md" + resolved_path: null # ============================================================================= # Group 7: Markdown link resolution — relative to containing file (§8.2.2) diff --git a/tests/level-4/links-resolution.yaml b/tests/level-4/links-resolution.yaml index 75ea613..44ef2d4 100644 --- a/tests/level-4/links-resolution.yaml +++ b/tests/level-4/links-resolution.yaml @@ -366,10 +366,10 @@ groups: resolved_path: "people/alice.md" # ============================================================================= - # Group 6: Tiebreaker rules for simple name resolution + # Group 6: Ambiguous simple-name resolution fails closed # ============================================================================= - - name: "simple name tiebreakers" + - name: "same-directory ambiguity" spec_ref: "§8.4" setup: @@ -400,15 +400,15 @@ groups: --- tests: - - name: "tiebreaker prefers same directory" + - name: "same-directory duplicate remains ambiguous" operation: resolve_link input: path: "tasks/source.md" field: ref expect: - resolved_path: "tasks/target.md" + resolved_path: null - - name: "simple name tiebreaker - shortest path" + - name: "path-length ambiguity" spec_ref: "§8.4" setup: @@ -439,15 +439,15 @@ groups: --- tests: - - name: "tiebreaker prefers shortest path when not in same directory" + - name: "shorter path does not win an ambiguous simple name" operation: resolve_link input: path: "source.md" field: ref expect: - resolved_path: "a/target.md" + resolved_path: null - - name: "simple name tiebreaker - alphabetical" + - name: "lexical-order ambiguity" spec_ref: "§8.4" setup: @@ -478,13 +478,13 @@ groups: --- tests: - - name: "tiebreaker uses alphabetical order for equal-length paths" + - name: "lexical order does not win an ambiguous simple name" operation: resolve_link input: path: "source.md" field: ref expect: - resolved_path: "alpha/target.md" + resolved_path: null # ============================================================================= # Group 7: Ambiguous link detection diff --git a/v0.2/08-links.md b/v0.2/08-links.md index f0d908c..d9f5bc3 100644 --- a/v0.2/08-links.md +++ b/v0.2/08-links.md @@ -167,11 +167,15 @@ Given a link value and the path of the file containing it: - If exactly one match, resolve to it - If multiple matches, resolution MUST fail with `ambiguous_link` - **Filename match pass**: If no `id_field` match exists, search scoped **markdown files (records)** by filename - - If multiple filename candidates match, apply tiebreakers in order: - a. **Same directory**: Prefer a file in the same directory as the referring file - b. **Shortest path**: Prefer the file with the shortest path (closest to collection root) - c. **Alphabetical**: Sort candidate paths lexicographically and take the first - - If multiple candidates remain after all tiebreakers, resolve to `null` and emit an `ambiguous_link` warning + - If exactly one match exists, resolve to it + - If multiple matches exist, resolution MUST fail closed: resolve to `null` and emit `ambiguous_link` + + Implementations MUST NOT choose among duplicate simple-name matches by + referring directory, path length, scan order, or lexical order. Those + choices are unstable across providers and can silently bind a relationship + to a different record after an unrelated move or import. Authors can make + the target unambiguous with a path, a unique `id_field`, or a narrower typed + target constraint. > **Note:** Non-markdown files are not records and are not candidates for simple name matching (per [§2.9](./02-collection-layout.md)). A wikilink `[[diagram]]` will not match `diagram.png` — only markdown files are searched by `id_field` and filename. Non-markdown files are only resolved when explicitly referenced by path (with extension or relative path). diff --git a/v0.2/appendix-c-error-codes.md b/v0.2/appendix-c-error-codes.md index 54ccd72..857dc24 100644 --- a/v0.2/appendix-c-error-codes.md +++ b/v0.2/appendix-c-error-codes.md @@ -63,7 +63,7 @@ This appendix defines standard error codes for validation issues and operation e | `invalid_link` | Link cannot be parsed | Malformed wikilink | | `link_not_found` | Link target doesn't exist | Target "[[missing]]" not found | | `link_wrong_type` | Target is wrong type | Expected person, found task | -| `ambiguous_link` | Multiple candidates for a simple-name link (error on ID ambiguity, warning if still ambiguous after tiebreakers) | "[[note]]" matches notes/note.md and archive/note.md | +| `ambiguous_link` | Multiple candidates exist at the first populated simple-name lookup priority; resolution fails closed | "[[note]]" matches notes/note.md and archive/note.md | ### Date/Time Errors