runlist 0.0.0 → 0.94.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1112 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +296 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/public-inspection.mjs +19 -0
  136. package/src/query.mjs +977 -0
  137. package/src/record-action.mjs +11 -0
  138. package/src/record-create.mjs +82 -0
  139. package/src/reference-planner.mjs +388 -0
  140. package/src/reference-values.mjs +11 -0
  141. package/src/rename.mjs +107 -0
  142. package/src/render.mjs +678 -0
  143. package/src/roadmap.mjs +184 -0
  144. package/src/runlist.mjs +944 -0
  145. package/src/section.mjs +225 -0
  146. package/src/semantic-search-client.mjs +58 -0
  147. package/src/ship.mjs +166 -0
  148. package/src/show.mjs +101 -0
  149. package/src/skill-drift.mjs +98 -0
  150. package/src/source-editor.mjs +491 -0
  151. package/src/stages.mjs +73 -0
  152. package/src/state-migration.mjs +123 -0
  153. package/src/stats.mjs +200 -0
  154. package/src/status-metadata.mjs +87 -0
  155. package/src/statuses.mjs +721 -0
  156. package/src/summary.mjs +74 -0
  157. package/src/surfaces.mjs +28 -0
  158. package/src/sync-status.mjs +316 -0
  159. package/src/template-store.mjs +62 -0
  160. package/src/update.mjs +338 -0
  161. package/src/use.mjs +58 -0
  162. package/src/util.mjs +337 -0
  163. package/src/validate.mjs +795 -0
  164. package/src/watch.mjs +56 -0
  165. package/src/xref.mjs +406 -0
  166. package/src/yardstick-command.mjs +178 -0
  167. package/src/yardstick.mjs +112 -0
@@ -0,0 +1,244 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
3
+
4
+ export const NATIVE_RECORD_SCHEMA = 'runlist.record/v1';
5
+ export const NATIVE_RECORD_METADATA_BYTES = 256 * 1024;
6
+ const schema = JSON.parse(readFileSync(new URL('../assets/schemas/runlist-record-v1.schema.json', import.meta.url), 'utf8'));
7
+ export const nativeRecordSchema = () => structuredClone(schema);
8
+
9
+ // This validator implements only the keywords used by the bundled schema.
10
+ // It is not a general JSON Schema engine. External tools can use the same
11
+ // document with a Draft 2020-12 implementation.
12
+ function schemaErrors(value, rule, at = '$') {
13
+ const errors = [];
14
+ const add = message => errors.push({ code: 'record-schema', path: at, message });
15
+ if (rule.$ref) return schemaErrors(value, schema.$defs[rule.$ref.split('/').at(-1)], at);
16
+ if (rule.allOf) for (const child of rule.allOf) errors.push(...schemaErrors(value, child, at));
17
+ if (rule.oneOf && rule.oneOf.filter(child => schemaErrors(value, child, at).length === 0).length !== 1) {
18
+ add('Must match exactly one record kind.');
19
+ const selected = rule.oneOf.find(child => child.properties?.type?.const === value?.type);
20
+ if (selected) errors.push(...schemaErrors(value, selected, at));
21
+ }
22
+ if (Object.hasOwn(rule, 'const') && value !== rule.const) add(`Expected ${JSON.stringify(rule.const)}.`);
23
+ if (rule.enum && !rule.enum.includes(value)) add(`Expected one of ${rule.enum.join(', ')}.`);
24
+ const matches = type => type === 'object' ? value !== null && typeof value === 'object' && !Array.isArray(value)
25
+ : type === 'array' ? Array.isArray(value) : type === 'integer' ? Number.isSafeInteger(value) : typeof value === type;
26
+ if (rule.type && !matches(rule.type)) { add(`Expected ${rule.type}.`); return errors; }
27
+ if (typeof value === 'string') {
28
+ if (rule.minLength && [...value].length < rule.minLength) add('Must not be empty.');
29
+ if (rule.pattern && !new RegExp(rule.pattern).test(value)) add('Value does not match the required pattern.');
30
+ if (rule.format === 'date-time') {
31
+ const valid = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(value) && Number.isFinite(Date.parse(value));
32
+ if (!valid || new Date(value).toISOString().slice(0, 19) !== value.slice(0, 19)) add('Expected a valid UTC timestamp.');
33
+ }
34
+ }
35
+ if (typeof value === 'number' && rule.minimum !== undefined && value < rule.minimum) add(`Must be at least ${rule.minimum}.`);
36
+ if (Array.isArray(value) && rule.items) value.forEach((item, i) => errors.push(...schemaErrors(item, rule.items, `${at}[${i}]`)));
37
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
38
+ for (const key of rule.required ?? []) if (!Object.hasOwn(value, key)) errors.push({ code: 'record-schema', path: `${at}.${key}`, message: 'Required field is missing.' });
39
+ for (const [key, child] of Object.entries(rule.properties ?? {})) if (Object.hasOwn(value, key)) errors.push(...schemaErrors(value[key], child, `${at}.${key}`));
40
+ }
41
+ return errors;
42
+ }
43
+
44
+ function strictJson(text) {
45
+ if (Buffer.byteLength(text) > NATIVE_RECORD_METADATA_BYTES) throw new Error('Record metadata exceeds 256 KiB.');
46
+ const value = JSON.parse(text);
47
+ let i = 0;
48
+ const whitespace = () => { while (/\s/.test(text[i] ?? '') && i < text.length) i++; };
49
+ const string = () => {
50
+ const start = i++;
51
+ while (i < text.length) {
52
+ if (text[i] === '\\') { i += 2; continue; }
53
+ if (text[i++] === '"') return JSON.parse(text.slice(start, i));
54
+ }
55
+ };
56
+ const visit = depth => {
57
+ if (depth > 64) throw new Error('Record metadata exceeds 64 nesting levels.');
58
+ whitespace();
59
+ const token = text[i];
60
+ if (token === '"') { string(); return; }
61
+ if (token === '{' || token === '[') {
62
+ const object = token === '{';
63
+ const end = object ? '}' : ']';
64
+ const keys = new Set();
65
+ i++; whitespace();
66
+ while (text[i] !== end) {
67
+ if (object) {
68
+ const key = string();
69
+ if (keys.has(key)) throw new Error(`Duplicate JSON key ${JSON.stringify(key)} at offset ${i}.`);
70
+ keys.add(key); whitespace(); i++;
71
+ }
72
+ visit(depth + 1); whitespace();
73
+ if (text[i] !== ',') break;
74
+ i++; whitespace();
75
+ }
76
+ i++;
77
+ } else {
78
+ while (i < text.length && !/[\s,\]}]/.test(text[i])) i++;
79
+ }
80
+ };
81
+ visit(0);
82
+ return value;
83
+ }
84
+
85
+ export function nativePlanItems(body) {
86
+ const items = [];
87
+ const lines = body.split('\n');
88
+ let fence = null;
89
+ for (let i = 0; i < lines.length; i++) {
90
+ const marker = lines[i].match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
91
+ if (marker) {
92
+ if (!fence) fence = marker[1];
93
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = null;
94
+ continue;
95
+ }
96
+ if (fence) continue;
97
+ const anchor = lines[i].match(/^ {0,3}<!-- runlist:item (item:[^\s]+) -->\s*$/);
98
+ if (!anchor) continue;
99
+ const next = lines[i + 1] ?? '';
100
+ const task = next.match(/^ {0,3}[-*+] \[([ xX])\] (.+)$/);
101
+ const phase = next.match(/^(#{2,6}) (.+)$/);
102
+ items.push({ id: anchor[1], kind: task ? 'task' : phase ? 'phase' : 'invalid', complete: task ? task[1].toLowerCase() === 'x' : null, line: i + 1 });
103
+ }
104
+ return items;
105
+ }
106
+
107
+ function envelopeErrors(text) {
108
+ const errors = [];
109
+ let mode = null;
110
+ let blockIndent = null;
111
+ for (const [i, line] of text.split('\n').entries()) {
112
+ if (!line.trim()) continue;
113
+ const key = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
114
+ if (key) {
115
+ mode = /^[>|][-+]?\s*$/.test(key[2]) ? 'block' : !key[2].trim() ? 'array' : null;
116
+ blockIndent = null;
117
+ continue;
118
+ }
119
+ if (line.startsWith('#')) { mode = null; continue; }
120
+ const indent = line.match(/^ */)[0].length;
121
+ if (mode === 'block' && indent > 0 && (blockIndent === null || indent >= blockIndent)) {
122
+ blockIndent ??= indent;
123
+ continue;
124
+ }
125
+ if (mode === 'array' && /^ {2}- (?![A-Za-z0-9_-]+:\s).+/.test(line)) continue;
126
+ errors.push({ code: 'unsupported-frontmatter', path: `$.frontmatter.line[${i + 1}]`, message: 'Use the flat frontmatter subset; nested metadata belongs in the literal JSON record_data block.' });
127
+ }
128
+ return errors;
129
+ }
130
+
131
+ export function validateNativeRecord(record, { body = '' } = {}) {
132
+ const errors = schemaErrors(record, schema);
133
+ if (errors.length) return errors;
134
+ const data = record.record_data;
135
+ const add = (path, message, code = 'record-invariant') => errors.push({ code, path, message });
136
+ if (data.requires?.length) add('$.record_data.requires', 'Unknown required features; expose source but do not perform managed edits.', 'unsupported-feature');
137
+ const declarations = new Set([record.id]);
138
+ for (const field of ['evidence','relations','history','options','rulings','resolutions']) {
139
+ for (const entry of data[field] ?? []) {
140
+ if (declarations.has(entry.id)) add(`$.record_data.${field}`, `Duplicate declared ID ${entry.id}.`);
141
+ declarations.add(entry.id);
142
+ }
143
+ }
144
+ const items = record.type === 'plan' ? nativePlanItems(body) : [];
145
+ const itemPattern = schema.$defs.target.properties.id.pattern.replace('(plan|item|flag|decision|evidence)', '(item)');
146
+ for (const item of items) {
147
+ if (item.kind === 'invalid' || !new RegExp(itemPattern).test(item.id)) add('$.body', `Invalid item anchor at body line ${item.line}.`);
148
+ if (declarations.has(item.id)) add('$.body', `Duplicate declared ID ${item.id}.`);
149
+ declarations.add(item.id);
150
+ }
151
+ const ownedSources = new Set([record.id, ...items.map(item => item.id)]);
152
+ const evidence = new Map(data.evidence.map(e => [e.id, e]));
153
+ const checkEvidence = (ids, at) => { for (const id of ids ?? []) if (!evidence.has(id)) add(at, `Unknown local evidence ID ${id}.`); };
154
+ for (const alias of data.aliases) if (data.aliases.filter(a => a.value === alias.value && a.namespace === alias.namespace).length > 1) add('$.record_data.aliases', 'Duplicate alias in the same namespace.');
155
+ const relativePath = value => !value.startsWith('/') && !value.includes('\\') && !/^[A-Za-z]:/.test(value) && !value.split('/').some(part => !part || part === '.' || part === '..');
156
+ for (const e of data.evidence) {
157
+ if (['code','document'].includes(e.kind)) {
158
+ if (!e.repository_id || !e.path || !e.revision) add('$.record_data.evidence', 'Code/document evidence requires repository, path and observed revision.');
159
+ }
160
+ if (e.path && !relativePath(e.path)) add('$.record_data.evidence', 'Evidence paths must be normalized repository-relative paths.');
161
+ if (e.revision?.kind === 'git' && !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(e.revision.value ?? '')) add('$.record_data.evidence', 'Git revisions use the full commit hash.');
162
+ if (e.revision?.kind === 'content' && !/^[0-9a-f]{64}$/.test(e.revision.value ?? '')) add('$.record_data.evidence', 'Content revisions use a SHA-256 digest.');
163
+ if (e.revision?.kind === 'unknown' && !e.revision.reason) add('$.record_data.evidence', 'Unknown revisions require an explicit reason.');
164
+ if (e.revision?.kind === 'git' && e.revision.dirty && !e.anchor?.sha256) add('$.record_data.evidence', 'Dirty Git observations also require an observed content fingerprint.');
165
+ if (e.anchor?.end_line && (!e.anchor.start_line || e.anchor.end_line < e.anchor.start_line)) add('$.record_data.evidence', 'Invalid observed line range.');
166
+ if (e.kind === 'url' && !e.uri) add('$.record_data.evidence', 'URL evidence requires a URI.');
167
+ if (['command','test'].includes(e.kind) && (!e.argv?.length || e.exit === undefined)) add('$.record_data.evidence', 'Command/test evidence requires argv and exit status.');
168
+ }
169
+ for (const relation of data.relations) {
170
+ if (!ownedSources.has(relation.source_id)) add('$.record_data.relations', 'A relation source must belong to this record.');
171
+ if (relation.target.path && !relativePath(relation.target.path)) add('$.record_data.relations', 'Navigation hints must be repository-relative paths.');
172
+ if (relation.kind === 'blocks' && !/^(plan|item):/.test(relation.target.id)) add('$.record_data.relations', 'Blocks targets execution: a plan or plan item.');
173
+ if (relation.kind === 'verified-by' && !relation.target.id.startsWith('evidence:')) add('$.record_data.relations', 'Verified-by targets evidence.');
174
+ if (relation.kind === 'resolved-by' && (!relation.source_id.startsWith('flag:') || !/^(plan|item|decision|evidence):/.test(relation.target.id))) add('$.record_data.relations', 'Resolved-by links a flag to work, a decision, or evidence.');
175
+ if (relation.kind === 'supersedes' && (relation.target.id === relation.source_id || relation.target.id.split(':')[0] !== relation.source_id.split(':')[0])) add('$.record_data.relations', 'Supersedes requires a distinct target of the same kind.');
176
+ }
177
+ for (const entry of [...data.history, ...(data.resolutions ?? []), ...(data.recommendations ?? []), ...(data.rulings ?? [])]) checkEvidence(entry.evidence_ids, '$.record_data');
178
+ if (record.type === 'flag') {
179
+ if (data.source.kind === 'check' && !data.source.rule_id) add('$.record_data.source', 'Check findings require a stable rule ID.');
180
+ if (data.triage.disposition !== 'unreviewed' && (!data.triage.by || !data.triage.at || !data.triage.reason)) add('$.record_data.triage', 'Triage requires actor, timestamp and reason.');
181
+ if ((record.status === 'rejected') !== (data.triage.disposition === 'rejected')) add('$.status', 'Rejected status and triage must agree.');
182
+ const resolution = data.resolutions.find(r => r.id === data.active_resolution_id);
183
+ if (record.status === 'resolved' ? !resolution : data.active_resolution_id !== undefined) add('$.record_data.active_resolution_id', 'Only a resolved flag has an active resolution, and it must exist.');
184
+ const observations = data.evidence.filter(e => ['code','document'].includes(e.kind));
185
+ const successfulProof = proof => ['command','test'].includes(proof?.kind) && proof.exit === 0
186
+ && proof.repository_id && ['git','content'].includes(proof.revision?.kind)
187
+ && Date.parse(proof.observed_at) <= Date.parse(resolution?.at)
188
+ && observations.filter(e => e.repository_id === proof.repository_id).every(e => Date.parse(proof.observed_at) >= Date.parse(e.observed_at));
189
+ if (resolution && ['check','test'].includes(resolution.method) && !resolution.evidence_ids.some(id => successfulProof(evidence.get(id)))) add('$.record_data.resolutions', 'Automated resolution requires successful command/test evidence pinned to a snapshot after the observations.');
190
+ if (resolution?.method === 'manual' && !['human','agent'].includes(resolution.by.kind)) add('$.record_data.resolutions', 'Manual resolution must identify its human or agent actor.');
191
+ if (resolution?.method === 'check' && data.source.kind !== 'check') add('$.record_data.resolutions', 'Only the originating check resolves a check-owned finding automatically.');
192
+ if (resolution?.method === 'check') {
193
+ const proofs = resolution.evidence_ids.map(id => evidence.get(id)).filter(p => successfulProof(p) && p.kind === 'command'
194
+ && p.rule_id === data.source.rule_id && p.coverage?.complete === true && p.coverage.repository_id === p.repository_id);
195
+ const subjects = observations.length ? observations : [{repository_id:data.repository_id}];
196
+ if (!subjects.every(subject => proofs.some(proof => proof.repository_id === subject.repository_id
197
+ && (proof.coverage.kind === 'repository' || (proof.coverage.kind === 'files' && subject.path && proof.coverage.paths.includes(subject.path)))))) {
198
+ add('$.record_data.resolutions', 'Check resolution requires complete coverage of the original evidence and the same rule.');
199
+ }
200
+ }
201
+ }
202
+ if (record.type === 'decision') {
203
+ const options = new Set(data.options.map(o => o.id));
204
+ for (const recommendation of data.recommendations) if (!options.has(recommendation.option_id)) add('$.record_data.recommendations', 'Recommendation names an unknown option.');
205
+ const earlier = new Set();
206
+ for (const ruling of data.rulings) {
207
+ if (ruling.kind === 'select' && !options.has(ruling.option_id)) add('$.record_data.rulings', 'Ruling names an unknown option.');
208
+ if (ruling.kind === 'withdraw' && ruling.option_id !== undefined) add('$.record_data.rulings', 'Withdrawal does not select an option.');
209
+ if (ruling.by.kind === 'human' ? ruling.authority.basis !== 'human' : ruling.by.kind !== 'agent' || ruling.authority.basis !== 'delegated' || !ruling.authority.grant_ref) add('$.record_data.rulings', 'Ruling provenance must identify a human or an explicit delegation.');
210
+ if (ruling.supersedes_ruling_id && !earlier.has(ruling.supersedes_ruling_id)) add('$.record_data.rulings', 'A ruling can supersede only a preceding ruling in this record.');
211
+ earlier.add(ruling.id);
212
+ }
213
+ const ruling = data.rulings.find(r => r.id === data.active_ruling_id);
214
+ const superseded = new Set(data.rulings.map(r => r.supersedes_ruling_id).filter(Boolean));
215
+ if (ruling && data.rulings.filter(r => !superseded.has(r.id)).length > 1) add('$.record_data.active_ruling_id', 'Competing rulings require an explicit superseding ruling; never select by timestamp.');
216
+ if (ruling && superseded.has(ruling.id)) add('$.record_data.active_ruling_id', 'The active ruling cannot be superseded.');
217
+ if (['ruled','closed'].includes(record.status) ? !ruling : data.active_ruling_id !== undefined) add('$.record_data.active_ruling_id', 'Only ruled/closed decisions have an active ruling, and it must exist.');
218
+ if (record.status === 'ruled' && (ruling?.kind !== 'select' || data.options.length < 2)) add('$.status', 'A ruled decision selects one of at least two options.');
219
+ if (record.status === 'held' && !data.hold) add('$.record_data.hold', 'A held decision requires actor, timestamp and reason.');
220
+ }
221
+ return errors;
222
+ }
223
+
224
+ // Read-only opt-in parser. Legacy CLI writers are deliberately not connected
225
+ // to this format until their mutation/migration contracts are implemented.
226
+ export function parseNativeRecord(raw) {
227
+ const result = { ok: false, record: null, body: '', source: raw, diagnostics: [] };
228
+ const add = (code, path, message) => result.diagnostics.push({ code, path, message });
229
+ const { frontmatter, body } = extractFrontmatter(raw);
230
+ result.body = body;
231
+ const warnings = [];
232
+ const fm = parseSimpleFrontmatter(frontmatter, warnings);
233
+ if (fm.record_schema !== NATIVE_RECORD_SCHEMA) { add('unsupported-schema', '$.record_schema', 'Expected runlist.record/v1; retain source without managed edits.'); return result; }
234
+ if (warnings.length) { for (const warning of warnings) add('duplicate-frontmatter', `$.${warning.key}`, warning.message); return result; }
235
+ result.diagnostics.push(...envelopeErrors(frontmatter));
236
+ if (result.diagnostics.length) return result;
237
+ if (!/^record_data:\s*\|[-+]?\s*$/m.test(frontmatter)) { add('record-json', '$.record_data', 'Nested data must use JSON in a literal record_data block.'); return result; }
238
+ try { fm.record_data = strictJson(fm.record_data); }
239
+ catch (err) { add('record-json', '$.record_data', err.message); return result; }
240
+ result.diagnostics.push(...validateNativeRecord(fm, { body }));
241
+ result.record = fm;
242
+ result.ok = result.diagnostics.length === 0;
243
+ return result;
244
+ }