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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use *;
/// Whether a frontmatter document still carries the retired `links:` key.
///
/// The key is not part of the model at all, so it cannot be detected after
/// deserialization, and it must never be *read* as typed data (C-001). The
/// frontmatter is therefore also parsed as a plain YAML mapping and checked for
/// a **top-level** `links` key.
///
/// A raw line scan was tried first and was wrong in both directions: it matched
/// an indented `links:` under some other key, and it missed a quoted `"links":`
/// key. Matching the parsed top-level key gets both right, and a line inside a
/// `|`/`>` block scalar is unambiguously not a key.
///
/// A frontmatter that will not parse as a mapping falls back to a line scan.
/// That fallback is **indentation-aware**, matching only column-0 keys.
///
/// It has to be. `serde_yaml` rejects duplicate keys when deserializing into a
/// `Mapping` but *tolerates* a duplicate unknown key when deserializing into
/// `Issue`, so a file can reach the fallback and still parse. An earlier
/// indentation-blind fallback then matched the *nested* `links:` in
/// `meta:\n links: []` and reported a retired key on a file that has none —
/// the two code paths disagreeing about what "top-level" means, which is
/// precisely what the primary path was introduced to get right.
///
/// **An empty `links` is not a finding.** The key is reported because its *value*
/// is no longer readable, and an empty value has nothing to lose. Treating mere
/// presence as the defect made `tix lint` reject every issue file this repository
/// has ever written — they all carry a `links: []` from the old model — with no
/// way to make them valid short of hand-editing every file, because the mission
/// provides no migration for the key. A tool that declares its entire existing
/// corpus invalid on the strength of a field that was empty is not reporting a
/// problem, it is refusing to read its own data.
/// Every top-level key this model knows, in `Issue`'s declaration order, plus the
/// retired `links:` at the end.
///
/// The trailing `links` is not a field — see below — so "declaration order" holds
/// for the modelled keys and the retired key is simply appended.
///
/// Used by [`capture_extra_frontmatter`] to decide what is "unknown". Kept as an
/// explicit list rather than derived from the struct because a macro cannot read a
/// struct's own field names, and a mistake here is silent data loss: a key wrongly
/// listed as known is dropped on write, which is the bug #1361 exists to remove.
///
/// A mistake in the other direction is equally silent and was equally real: when
/// `parent` was added to [`Issue`] and not here, [`capture_extra_frontmatter`] took it
/// for an unknown key and the writer emitted `parent: 7` **twice** in one file — once
/// from the field, once spliced back from the capture. The file still parsed, so
/// nothing failed until a test compared bytes.
/// `known_frontmatter_keys_cover_every_written_key` derives the key set from a
/// fully-populated [`Issue`] and is the guard for it: adding a frontmatter field
/// without registering its key here is now a compile-clean, test-red change.
///
/// The retired `links:` is here on purpose. It is **recognised but never captured**,
/// so it is reported by its own `retired_links_key` finding and then dropped (C-001).
/// Capturing it would round-trip it and quietly undo the typed-relationship
/// migration — the data would come back, but the model would still not understand it.
pub const KNOWN_FRONTMATTER_KEYS: & = &;
/// Capture every top-level frontmatter key this model does not know, as
/// `key -> raw source block` (#1361).
///
/// **Raw block, not a parsed value.** Re-serialising a parsed value would re-quote
/// and re-indent it, so a key written one way would come back another way and every
/// write would produce a diff. Splicing the original bytes keeps the round trip
/// byte-stable for content this model has no opinion about — which is the whole
/// point of preserving it.
///
/// A block is the key's own line plus every following line that continues it: an
/// indented line, or a block-sequence item. The scan is textual on purpose. A YAML
/// parse would be tidier, but the raw text is what has to be reproduced, and
/// re-deriving block boundaries from a parse is how a nested list ends up half
/// captured.
///
/// `links` is skipped. See [`KNOWN_FRONTMATTER_KEYS`].