@descent-vtt/spec-brief 0.1.0
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.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/bin/spec-brief.js +19 -0
- package/dist/apply.d.ts +26 -0
- package/dist/apply.js +71 -0
- package/dist/apply.js.map +1 -0
- package/dist/archive.d.ts +81 -0
- package/dist/archive.js +333 -0
- package/dist/archive.js.map +1 -0
- package/dist/brief.d.ts +60 -0
- package/dist/brief.js +152 -0
- package/dist/brief.js.map +1 -0
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +411 -0
- package/dist/cli.js.map +1 -0
- package/dist/collisions.d.ts +50 -0
- package/dist/collisions.js +127 -0
- package/dist/collisions.js.map +1 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.js +353 -0
- package/dist/config.js.map +1 -0
- package/dist/corpus.d.ts +41 -0
- package/dist/corpus.js +154 -0
- package/dist/corpus.js.map +1 -0
- package/dist/engine.d.ts +121 -0
- package/dist/engine.js +276 -0
- package/dist/engine.js.map +1 -0
- package/dist/frontmatter.d.ts +68 -0
- package/dist/frontmatter.js +311 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/fs.d.ts +59 -0
- package/dist/fs.js +189 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +59 -0
- package/dist/git.js +131 -0
- package/dist/git.js.map +1 -0
- package/dist/glob.d.ts +79 -0
- package/dist/glob.js +465 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity.d.ts +11 -0
- package/dist/integrity.js +20 -0
- package/dist/integrity.js.map +1 -0
- package/dist/links.d.ts +38 -0
- package/dist/links.js +142 -0
- package/dist/links.js.map +1 -0
- package/dist/lint.d.ts +38 -0
- package/dist/lint.js +90 -0
- package/dist/lint.js.map +1 -0
- package/dist/markdown.d.ts +65 -0
- package/dist/markdown.js +274 -0
- package/dist/markdown.js.map +1 -0
- package/dist/plugins.d.ts +16 -0
- package/dist/plugins.js +77 -0
- package/dist/plugins.js.map +1 -0
- package/dist/report.d.ts +38 -0
- package/dist/report.js +244 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +58 -0
- package/dist/rules.js +448 -0
- package/dist/rules.js.map +1 -0
- package/dist/scaffold.d.ts +25 -0
- package/dist/scaffold.js +81 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/schema.d.ts +47 -0
- package/dist/schema.js +195 -0
- package/dist/schema.js.map +1 -0
- package/dist/text.d.ts +40 -0
- package/dist/text.js +95 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +76 -0
- package/schema.json +321 -0
package/dist/archive.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archival and its reverse, planned.
|
|
3
|
+
*
|
|
4
|
+
* A plan is a pure function of the corpus and a request: what would be
|
|
5
|
+
* refused and why, the text every touched file would hold afterwards, and the
|
|
6
|
+
* operations that get there. Nothing is written here. `--dry-run` prints a
|
|
7
|
+
* plan; `apply.ts` executes one as a transaction. Splitting the two is what
|
|
8
|
+
* makes the ceremony testable without a disk and previewable without risk.
|
|
9
|
+
*/
|
|
10
|
+
import { BANNER_CLOSE, BANNER_OPEN, lineOfField } from './brief.js';
|
|
11
|
+
import { resolveDependency } from './corpus.js';
|
|
12
|
+
import { readFrontMatter, removeEntry, renderScalar, setEntry } from './frontmatter.js';
|
|
13
|
+
import { matchGlob, parseGlob } from './glob.js';
|
|
14
|
+
import { INTEGRITY_FIELD, integrityOf } from './integrity.js';
|
|
15
|
+
import { dirOf, linesLinkingTo, normalisePath, rewriteLinks } from './links.js';
|
|
16
|
+
import { fillTemplate, joinLines, labelMatches, lineEnding, templateHoles } from './text.js';
|
|
17
|
+
function problem(brief, rule, severity, line, message, hint) {
|
|
18
|
+
return { rule, severity, message, file: brief.file, line, brief: brief.id ?? undefined, hint };
|
|
19
|
+
}
|
|
20
|
+
function lineOf(brief, key) {
|
|
21
|
+
return lineOfField(brief, key) + 1;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Scope patterns, for matching the files a round changed. A changed path is a
|
|
25
|
+
* file, and a literal pattern matches its own path whether it is read as a
|
|
26
|
+
* file or as a directory, so the tree is not asked.
|
|
27
|
+
*/
|
|
28
|
+
function globs(patterns) {
|
|
29
|
+
return patterns.flatMap((p) => {
|
|
30
|
+
const parsed = parseGlob(p);
|
|
31
|
+
return parsed.ok ? [parsed.glob] : [];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether an open box carries a note that closes it: "Delegated to ...",
|
|
36
|
+
* "Rejected ...". The note belongs to the box above it, so the search stops at
|
|
37
|
+
* the first nested box: a child's note does not close its parent.
|
|
38
|
+
*/
|
|
39
|
+
function dispositioned(scanned, line, end, markers) {
|
|
40
|
+
const boxes = new Set(scanned.tasks.map((t) => t.line));
|
|
41
|
+
for (let i = line; i < end; i += 1) {
|
|
42
|
+
if (i > line && boxes.has(i))
|
|
43
|
+
return false;
|
|
44
|
+
const text = scanned.masked[i];
|
|
45
|
+
if (markers.some((marker) => text.includes(marker)))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/** A path inside a directory that may be the root itself. */
|
|
51
|
+
function within(directory, name) {
|
|
52
|
+
return directory === '' ? name : `${directory}/${name}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a path is a brief: a file directly in the live or archive directory
|
|
56
|
+
* whose name the configuration takes for a brief. Edits to briefs are the
|
|
57
|
+
* ceremony, not the round, and are left out of the scope and tree checks.
|
|
58
|
+
* Nothing else in those directories is: a README beside the briefs is work.
|
|
59
|
+
*/
|
|
60
|
+
function isBriefPath(path, config) {
|
|
61
|
+
const directory = dirOf(path);
|
|
62
|
+
if (directory !== normalisePath(config.briefs) && directory !== normalisePath(config.archive))
|
|
63
|
+
return false;
|
|
64
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
65
|
+
const accept = parseGlob(config.files);
|
|
66
|
+
const excluded = config.exclude.some((e) => {
|
|
67
|
+
const parsed = parseGlob(e);
|
|
68
|
+
return parsed.ok && matchGlob(parsed.glob, name);
|
|
69
|
+
});
|
|
70
|
+
return accept.ok && matchGlob(accept.glob, name) && !excluded;
|
|
71
|
+
}
|
|
72
|
+
/** Task items that must be closed before a brief can be archived. */
|
|
73
|
+
export function openTasks(brief, corpus) {
|
|
74
|
+
const { tasks: scope, dispositions } = corpus.config.archiving;
|
|
75
|
+
const inScope = scope === 'all'
|
|
76
|
+
? brief.tasks
|
|
77
|
+
: brief.tasks.filter((task) => brief.sections.some((s) => task.line > s.heading.line && task.line < s.end && scope.some((name) => labelMatches(s.heading.text, name))));
|
|
78
|
+
return inScope.filter((task) => !task.checked && !dispositioned(brief.scan, task.line, task.end, dispositions));
|
|
79
|
+
}
|
|
80
|
+
function diffstat(changes) {
|
|
81
|
+
const insertions = changes.reduce((sum, c) => sum + (c.insertions ?? 0), 0);
|
|
82
|
+
const deletions = changes.reduce((sum, c) => sum + (c.deletions ?? 0), 0);
|
|
83
|
+
const files = changes.length === 1 ? '1 file changed' : `${changes.length} files changed`;
|
|
84
|
+
return `${files}, +${insertions} \u2212${deletions}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The banner's lines, markers included. A template line whose placeholders
|
|
88
|
+
* are not all filled is left out, so a round with no pull request has no
|
|
89
|
+
* pull-request sentence rather than a sentence with a hole in it. A template
|
|
90
|
+
* with nothing left writes no banner at all, markers included.
|
|
91
|
+
*/
|
|
92
|
+
export function renderBanner(template, values) {
|
|
93
|
+
const kept = template
|
|
94
|
+
.filter((line) => templateHoles(line).every((hole) => (values[hole] ?? '') !== ''))
|
|
95
|
+
.map((line) => fillTemplate(line, values));
|
|
96
|
+
if (kept.length === 0)
|
|
97
|
+
return [];
|
|
98
|
+
return [BANNER_OPEN, ...kept.map((line) => (line === '' ? '>' : `> ${line}`)), BANNER_CLOSE];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Removes a banner block and the blank line written after it. The exact
|
|
102
|
+
* reverse of {@link withBanner}, so reopening a brief leaves its blank lines
|
|
103
|
+
* as they were.
|
|
104
|
+
*/
|
|
105
|
+
function withoutBanner(lines, banner) {
|
|
106
|
+
if (banner === null)
|
|
107
|
+
return [...lines];
|
|
108
|
+
const after = lines[banner.end]?.trim() === '' ? banner.end + 1 : banner.end;
|
|
109
|
+
return [...lines.slice(0, banner.start), ...lines.slice(after)];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Inserts a banner under the front matter, after the blank line that usually
|
|
113
|
+
* follows it, and writes one blank line after the banner.
|
|
114
|
+
*/
|
|
115
|
+
function withBanner(lines, banner) {
|
|
116
|
+
const frontMatter = readFrontMatter(lines);
|
|
117
|
+
let at = frontMatter === null || frontMatter.close < 0 ? 0 : frontMatter.close + 1;
|
|
118
|
+
if (at > 0 && lines[at]?.trim() === '')
|
|
119
|
+
at += 1;
|
|
120
|
+
return [...lines.slice(0, at), ...banner, '', ...lines.slice(at)];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Sets the integrity field to the hash of everything else. The field is put in
|
|
124
|
+
* place before hashing, so the lines hashed are exactly the lines a later
|
|
125
|
+
* check sees once it takes the field back out.
|
|
126
|
+
*/
|
|
127
|
+
function withIntegrity(lines) {
|
|
128
|
+
const placed = setEntry(lines, readFrontMatter(lines), INTEGRITY_FIELD, 'pending');
|
|
129
|
+
const hash = integrityOf(joinLines(placed, '\n'));
|
|
130
|
+
return setEntry(placed, readFrontMatter(placed), INTEGRITY_FIELD, hash);
|
|
131
|
+
}
|
|
132
|
+
/** Lines written back with the source's byte-order mark, line ending and final newline, or lack of one. */
|
|
133
|
+
function encodeLike(source, lines) {
|
|
134
|
+
const bom = source.charCodeAt(0) === 0xfeff ? '\ufeff' : '';
|
|
135
|
+
const eol = lineEnding(source);
|
|
136
|
+
const text = joinLines(lines, eol);
|
|
137
|
+
const finalNewline = source === '' || source.endsWith('\n');
|
|
138
|
+
return `${bom}${finalNewline ? text : text.slice(0, text.length - eol.length)}`;
|
|
139
|
+
}
|
|
140
|
+
/** Links other briefs hold to a file that is moving: rewritten in live briefs, reported in frozen ones. */
|
|
141
|
+
function inbound(corpus, moving, to) {
|
|
142
|
+
const result = { ops: [], rewritten: [], frozen: [] };
|
|
143
|
+
for (const other of corpus.briefs) {
|
|
144
|
+
if (other === moving)
|
|
145
|
+
continue;
|
|
146
|
+
const directory = dirOf(other.file);
|
|
147
|
+
const lines = linesLinkingTo(other.scan, directory, moving.file);
|
|
148
|
+
if (lines.length === 0)
|
|
149
|
+
continue;
|
|
150
|
+
if (other.phase === 'archived') {
|
|
151
|
+
for (const line of lines)
|
|
152
|
+
result.frozen.push({ file: other.file, line: line + 1 });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const rewritten = rewriteLinks(other.scan, directory, directory, (p) => (p === moving.file ? to : p));
|
|
156
|
+
result.ops.push({ kind: 'write', path: other.file, content: encodeLike(other.source, rewritten.lines), before: other.source });
|
|
157
|
+
result.rewritten.push(other.file);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function donePlan(action, brief) {
|
|
162
|
+
return {
|
|
163
|
+
action,
|
|
164
|
+
brief,
|
|
165
|
+
from: brief.file,
|
|
166
|
+
to: brief.file,
|
|
167
|
+
done: true,
|
|
168
|
+
ops: [],
|
|
169
|
+
blocking: [],
|
|
170
|
+
warnings: [],
|
|
171
|
+
linksRewritten: 0,
|
|
172
|
+
inboundRewritten: [],
|
|
173
|
+
inboundFrozen: [],
|
|
174
|
+
banner: [],
|
|
175
|
+
changes: [],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function planArchive(corpus, brief, request) {
|
|
179
|
+
if (brief.phase === 'archived')
|
|
180
|
+
return donePlan('archive', brief);
|
|
181
|
+
const { config } = corpus;
|
|
182
|
+
const archiveDir = normalisePath(config.archive);
|
|
183
|
+
const to = within(archiveDir, brief.name);
|
|
184
|
+
const blocking = [];
|
|
185
|
+
const warnings = [];
|
|
186
|
+
// Under --strict a warning is an error, here as in lint.
|
|
187
|
+
for (const finding of request.findings ?? []) {
|
|
188
|
+
const refuses = finding.severity === 'error' || (request.strict === true && finding.severity === 'warning');
|
|
189
|
+
if (finding.file === brief.file && refuses)
|
|
190
|
+
blocking.push(finding);
|
|
191
|
+
}
|
|
192
|
+
// Only a status field can say "draft", so there is one to point at.
|
|
193
|
+
const field = config.status.field;
|
|
194
|
+
if (brief.status === 'draft') {
|
|
195
|
+
blocking.push(problem(brief, 'archive-draft', 'error', lineOf(brief, field), 'is a draft, and a draft has not been executed', `set "${field}: ${config.status.active}" once the round runs`));
|
|
196
|
+
}
|
|
197
|
+
for (const task of openTasks(brief, corpus)) {
|
|
198
|
+
blocking.push(problem(brief, 'open-task', 'error', task.line + 1, `"${task.text === '' ? '(empty)' : task.text}" is neither ticked nor dispositioned`, `tick it, or say why under it with a note starting ${config.archiving.dispositions.map((d) => `"${d}"`).join(', ')}`));
|
|
199
|
+
}
|
|
200
|
+
for (const dependency of brief.dependsOn) {
|
|
201
|
+
const target = resolveDependency(corpus, dependency);
|
|
202
|
+
if (target !== undefined && target !== brief && target.phase === 'live') {
|
|
203
|
+
blocking.push(problem(brief, 'dependency-open', 'error', lineOf(brief, 'dependsOn'), `depends on ${target.id ?? target.name}, which is not archived yet`, `archive ${target.id ?? target.name} first, or drop the dependency if it was never real`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const bookkeeping = (path) => isBriefPath(path, config);
|
|
207
|
+
if (request.dirty !== undefined && request.allowDirty !== true) {
|
|
208
|
+
const outside = request.dirty.filter((path) => !bookkeeping(path));
|
|
209
|
+
if (outside.length > 0) {
|
|
210
|
+
const shown = outside.slice(0, 5).join(', ') + (outside.length > 5 ? `, and ${outside.length - 5} more` : '');
|
|
211
|
+
blocking.push(problem(brief, 'dirty-tree', 'error', 1, `the working tree has uncommitted changes outside the briefs: ${shown}`, 'commit them so the recorded commit holds the round, or pass --allow-dirty'));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const work = (request.changes ?? []).filter((change) => !bookkeeping(change.path));
|
|
215
|
+
const protectedGlobs = globs(brief.protectedFiles);
|
|
216
|
+
const affectedGlobs = globs(brief.affectedFiles);
|
|
217
|
+
for (const change of work) {
|
|
218
|
+
if (protectedGlobs.some((g) => matchGlob(g, change.path))) {
|
|
219
|
+
blocking.push(problem(brief, 'protected-file', 'error', lineOf(brief, 'protectedFiles'), `the round changed ${change.path}, which this brief protects`, 'revert the change, or record the departure in the brief before archiving it'));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// A protected file is reported once, as protected, and not again as out of scope.
|
|
223
|
+
const unprotected = work.filter((c) => !protectedGlobs.some((g) => matchGlob(g, c.path)));
|
|
224
|
+
const outside = affectedGlobs.length === 0 ? [] : unprotected.filter((c) => !affectedGlobs.some((g) => matchGlob(g, c.path)));
|
|
225
|
+
if (outside.length > 0) {
|
|
226
|
+
const shown = outside.slice(0, 5).map((c) => c.path).join(', ') + (outside.length > 5 ? `, and ${outside.length - 5} more` : '');
|
|
227
|
+
const finding = problem(brief, 'out-of-scope', request.strict === true ? 'error' : 'warning', lineOf(brief, 'affectedFiles'), `the round changed ${outside.length} file(s) outside affectedFiles: ${shown}`, 'widen affectedFiles if the scope was wrong, or say why in the summary');
|
|
228
|
+
(request.strict === true ? blocking : warnings).push(finding);
|
|
229
|
+
}
|
|
230
|
+
if (corpus.briefs.some((b) => b.file === to)) {
|
|
231
|
+
blocking.push(problem(brief, 'archive-exists', 'error', 1, `${to} already exists`, 'an archived brief is never overwritten'));
|
|
232
|
+
}
|
|
233
|
+
let lines = [...brief.lines];
|
|
234
|
+
let linksRewritten = 0;
|
|
235
|
+
if (config.archiving.rewriteLinks) {
|
|
236
|
+
const rewritten = rewriteLinks(brief.scan, dirOf(brief.file), archiveDir, (p) => (p === brief.file ? to : p));
|
|
237
|
+
lines = rewritten.lines;
|
|
238
|
+
linksRewritten = rewritten.count;
|
|
239
|
+
}
|
|
240
|
+
lines = withoutBanner(lines, brief.banner);
|
|
241
|
+
const pr = request.pr;
|
|
242
|
+
const values = {
|
|
243
|
+
date: request.date,
|
|
244
|
+
summary: request.summary?.trim() ?? '',
|
|
245
|
+
pr: pr === undefined ? '' : pr.url === null ? `#${pr.number}` : `[#${pr.number}](${pr.url})`,
|
|
246
|
+
commit: request.commit?.sha.slice(0, 7) ?? '',
|
|
247
|
+
diffstat: request.commit === undefined || request.changes === undefined ? '' : diffstat(request.changes),
|
|
248
|
+
links: linksRewritten === 0
|
|
249
|
+
? ''
|
|
250
|
+
: `Relative links were rewritten to resolve from \`${archiveDir}/\` (${linksRewritten}); no other word changed.`,
|
|
251
|
+
id: brief.id ?? '',
|
|
252
|
+
title: brief.title ?? '',
|
|
253
|
+
author: request.commit?.author ?? '',
|
|
254
|
+
};
|
|
255
|
+
const banner = renderBanner(config.archiving.banner, values);
|
|
256
|
+
if (banner.length > 0)
|
|
257
|
+
lines = withBanner(lines, banner);
|
|
258
|
+
if (config.status.field !== null) {
|
|
259
|
+
lines = setEntry(lines, readFrontMatter(lines), config.status.field, renderScalar(config.status.archived));
|
|
260
|
+
}
|
|
261
|
+
lines = removeEntry(lines, readFrontMatter(lines), INTEGRITY_FIELD);
|
|
262
|
+
if (config.archiving.freeze)
|
|
263
|
+
lines = withIntegrity(lines);
|
|
264
|
+
const incoming = inbound(corpus, brief, to);
|
|
265
|
+
return {
|
|
266
|
+
action: 'archive',
|
|
267
|
+
brief,
|
|
268
|
+
from: brief.file,
|
|
269
|
+
to,
|
|
270
|
+
done: false,
|
|
271
|
+
ops: [
|
|
272
|
+
{ kind: 'write', path: to, content: encodeLike(brief.source, lines), before: null },
|
|
273
|
+
...incoming.ops,
|
|
274
|
+
{ kind: 'remove', path: brief.file, before: brief.source },
|
|
275
|
+
],
|
|
276
|
+
blocking,
|
|
277
|
+
warnings,
|
|
278
|
+
linksRewritten,
|
|
279
|
+
inboundRewritten: incoming.rewritten,
|
|
280
|
+
inboundFrozen: incoming.frozen,
|
|
281
|
+
banner,
|
|
282
|
+
changes: request.changes ?? [],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Reopens an archived brief: the banner and the freeze come off, the status
|
|
287
|
+
* goes back to the live word, and the links are rewritten for the directory
|
|
288
|
+
* it returns to. What the round wrote stays in history, where it belongs.
|
|
289
|
+
*/
|
|
290
|
+
export function planUnarchive(corpus, brief) {
|
|
291
|
+
if (brief.phase === 'live')
|
|
292
|
+
return donePlan('unarchive', brief);
|
|
293
|
+
const { config } = corpus;
|
|
294
|
+
const briefsDir = normalisePath(config.briefs);
|
|
295
|
+
const to = within(briefsDir, brief.name);
|
|
296
|
+
const blocking = [];
|
|
297
|
+
if (corpus.briefs.some((b) => b.file === to)) {
|
|
298
|
+
blocking.push(problem(brief, 'unarchive-exists', 'error', 1, `${to} already exists`, 'rename one of them first'));
|
|
299
|
+
}
|
|
300
|
+
let lines = [...brief.lines];
|
|
301
|
+
let linksRewritten = 0;
|
|
302
|
+
if (config.archiving.rewriteLinks) {
|
|
303
|
+
const rewritten = rewriteLinks(brief.scan, dirOf(brief.file), briefsDir, (p) => (p === brief.file ? to : p));
|
|
304
|
+
lines = rewritten.lines;
|
|
305
|
+
linksRewritten = rewritten.count;
|
|
306
|
+
}
|
|
307
|
+
lines = withoutBanner(lines, brief.banner);
|
|
308
|
+
lines = removeEntry(lines, readFrontMatter(lines), INTEGRITY_FIELD);
|
|
309
|
+
if (config.status.field !== null) {
|
|
310
|
+
lines = setEntry(lines, readFrontMatter(lines), config.status.field, renderScalar(config.status.active));
|
|
311
|
+
}
|
|
312
|
+
const incoming = inbound(corpus, brief, to);
|
|
313
|
+
return {
|
|
314
|
+
action: 'unarchive',
|
|
315
|
+
brief,
|
|
316
|
+
from: brief.file,
|
|
317
|
+
to,
|
|
318
|
+
done: false,
|
|
319
|
+
ops: [
|
|
320
|
+
{ kind: 'write', path: to, content: encodeLike(brief.source, lines), before: null },
|
|
321
|
+
...incoming.ops,
|
|
322
|
+
{ kind: 'remove', path: brief.file, before: brief.source },
|
|
323
|
+
],
|
|
324
|
+
blocking,
|
|
325
|
+
warnings: [],
|
|
326
|
+
linksRewritten,
|
|
327
|
+
inboundRewritten: incoming.rewritten,
|
|
328
|
+
inboundFrozen: incoming.frozen,
|
|
329
|
+
banner: [],
|
|
330
|
+
changes: [],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Archival and its reverse, planned.\n *\n * A plan is a pure function of the corpus and a request: what would be\n * refused and why, the text every touched file would hold afterwards, and the\n * operations that get there. Nothing is written here. `--dry-run` prints a\n * plan; `apply.ts` executes one as a transaction. Splitting the two is what\n * makes the ceremony testable without a disk and previewable without risk.\n */\n\nimport { BANNER_CLOSE, BANNER_OPEN, type Brief, lineOfField } from './brief.js';\nimport type { Config } from './config.js';\nimport { type Corpus, resolveDependency } from './corpus.js';\nimport { readFrontMatter, removeEntry, renderScalar, setEntry } from './frontmatter.js';\nimport type { CommitInfo, FileChange } from './git.js';\nimport { type Glob, matchGlob, parseGlob } from './glob.js';\nimport { INTEGRITY_FIELD, integrityOf } from './integrity.js';\nimport { dirOf, linesLinkingTo, normalisePath, rewriteLinks } from './links.js';\nimport type { Scan } from './markdown.js';\nimport { fillTemplate, joinLines, labelMatches, lineEnding, templateHoles } from './text.js';\nimport type { Finding, Severity } from './types.js';\n\nexport interface PullRequest {\n readonly number: number;\n readonly url: string | null;\n}\n\nexport interface ArchiveRequest {\n /** `YYYY-MM-DD`. */\n readonly date: string;\n readonly summary?: string | undefined;\n readonly pr?: PullRequest | undefined;\n readonly commit?: CommitInfo | undefined;\n /** The files the round changed, when a commit is known. */\n readonly changes?: readonly FileChange[] | undefined;\n /** Uncommitted paths, when the working tree was read. */\n readonly dirty?: readonly string[] | undefined;\n readonly allowDirty?: boolean | undefined;\n /** Out-of-scope changes refuse the archival instead of warning. */\n readonly strict?: boolean | undefined;\n /** The corpus's lint findings; errors on the brief refuse the archival. */\n readonly findings?: readonly Finding[] | undefined;\n}\n\nexport type FileOp =\n | {\n readonly kind: 'write';\n readonly path: string;\n readonly content: string;\n /** What the file holds when the plan is made; `null` for a file that must not exist yet. */\n readonly before: string | null;\n }\n | { readonly kind: 'remove'; readonly path: string; readonly before: string };\n\nexport interface Plan {\n readonly action: 'archive' | 'unarchive';\n readonly brief: Brief;\n readonly from: string;\n readonly to: string;\n /** The brief is already where this action would put it. */\n readonly done: boolean;\n readonly ops: readonly FileOp[];\n /** Why the action is refused; empty when it may proceed. */\n readonly blocking: readonly Finding[];\n readonly warnings: readonly Finding[];\n readonly linksRewritten: number;\n readonly inboundRewritten: readonly string[];\n /** Links archived briefs hold to the old path. They are frozen and left as they are. */\n readonly inboundFrozen: readonly { readonly file: string; readonly line: number }[];\n readonly banner: readonly string[];\n readonly changes: readonly FileChange[];\n}\n\nfunction problem(\n brief: Brief,\n rule: string,\n severity: Severity,\n line: number,\n message: string,\n hint?: string,\n): Finding {\n return { rule, severity, message, file: brief.file, line, brief: brief.id ?? undefined, hint };\n}\n\nfunction lineOf(brief: Brief, key: string): number {\n return lineOfField(brief, key) + 1;\n}\n\n/**\n * Scope patterns, for matching the files a round changed. A changed path is a\n * file, and a literal pattern matches its own path whether it is read as a\n * file or as a directory, so the tree is not asked.\n */\nfunction globs(patterns: readonly string[]): Glob[] {\n return patterns.flatMap((p) => {\n const parsed = parseGlob(p);\n return parsed.ok ? [parsed.glob] : [];\n });\n}\n\n/**\n * Whether an open box carries a note that closes it: \"Delegated to ...\",\n * \"Rejected ...\". The note belongs to the box above it, so the search stops at\n * the first nested box: a child's note does not close its parent.\n */\nfunction dispositioned(scanned: Scan, line: number, end: number, markers: readonly string[]): boolean {\n const boxes = new Set(scanned.tasks.map((t) => t.line));\n for (let i = line; i < end; i += 1) {\n if (i > line && boxes.has(i)) return false;\n const text = scanned.masked[i] as string;\n if (markers.some((marker) => text.includes(marker))) return true;\n }\n return false;\n}\n\n/** A path inside a directory that may be the root itself. */\nfunction within(directory: string, name: string): string {\n return directory === '' ? name : `${directory}/${name}`;\n}\n\n/**\n * Whether a path is a brief: a file directly in the live or archive directory\n * whose name the configuration takes for a brief. Edits to briefs are the\n * ceremony, not the round, and are left out of the scope and tree checks.\n * Nothing else in those directories is: a README beside the briefs is work.\n */\nfunction isBriefPath(path: string, config: Config): boolean {\n const directory = dirOf(path);\n if (directory !== normalisePath(config.briefs) && directory !== normalisePath(config.archive)) return false;\n const name = path.slice(path.lastIndexOf('/') + 1);\n const accept = parseGlob(config.files);\n const excluded = config.exclude.some((e) => {\n const parsed = parseGlob(e);\n return parsed.ok && matchGlob(parsed.glob, name);\n });\n return accept.ok && matchGlob(accept.glob, name) && !excluded;\n}\n\n/** Task items that must be closed before a brief can be archived. */\nexport function openTasks(brief: Brief, corpus: Corpus): Brief['tasks'] {\n const { tasks: scope, dispositions } = corpus.config.archiving;\n const inScope =\n scope === 'all'\n ? brief.tasks\n : brief.tasks.filter((task) =>\n brief.sections.some(\n (s) => task.line > s.heading.line && task.line < s.end && scope.some((name) => labelMatches(s.heading.text, name)),\n ),\n );\n return inScope.filter((task) => !task.checked && !dispositioned(brief.scan, task.line, task.end, dispositions));\n}\n\nfunction diffstat(changes: readonly FileChange[]): string {\n const insertions = changes.reduce((sum, c) => sum + (c.insertions ?? 0), 0);\n const deletions = changes.reduce((sum, c) => sum + (c.deletions ?? 0), 0);\n const files = changes.length === 1 ? '1 file changed' : `${changes.length} files changed`;\n return `${files}, +${insertions} \\u2212${deletions}`;\n}\n\n/**\n * The banner's lines, markers included. A template line whose placeholders\n * are not all filled is left out, so a round with no pull request has no\n * pull-request sentence rather than a sentence with a hole in it. A template\n * with nothing left writes no banner at all, markers included.\n */\nexport function renderBanner(template: readonly string[], values: Readonly<Record<string, string>>): string[] {\n const kept = template\n .filter((line) => templateHoles(line).every((hole) => (values[hole] ?? '') !== ''))\n .map((line) => fillTemplate(line, values));\n if (kept.length === 0) return [];\n return [BANNER_OPEN, ...kept.map((line) => (line === '' ? '>' : `> ${line}`)), BANNER_CLOSE];\n}\n\n/**\n * Removes a banner block and the blank line written after it. The exact\n * reverse of {@link withBanner}, so reopening a brief leaves its blank lines\n * as they were.\n */\nfunction withoutBanner(lines: readonly string[], banner: Brief['banner']): string[] {\n if (banner === null) return [...lines];\n const after = lines[banner.end]?.trim() === '' ? banner.end + 1 : banner.end;\n return [...lines.slice(0, banner.start), ...lines.slice(after)];\n}\n\n/**\n * Inserts a banner under the front matter, after the blank line that usually\n * follows it, and writes one blank line after the banner.\n */\nfunction withBanner(lines: readonly string[], banner: readonly string[]): string[] {\n const frontMatter = readFrontMatter(lines);\n let at = frontMatter === null || frontMatter.close < 0 ? 0 : frontMatter.close + 1;\n if (at > 0 && lines[at]?.trim() === '') at += 1;\n return [...lines.slice(0, at), ...banner, '', ...lines.slice(at)];\n}\n\n/**\n * Sets the integrity field to the hash of everything else. The field is put in\n * place before hashing, so the lines hashed are exactly the lines a later\n * check sees once it takes the field back out.\n */\nfunction withIntegrity(lines: readonly string[]): string[] {\n const placed = setEntry(lines, readFrontMatter(lines), INTEGRITY_FIELD, 'pending');\n const hash = integrityOf(joinLines(placed, '\\n'));\n return setEntry(placed, readFrontMatter(placed), INTEGRITY_FIELD, hash);\n}\n\n/** Lines written back with the source's byte-order mark, line ending and final newline, or lack of one. */\nfunction encodeLike(source: string, lines: readonly string[]): string {\n const bom = source.charCodeAt(0) === 0xfeff ? '\\ufeff' : '';\n const eol = lineEnding(source);\n const text = joinLines(lines, eol);\n const finalNewline = source === '' || source.endsWith('\\n');\n return `${bom}${finalNewline ? text : text.slice(0, text.length - eol.length)}`;\n}\n\ninterface Inbound {\n readonly ops: FileOp[];\n readonly rewritten: string[];\n readonly frozen: { file: string; line: number }[];\n}\n\n/** Links other briefs hold to a file that is moving: rewritten in live briefs, reported in frozen ones. */\nfunction inbound(corpus: Corpus, moving: Brief, to: string): Inbound {\n const result: Inbound = { ops: [], rewritten: [], frozen: [] };\n for (const other of corpus.briefs) {\n if (other === moving) continue;\n const directory = dirOf(other.file);\n const lines = linesLinkingTo(other.scan, directory, moving.file);\n if (lines.length === 0) continue;\n if (other.phase === 'archived') {\n for (const line of lines) result.frozen.push({ file: other.file, line: line + 1 });\n continue;\n }\n const rewritten = rewriteLinks(other.scan, directory, directory, (p) => (p === moving.file ? to : p));\n result.ops.push({ kind: 'write', path: other.file, content: encodeLike(other.source, rewritten.lines), before: other.source });\n result.rewritten.push(other.file);\n }\n return result;\n}\n\nfunction donePlan(action: Plan['action'], brief: Brief): Plan {\n return {\n action,\n brief,\n from: brief.file,\n to: brief.file,\n done: true,\n ops: [],\n blocking: [],\n warnings: [],\n linksRewritten: 0,\n inboundRewritten: [],\n inboundFrozen: [],\n banner: [],\n changes: [],\n };\n}\n\nexport function planArchive(corpus: Corpus, brief: Brief, request: ArchiveRequest): Plan {\n if (brief.phase === 'archived') return donePlan('archive', brief);\n const { config } = corpus;\n const archiveDir = normalisePath(config.archive);\n const to = within(archiveDir, brief.name);\n const blocking: Finding[] = [];\n const warnings: Finding[] = [];\n\n // Under --strict a warning is an error, here as in lint.\n for (const finding of request.findings ?? []) {\n const refuses = finding.severity === 'error' || (request.strict === true && finding.severity === 'warning');\n if (finding.file === brief.file && refuses) blocking.push(finding);\n }\n // Only a status field can say \"draft\", so there is one to point at.\n const field = config.status.field as string;\n if (brief.status === 'draft') {\n blocking.push(\n problem(brief, 'archive-draft', 'error', lineOf(brief, field), 'is a draft, and a draft has not been executed', `set \"${field}: ${config.status.active}\" once the round runs`),\n );\n }\n for (const task of openTasks(brief, corpus)) {\n blocking.push(\n problem(\n brief,\n 'open-task',\n 'error',\n task.line + 1,\n `\"${task.text === '' ? '(empty)' : task.text}\" is neither ticked nor dispositioned`,\n `tick it, or say why under it with a note starting ${config.archiving.dispositions.map((d) => `\"${d}\"`).join(', ')}`,\n ),\n );\n }\n for (const dependency of brief.dependsOn) {\n const target = resolveDependency(corpus, dependency);\n if (target !== undefined && target !== brief && target.phase === 'live') {\n blocking.push(\n problem(brief, 'dependency-open', 'error', lineOf(brief, 'dependsOn'), `depends on ${target.id ?? target.name}, which is not archived yet`, `archive ${target.id ?? target.name} first, or drop the dependency if it was never real`),\n );\n }\n }\n const bookkeeping = (path: string): boolean => isBriefPath(path, config);\n if (request.dirty !== undefined && request.allowDirty !== true) {\n const outside = request.dirty.filter((path) => !bookkeeping(path));\n if (outside.length > 0) {\n const shown = outside.slice(0, 5).join(', ') + (outside.length > 5 ? `, and ${outside.length - 5} more` : '');\n blocking.push(\n problem(brief, 'dirty-tree', 'error', 1, `the working tree has uncommitted changes outside the briefs: ${shown}`, 'commit them so the recorded commit holds the round, or pass --allow-dirty'),\n );\n }\n }\n\n const work = (request.changes ?? []).filter((change) => !bookkeeping(change.path));\n const protectedGlobs = globs(brief.protectedFiles);\n const affectedGlobs = globs(brief.affectedFiles);\n for (const change of work) {\n if (protectedGlobs.some((g) => matchGlob(g, change.path))) {\n blocking.push(\n problem(brief, 'protected-file', 'error', lineOf(brief, 'protectedFiles'), `the round changed ${change.path}, which this brief protects`, 'revert the change, or record the departure in the brief before archiving it'),\n );\n }\n }\n // A protected file is reported once, as protected, and not again as out of scope.\n const unprotected = work.filter((c) => !protectedGlobs.some((g) => matchGlob(g, c.path)));\n const outside = affectedGlobs.length === 0 ? [] : unprotected.filter((c) => !affectedGlobs.some((g) => matchGlob(g, c.path)));\n if (outside.length > 0) {\n const shown = outside.slice(0, 5).map((c) => c.path).join(', ') + (outside.length > 5 ? `, and ${outside.length - 5} more` : '');\n const finding = problem(\n brief,\n 'out-of-scope',\n request.strict === true ? 'error' : 'warning',\n lineOf(brief, 'affectedFiles'),\n `the round changed ${outside.length} file(s) outside affectedFiles: ${shown}`,\n 'widen affectedFiles if the scope was wrong, or say why in the summary',\n );\n (request.strict === true ? blocking : warnings).push(finding);\n }\n if (corpus.briefs.some((b) => b.file === to)) {\n blocking.push(problem(brief, 'archive-exists', 'error', 1, `${to} already exists`, 'an archived brief is never overwritten'));\n }\n\n let lines: string[] = [...brief.lines];\n let linksRewritten = 0;\n if (config.archiving.rewriteLinks) {\n const rewritten = rewriteLinks(brief.scan, dirOf(brief.file), archiveDir, (p) => (p === brief.file ? to : p));\n lines = rewritten.lines;\n linksRewritten = rewritten.count;\n }\n lines = withoutBanner(lines, brief.banner);\n\n const pr = request.pr;\n const values: Record<string, string> = {\n date: request.date,\n summary: request.summary?.trim() ?? '',\n pr: pr === undefined ? '' : pr.url === null ? `#${pr.number}` : `[#${pr.number}](${pr.url})`,\n commit: request.commit?.sha.slice(0, 7) ?? '',\n diffstat: request.commit === undefined || request.changes === undefined ? '' : diffstat(request.changes),\n links:\n linksRewritten === 0\n ? ''\n : `Relative links were rewritten to resolve from \\`${archiveDir}/\\` (${linksRewritten}); no other word changed.`,\n id: brief.id ?? '',\n title: brief.title ?? '',\n author: request.commit?.author ?? '',\n };\n const banner = renderBanner(config.archiving.banner, values);\n if (banner.length > 0) lines = withBanner(lines, banner);\n if (config.status.field !== null) {\n lines = setEntry(lines, readFrontMatter(lines), config.status.field, renderScalar(config.status.archived));\n }\n lines = removeEntry(lines, readFrontMatter(lines), INTEGRITY_FIELD);\n if (config.archiving.freeze) lines = withIntegrity(lines);\n\n const incoming = inbound(corpus, brief, to);\n return {\n action: 'archive',\n brief,\n from: brief.file,\n to,\n done: false,\n ops: [\n { kind: 'write', path: to, content: encodeLike(brief.source, lines), before: null },\n ...incoming.ops,\n { kind: 'remove', path: brief.file, before: brief.source },\n ],\n blocking,\n warnings,\n linksRewritten,\n inboundRewritten: incoming.rewritten,\n inboundFrozen: incoming.frozen,\n banner,\n changes: request.changes ?? [],\n };\n}\n\n/**\n * Reopens an archived brief: the banner and the freeze come off, the status\n * goes back to the live word, and the links are rewritten for the directory\n * it returns to. What the round wrote stays in history, where it belongs.\n */\nexport function planUnarchive(corpus: Corpus, brief: Brief): Plan {\n if (brief.phase === 'live') return donePlan('unarchive', brief);\n const { config } = corpus;\n const briefsDir = normalisePath(config.briefs);\n const to = within(briefsDir, brief.name);\n const blocking: Finding[] = [];\n if (corpus.briefs.some((b) => b.file === to)) {\n blocking.push(problem(brief, 'unarchive-exists', 'error', 1, `${to} already exists`, 'rename one of them first'));\n }\n\n let lines: string[] = [...brief.lines];\n let linksRewritten = 0;\n if (config.archiving.rewriteLinks) {\n const rewritten = rewriteLinks(brief.scan, dirOf(brief.file), briefsDir, (p) => (p === brief.file ? to : p));\n lines = rewritten.lines;\n linksRewritten = rewritten.count;\n }\n lines = withoutBanner(lines, brief.banner);\n lines = removeEntry(lines, readFrontMatter(lines), INTEGRITY_FIELD);\n if (config.status.field !== null) {\n lines = setEntry(lines, readFrontMatter(lines), config.status.field, renderScalar(config.status.active));\n }\n\n const incoming = inbound(corpus, brief, to);\n return {\n action: 'unarchive',\n brief,\n from: brief.file,\n to,\n done: false,\n ops: [\n { kind: 'write', path: to, content: encodeLike(brief.source, lines), before: null },\n ...incoming.ops,\n { kind: 'remove', path: brief.file, before: brief.source },\n ],\n blocking,\n warnings: [],\n linksRewritten,\n inboundRewritten: incoming.rewritten,\n inboundFrozen: incoming.frozen,\n banner: [],\n changes: [],\n };\n}\n"]}
|
package/dist/brief.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One brief, read: its identity, its front matter, its sections and its task
|
|
3
|
+
* items. A pure function of a file's path and text.
|
|
4
|
+
*/
|
|
5
|
+
import type { Config } from './config.js';
|
|
6
|
+
import { type FrontMatter } from './frontmatter.js';
|
|
7
|
+
import { type Scan, type Section, type TaskItem } from './markdown.js';
|
|
8
|
+
import type { Phase, Status } from './types.js';
|
|
9
|
+
export declare const BANNER_OPEN = "<!-- spec-brief:banner -->";
|
|
10
|
+
export declare const BANNER_CLOSE = "<!-- /spec-brief:banner -->";
|
|
11
|
+
/** Front-matter keys spec-brief itself reads, in the spelling it documents. */
|
|
12
|
+
export declare const BUILT_IN_FIELDS: readonly string[];
|
|
13
|
+
export interface FieldProblem {
|
|
14
|
+
readonly field: string;
|
|
15
|
+
/** 0-based line. */
|
|
16
|
+
readonly line: number;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Brief {
|
|
20
|
+
/** `null` when no id can be read, which a rule reports. */
|
|
21
|
+
readonly id: string | null;
|
|
22
|
+
/** Repository-relative path. */
|
|
23
|
+
readonly file: string;
|
|
24
|
+
/** The file name. */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly phase: Phase;
|
|
27
|
+
/** The file exactly as read, byte-order mark and line endings included. */
|
|
28
|
+
readonly source: string;
|
|
29
|
+
/** The text parsed: the source without a byte-order mark. */
|
|
30
|
+
readonly text: string;
|
|
31
|
+
readonly lines: readonly string[];
|
|
32
|
+
readonly frontMatter: FrontMatter | null;
|
|
33
|
+
/** 0-based first line after the front matter. */
|
|
34
|
+
readonly bodyStart: number;
|
|
35
|
+
readonly scan: Scan;
|
|
36
|
+
readonly sections: readonly Section[];
|
|
37
|
+
readonly tasks: readonly TaskItem[];
|
|
38
|
+
readonly title: string | null;
|
|
39
|
+
readonly status: Status | null;
|
|
40
|
+
readonly statusWord: string | null;
|
|
41
|
+
readonly type: string | null;
|
|
42
|
+
readonly wave: number | null;
|
|
43
|
+
readonly dependsOn: readonly string[];
|
|
44
|
+
readonly affectedFiles: readonly string[];
|
|
45
|
+
readonly protectedFiles: readonly string[];
|
|
46
|
+
readonly integrity: string | null;
|
|
47
|
+
/** The frozen banner's lines, markers included, end exclusive. */
|
|
48
|
+
readonly banner: {
|
|
49
|
+
readonly start: number;
|
|
50
|
+
readonly end: number;
|
|
51
|
+
} | null;
|
|
52
|
+
readonly problems: readonly FieldProblem[];
|
|
53
|
+
}
|
|
54
|
+
/** The id a file name carries: everything before the separator, or the whole stem. */
|
|
55
|
+
export declare function idFromName(name: string, separator: string): string;
|
|
56
|
+
export declare function parseBrief(file: string, text: string, config: Config, phase: Phase): Brief;
|
|
57
|
+
/** The 0-based line a front-matter key is declared on, or the first line when it is absent. */
|
|
58
|
+
export declare function lineOfField(brief: Brief, field: string): number;
|
|
59
|
+
/** Keys spec-brief knows, compared the way front matter compares them. */
|
|
60
|
+
export declare function knownFields(config: Config): Set<string>;
|
package/dist/brief.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One brief, read: its identity, its front matter, its sections and its task
|
|
3
|
+
* items. A pure function of a file's path and text.
|
|
4
|
+
*/
|
|
5
|
+
import { findEntry, isNull, keyName, readFrontMatter } from './frontmatter.js';
|
|
6
|
+
import { scan, sectionsOf, titleOf } from './markdown.js';
|
|
7
|
+
import { splitLines, stripBom } from './text.js';
|
|
8
|
+
export const BANNER_OPEN = '<!-- spec-brief:banner -->';
|
|
9
|
+
export const BANNER_CLOSE = '<!-- /spec-brief:banner -->';
|
|
10
|
+
/** Front-matter keys spec-brief itself reads, in the spelling it documents. */
|
|
11
|
+
export const BUILT_IN_FIELDS = [
|
|
12
|
+
'id',
|
|
13
|
+
'title',
|
|
14
|
+
'type',
|
|
15
|
+
'status',
|
|
16
|
+
'date',
|
|
17
|
+
'wave',
|
|
18
|
+
'dependsOn',
|
|
19
|
+
'affectedFiles',
|
|
20
|
+
'protectedFiles',
|
|
21
|
+
'integrity',
|
|
22
|
+
];
|
|
23
|
+
function valueText(value) {
|
|
24
|
+
if (value.kind === 'unsupported')
|
|
25
|
+
return { problem: value.reason };
|
|
26
|
+
if (value.kind === 'list')
|
|
27
|
+
return { problem: 'must be a single value, not a list' };
|
|
28
|
+
return isNull(value.scalar) ? null : value.scalar.text;
|
|
29
|
+
}
|
|
30
|
+
function valueList(value) {
|
|
31
|
+
if (value.kind === 'unsupported')
|
|
32
|
+
return { problem: value.reason };
|
|
33
|
+
if (value.kind === 'scalar')
|
|
34
|
+
return isNull(value.scalar) ? [] : [value.scalar.text];
|
|
35
|
+
const empty = value.items.find((item) => isNull(item) || item.text.trim() === '');
|
|
36
|
+
if (empty !== undefined)
|
|
37
|
+
return { problem: 'must not contain an empty item' };
|
|
38
|
+
return value.items.map((item) => item.text.trim());
|
|
39
|
+
}
|
|
40
|
+
/** The id a file name carries: everything before the separator, or the whole stem. */
|
|
41
|
+
export function idFromName(name, separator) {
|
|
42
|
+
const stem = name.replace(/\.md$/i, '');
|
|
43
|
+
const at = stem.indexOf(separator);
|
|
44
|
+
return at > 0 ? stem.slice(0, at) : stem;
|
|
45
|
+
}
|
|
46
|
+
function statusFrom(word, config, phase) {
|
|
47
|
+
if (config.status.field === null)
|
|
48
|
+
return phase === 'archived' ? 'archived' : 'active';
|
|
49
|
+
if (word === null)
|
|
50
|
+
return null;
|
|
51
|
+
const lower = word.toLowerCase();
|
|
52
|
+
if (lower === config.status.archived.toLowerCase())
|
|
53
|
+
return 'archived';
|
|
54
|
+
if (lower === config.status.active.toLowerCase())
|
|
55
|
+
return 'active';
|
|
56
|
+
if (config.status.draft !== null && lower === config.status.draft.toLowerCase())
|
|
57
|
+
return 'draft';
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export function parseBrief(file, text, config, phase) {
|
|
61
|
+
const content = stripBom(text);
|
|
62
|
+
const lines = splitLines(content);
|
|
63
|
+
const frontMatter = readFrontMatter(lines);
|
|
64
|
+
const bodyStart = frontMatter === null || frontMatter.close < 0 ? 0 : frontMatter.close + 1;
|
|
65
|
+
const scanned = scan(lines, bodyStart);
|
|
66
|
+
const problems = [];
|
|
67
|
+
const name = file.slice(file.lastIndexOf('/') + 1);
|
|
68
|
+
const text1 = (field) => {
|
|
69
|
+
const entry = findEntry(frontMatter, field);
|
|
70
|
+
if (entry === undefined)
|
|
71
|
+
return null;
|
|
72
|
+
const value = valueText(entry.value);
|
|
73
|
+
if (value !== null && typeof value === 'object') {
|
|
74
|
+
problems.push({ field, line: entry.line, message: `"${entry.key}" ${value.problem}` });
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
};
|
|
79
|
+
const list = (field) => {
|
|
80
|
+
const entry = findEntry(frontMatter, field);
|
|
81
|
+
if (entry === undefined)
|
|
82
|
+
return [];
|
|
83
|
+
const value = valueList(entry.value);
|
|
84
|
+
if (!Array.isArray(value)) {
|
|
85
|
+
problems.push({ field, line: entry.line, message: `"${entry.key}" ${value.problem}` });
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
};
|
|
90
|
+
const waveText = text1('wave');
|
|
91
|
+
let wave = null;
|
|
92
|
+
const waveEntry = findEntry(frontMatter, 'wave');
|
|
93
|
+
if (waveText !== null && waveEntry !== undefined) {
|
|
94
|
+
const quoted = waveEntry.value.kind === 'scalar' && waveEntry.value.scalar.quoted;
|
|
95
|
+
if (!quoted && /^\d{1,9}$/.test(waveText))
|
|
96
|
+
wave = Number(waveText);
|
|
97
|
+
else
|
|
98
|
+
problems.push({ field: 'wave', line: waveEntry.line, message: `"wave" must be a whole number, not "${waveText}"` });
|
|
99
|
+
}
|
|
100
|
+
const statusWord = config.status.field === null ? null : text1(config.status.field);
|
|
101
|
+
const frontTitle = text1('title');
|
|
102
|
+
const heading = titleOf(scanned);
|
|
103
|
+
const id = config.id.source === 'filename' ? idFromName(name, config.id.separator) : text1('id');
|
|
104
|
+
// A marker is a comment on a line of its own. Quoted in a code block it is
|
|
105
|
+
// text: the prose mask keeps code and blanks comments, so a real marker is
|
|
106
|
+
// a line whose prose is empty.
|
|
107
|
+
const marker = (line, text) => lines[line].trim() === text && scanned.prose[line].trim() === '';
|
|
108
|
+
let banner = null;
|
|
109
|
+
const open = lines.findIndex((_, i) => i >= bodyStart && marker(i, BANNER_OPEN));
|
|
110
|
+
if (open >= 0) {
|
|
111
|
+
const close = lines.findIndex((_, i) => i > open && marker(i, BANNER_CLOSE));
|
|
112
|
+
if (close > open)
|
|
113
|
+
banner = { start: open, end: close + 1 };
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
id: id === null || id.trim() === '' ? null : id.trim(),
|
|
117
|
+
file,
|
|
118
|
+
name,
|
|
119
|
+
phase,
|
|
120
|
+
source: text,
|
|
121
|
+
text: content,
|
|
122
|
+
lines,
|
|
123
|
+
frontMatter,
|
|
124
|
+
bodyStart,
|
|
125
|
+
scan: scanned,
|
|
126
|
+
sections: sectionsOf(scanned),
|
|
127
|
+
tasks: scanned.tasks,
|
|
128
|
+
title: frontTitle ?? (heading === undefined || heading.text === '' ? null : heading.text),
|
|
129
|
+
status: statusFrom(statusWord, config, phase),
|
|
130
|
+
statusWord,
|
|
131
|
+
type: text1('type'),
|
|
132
|
+
wave,
|
|
133
|
+
dependsOn: list('dependsOn'),
|
|
134
|
+
affectedFiles: list('affectedFiles'),
|
|
135
|
+
protectedFiles: list('protectedFiles'),
|
|
136
|
+
integrity: text1('integrity'),
|
|
137
|
+
banner,
|
|
138
|
+
problems,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** The 0-based line a front-matter key is declared on, or the first line when it is absent. */
|
|
142
|
+
export function lineOfField(brief, field) {
|
|
143
|
+
return findEntry(brief.frontMatter, field)?.line ?? 0;
|
|
144
|
+
}
|
|
145
|
+
/** Keys spec-brief knows, compared the way front matter compares them. */
|
|
146
|
+
export function knownFields(config) {
|
|
147
|
+
const names = [...BUILT_IN_FIELDS, ...config.fields];
|
|
148
|
+
if (config.status.field !== null)
|
|
149
|
+
names.push(config.status.field);
|
|
150
|
+
return new Set(names.map(keyName));
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=brief.js.map
|