1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! The canonical SLD spec-reference grammar (DOC-38 §2.1–§2.2).
//!
//! # Spec References
//!
//! - [`SPEC-SLD-02~draft`](docs/specs/spec-linked-documentation.md#SPEC-SLD-02~draft)
//!
//! Why: DOC-38 fixes ONE reference grammar for every language — a
//! `(spec-ID, path, anchor)` triple with a self-checking anchor. Centralising
//! the `SPEC-{SUBSYSTEM}-{NN}~{rev}` id pattern and the revision helpers here
//! means the resolver (`intent_source::spec_resolve`) and any linter parse the
//! same grammar instead of drifting apart (§2.4 rationale, goal G1).
//! What: the compiled id/reference regexes (§2.1–§2.2), `is_valid_spec_id`, and
//! the revision splitters `revision_of` / `base_id` — the single source both
//! this module and `spec_resolve` share.
//! Test: `super::tests::grammar_*`.
use OnceLock;
use Regex;
/// The SLD spec-ID pattern body, shared by the id and reference regexes.
///
/// Why: DOC-38 §2.1 fixes the id shape once; expressing it as one string keeps
/// the standalone-id matcher and the three-capture reference matcher provably
/// consistent (they cannot drift to different id grammars).
/// What: `SPEC-{SUBSYSTEM}-{NN}~{rev}` where SUBSYSTEM is
/// `[A-Z0-9][A-Z0-9-]*`, NN is `[0-9]{2,}` (zero-padded, opaque), and REV is the
/// open lowercase token `[a-z][a-z0-9]*` (`draft`, `v1`, `approved`, …).
/// Test: `super::tests::grammar_valid_ids` / `grammar_rejects_malformed`.
const SPEC_ID_BODY: &str = r"SPEC-[A-Z0-9][A-Z0-9-]*-[0-9]{2,}~[a-z][a-z0-9]*";
/// The compiled anchored matcher for a standalone spec ID (§2.1).
///
/// Why: validating an id or an anchor in isolation (e.g. a `{#SPEC-…}` heading
/// marker, or a frontmatter `id:` field) needs an anchored match so a partial
/// substring does not pass.
/// What: `^{SPEC_ID_BODY}$`, compiled once (the workspace `OnceLock` exception).
/// Test: `super::tests::grammar_valid_ids`.
/// The compiled three-capture SLD reference matcher (§2.2).
///
/// Why: §2.2 admits two inline surface forms (a Markdown link and a bare
/// `id path#anchor`) but parses both with ONE regex that captures the three
/// fields and ignores link/comment punctuation between them.
/// What: captures `(id, path.md, anchor)` — the id, a repo-root-relative `.md`
/// path, and the anchor after `#`, tolerating arbitrary same-line noise between
/// the id and the path (`[^\n]*?`, lazy). Compiled once.
/// Test: `super::tests::grammar_reference_both_forms`.
/// True when `id` is a well-formed spec ID per DOC-38 §2.1.
///
/// Why: the linter's id-grammar check and anchor validation both need a single
/// authoritative "is this a valid `SPEC-…~rev` id?" predicate so they agree.
/// What: anchored match against `SPEC_ID_BODY` — subsystem, a 2+-digit opaque
/// counter, and an open lowercase revision token. A resolver MUST NOT reject a
/// well-formed `~<token>` revision it does not recognise (§2.1), so no revision
/// allowlist is applied.
/// Test: `super::tests::grammar_valid_ids`, `grammar_rejects_malformed`.
/// Split a `SPEC-…~rev` id/anchor into its revision (the part after the last `~`).
///
/// Why: revision-drift awareness (§4.4) and revision-tolerant section matching
/// both compare the revision independently of the base id. This is the single
/// canonical implementation — `intent_source::spec_resolve` re-exports it rather
/// than defining its own (DOC-38 one-grammar consolidation).
/// What: returns the substring after the last `~`, or `None` when the id carries
/// no `~` suffix.
/// Test: `super::tests::grammar_revision_of`.
/// The revision-insensitive base of a `SPEC-…~rev` id/anchor.
///
/// Why: matching a referencing anchor (`SPEC-X-01~v1`) to a section heading
/// (`SPEC-X-01~v2`) must key on the stable base id, not the revision — so a
/// reference still resolves across a revision bump (§4.4; drift is flagged, not
/// enforced — §1.3). The single canonical implementation shared with
/// `spec_resolve`.
/// What: returns the substring before the last `~`, or the whole id when there
/// is no `~`.
/// Test: `super::tests::grammar_base_id`.
/// True when a captured reference path could escape the repository root.
///
/// Why: a repo-root-relative reference path (§2.1) must never resolve outside
/// the repository. `..` traversal is one escape vector; an **absolute** path
/// (`/etc/passwd`, or a Windows drive-prefixed `C:\...`) is another and more
/// dangerous one — `PathBuf::join` silently *discards* its base when the
/// argument is absolute, so a naive `root.join(path)` reader would read
/// wherever the declared path points, using the repository as a confused
/// deputy (a malicious or buggy `spec_refs:`/inline reference becomes a
/// filesystem-read oracle). The `regex` crate has no look-around, so both
/// checks are a post-match filter (mirrors the `intent_source` defence-in-depth
/// guard). Callers reading the filesystem MUST apply a second, independent
/// guard at the read site (canonicalize + prefix-check) — this function is
/// the first layer, not the only one.
/// What: returns `true` when any `/`- or `\`-split segment equals `..`, when
/// `path` begins with `/` or `\`, when `path` begins with a Windows drive
/// prefix (`C:`), or when [`Path::is_absolute`] reports true for the host
/// platform's own notion of absolute (belt-and-suspenders for native builds).
/// Test: `super::tests::grammar_rejects_traversal`,
/// `super::tests::grammar_rejects_absolute`.