@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/rules.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in rules. Each is a pure function of one brief and the corpus
|
|
3
|
+
* around it, and returns what it found with 1-based lines.
|
|
4
|
+
*
|
|
5
|
+
* Structure is checked on live briefs only. An archived brief is a frozen
|
|
6
|
+
* record of the tree before a round, and holding it to today's schema would
|
|
7
|
+
* ask somebody to edit what must not be edited; the rules that do reach the
|
|
8
|
+
* archive are about identity and the freeze itself.
|
|
9
|
+
*/
|
|
10
|
+
import { BUILT_IN_FIELDS, knownFields, lineOfField } from './brief.js';
|
|
11
|
+
import { dependencyCycles, duplicateIds, idKey, resolveDependency } from './corpus.js';
|
|
12
|
+
import { integrityOf } from './integrity.js';
|
|
13
|
+
import { intersectGlobs, matchGlob, parseGlob } from './glob.js';
|
|
14
|
+
import { hasContent } from './markdown.js';
|
|
15
|
+
import { closest } from './schema.js';
|
|
16
|
+
import { labelMatches } from './text.js';
|
|
17
|
+
export function analyse(corpus) {
|
|
18
|
+
return { cycles: dependencyCycles(corpus), duplicates: duplicateIds(corpus) };
|
|
19
|
+
}
|
|
20
|
+
const at = (line0) => line0 + 1;
|
|
21
|
+
/** The sections a brief must carry: the configured ones, then its type's. */
|
|
22
|
+
export function sectionRules(brief, config) {
|
|
23
|
+
const typed = brief.type === null ? [] : (config.types[brief.type] ?? []);
|
|
24
|
+
return [...config.sections, ...typed];
|
|
25
|
+
}
|
|
26
|
+
/** The headings that fill a section rule. */
|
|
27
|
+
export function sectionsFilling(brief, rule) {
|
|
28
|
+
const names = [rule.name, ...rule.aliases];
|
|
29
|
+
return brief.sections.filter((s) => names.some((name) => labelMatches(s.heading.text, name)));
|
|
30
|
+
}
|
|
31
|
+
/** A section's own lines that carry content: comments dropped, sub-headings not counted. */
|
|
32
|
+
function contentLines(brief, section) {
|
|
33
|
+
const headingLines = new Set(brief.scan.headings.map((h) => h.line));
|
|
34
|
+
const lines = [];
|
|
35
|
+
for (let i = section.heading.line + 1; i < section.end; i += 1) {
|
|
36
|
+
if (!headingLines.has(i) && hasContent(brief.scan.prose[i]))
|
|
37
|
+
lines.push(i);
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
function isPlaceholder(line, placeholders) {
|
|
42
|
+
const text = line
|
|
43
|
+
.replace(/^\s*(?:[-*+]|\d{1,9}[.)])\s+/, '')
|
|
44
|
+
.replace(/^\[[ xX]\]\s*/, '')
|
|
45
|
+
.replace(/[*_]/g, '')
|
|
46
|
+
.trim();
|
|
47
|
+
if (text === '')
|
|
48
|
+
return true;
|
|
49
|
+
const lower = text.toLowerCase();
|
|
50
|
+
return placeholders.some((p) => {
|
|
51
|
+
const word = p.toLowerCase();
|
|
52
|
+
return lower === word || (lower.startsWith(word) && /^[\s:.\-\u2014]/.test(lower.slice(word.length)));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Completeness findings on a draft are reminders, not failures. */
|
|
56
|
+
function leniency(brief) {
|
|
57
|
+
return brief.status === 'draft' ? 'warning' : undefined;
|
|
58
|
+
}
|
|
59
|
+
function live(check) {
|
|
60
|
+
return (context) => (context.brief.phase === 'live' ? check(context) : []);
|
|
61
|
+
}
|
|
62
|
+
function globsOf(patterns, repoFiles) {
|
|
63
|
+
const files = repoFiles === null ? null : new Set(repoFiles);
|
|
64
|
+
return patterns.map((pattern) => {
|
|
65
|
+
const parsed = parseGlob(pattern, { isFile: files === null ? undefined : (path) => files.has(path) });
|
|
66
|
+
return parsed.ok ? { pattern, glob: parsed.glob, error: null } : { pattern, glob: null, error: parsed.error };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export const RULES = [
|
|
70
|
+
{
|
|
71
|
+
id: 'front-matter',
|
|
72
|
+
severity: 'error',
|
|
73
|
+
description: 'Front matter parses, with no duplicate keys and no YAML beyond the supported subset.',
|
|
74
|
+
check: ({ brief }) => (brief.frontMatter?.problems ?? []).map((p) => ({
|
|
75
|
+
line: at(p.line),
|
|
76
|
+
message: p.message,
|
|
77
|
+
hint: 'front matter is flat "key: value" lines, with lists as [a, b] or "- item" lines',
|
|
78
|
+
})),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'field',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
description: 'Fields spec-brief reads hold values of the right shape.',
|
|
84
|
+
check: ({ brief }) => brief.problems.map((p) => ({ line: at(p.line), message: p.message })),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'unknown-field',
|
|
88
|
+
severity: 'warning',
|
|
89
|
+
description: 'Front-matter keys are ones spec-brief or the configuration knows.',
|
|
90
|
+
check: ({ brief, config }) => {
|
|
91
|
+
const known = knownFields(config);
|
|
92
|
+
return (brief.frontMatter?.entries ?? [])
|
|
93
|
+
.filter((entry) => !known.has(entry.name))
|
|
94
|
+
.map((entry) => {
|
|
95
|
+
const guess = closest(entry.key, BUILT_IN_FIELDS);
|
|
96
|
+
return {
|
|
97
|
+
line: at(entry.line),
|
|
98
|
+
message: `"${entry.key}" is not a key spec-brief knows`,
|
|
99
|
+
hint: guess === undefined
|
|
100
|
+
? `if the repository uses it, list it under "fields" in the configuration`
|
|
101
|
+
: `did you mean "${guess}"? If not, list "${entry.key}" under "fields" in the configuration`,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'status',
|
|
108
|
+
severity: 'error',
|
|
109
|
+
description: 'The status agrees with where the brief lives.',
|
|
110
|
+
check: ({ brief, config }) => {
|
|
111
|
+
const field = config.status.field;
|
|
112
|
+
if (field === null)
|
|
113
|
+
return [];
|
|
114
|
+
const line = at(lineOfField(brief, field));
|
|
115
|
+
const expected = brief.phase === 'archived' ? config.status.archived : config.status.active;
|
|
116
|
+
if (brief.statusWord === null) {
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
line,
|
|
120
|
+
message: `declares no "${field}"`,
|
|
121
|
+
hint: `add "${field}: ${expected}" to the front matter`,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
if (brief.status === null) {
|
|
126
|
+
const words = [config.status.draft, config.status.active, config.status.archived].filter((w) => w !== null);
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
line,
|
|
130
|
+
message: `"${brief.statusWord}" is not a status here`,
|
|
131
|
+
hint: `use one of ${words.map((w) => `"${w}"`).join(', ')}`,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
if (brief.phase === 'live' && brief.status === 'archived') {
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
line,
|
|
139
|
+
message: `is marked "${brief.statusWord}" but lives in ${config.briefs}/`,
|
|
140
|
+
hint: `run "spec-brief archive ${brief.id ?? brief.name}", which moves it and writes its banner`,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
if (brief.phase === 'archived' && brief.status !== 'archived') {
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
line,
|
|
148
|
+
message: `lives in ${config.archive}/ but is marked "${brief.statusWord}"`,
|
|
149
|
+
hint: `run "spec-brief unarchive ${brief.id ?? brief.name}" to reopen it, or set "${field}: ${config.status.archived}"`,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
return [];
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'id',
|
|
158
|
+
severity: 'error',
|
|
159
|
+
description: 'Every brief has an id with no whitespace or slashes in it.',
|
|
160
|
+
check: ({ brief, config }) => {
|
|
161
|
+
if (brief.id === null) {
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
line: 1,
|
|
165
|
+
message: config.id.source === 'frontmatter' ? 'declares no "id"' : 'has no id in its file name',
|
|
166
|
+
hint: config.id.source === 'frontmatter' ? 'add "id: <id>" to the front matter' : undefined,
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
if (/[\s/\\]/.test(brief.id))
|
|
171
|
+
return [{ line: 1, message: `the id "${brief.id}" contains whitespace or a slash` }];
|
|
172
|
+
return [];
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'duplicate-id',
|
|
177
|
+
severity: 'error',
|
|
178
|
+
description: 'No two briefs share an id, live or archived.',
|
|
179
|
+
check: ({ brief, shared }) => {
|
|
180
|
+
if (brief.id === null)
|
|
181
|
+
return [];
|
|
182
|
+
const group = shared.duplicates.get(idKey(brief.id));
|
|
183
|
+
if (group === undefined || group[0] === brief)
|
|
184
|
+
return [];
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
line: 1,
|
|
188
|
+
message: `the id "${brief.id}" is also ${group[0].file}'s`,
|
|
189
|
+
hint: 'ids are allocated once and never reused; give this brief the next free one',
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'title',
|
|
196
|
+
severity: 'warning',
|
|
197
|
+
description: 'A brief has a title: a "title" field or a level-one heading.',
|
|
198
|
+
check: live(({ brief }) => brief.title === null ? [{ line: at(brief.bodyStart), message: 'has no title', hint: 'start the body with "# <title>"' }] : []),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'unknown-type',
|
|
202
|
+
severity: 'error',
|
|
203
|
+
description: 'A declared type is one the configuration defines.',
|
|
204
|
+
check: live(({ brief, config }) => {
|
|
205
|
+
if (brief.type === null || config.types[brief.type] !== undefined)
|
|
206
|
+
return [];
|
|
207
|
+
const names = Object.keys(config.types);
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
line: at(lineOfField(brief, 'type')),
|
|
211
|
+
message: `"${brief.type}" is not a brief type here`,
|
|
212
|
+
hint: names.length === 0 ? 'the configuration defines no types' : `use one of ${names.map((n) => `"${n}"`).join(', ')}`,
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'missing-section',
|
|
219
|
+
severity: 'error',
|
|
220
|
+
description: 'Every required section is present.',
|
|
221
|
+
check: live(({ brief, config }) => sectionRules(brief, config)
|
|
222
|
+
.filter((rule) => !rule.optional && sectionsFilling(brief, rule).length === 0)
|
|
223
|
+
.map((rule) => ({
|
|
224
|
+
line: at(brief.bodyStart),
|
|
225
|
+
message: `has no "${rule.name}" section`,
|
|
226
|
+
hint: rule.aliases.length === 0
|
|
227
|
+
? `add a "## ${rule.name}" heading`
|
|
228
|
+
: `add a "## ${rule.name}" heading (also accepted: ${rule.aliases.map((a) => `"${a}"`).join(', ')})`,
|
|
229
|
+
}))),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'duplicate-section',
|
|
233
|
+
severity: 'warning',
|
|
234
|
+
description: 'A section appears once.',
|
|
235
|
+
check: live(({ brief, config }) => sectionRules(brief, config).flatMap((rule) => sectionsFilling(brief, rule)
|
|
236
|
+
.slice(1)
|
|
237
|
+
.map((s) => ({ line: at(s.heading.line), message: `a second "${rule.name}" section`, hint: 'merge the two' })))),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'empty-section',
|
|
241
|
+
severity: 'error',
|
|
242
|
+
description: 'A section carries content; a comment alone is not content.',
|
|
243
|
+
check: live(({ brief, config }) => sectionRules(brief, config).flatMap((rule) => sectionsFilling(brief, rule)
|
|
244
|
+
.slice(0, 1)
|
|
245
|
+
.filter((s) => contentLines(brief, s).length === 0)
|
|
246
|
+
.map((s) => ({ line: at(s.heading.line), message: `the "${rule.name}" section is empty`, severity: leniency(brief) })))),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'placeholder',
|
|
250
|
+
severity: 'error',
|
|
251
|
+
description: 'A section says something: not only "TBD", "TODO" or an empty box.',
|
|
252
|
+
check: live(({ brief, config }) => sectionRules(brief, config).flatMap((rule) => sectionsFilling(brief, rule)
|
|
253
|
+
.slice(0, 1)
|
|
254
|
+
.filter((s) => {
|
|
255
|
+
const lines = contentLines(brief, s);
|
|
256
|
+
return lines.length > 0 && lines.every((i) => isPlaceholder(brief.scan.prose[i], config.placeholders));
|
|
257
|
+
})
|
|
258
|
+
.map((s) => ({
|
|
259
|
+
line: at(s.heading.line),
|
|
260
|
+
message: `the "${rule.name}" section holds only a placeholder`,
|
|
261
|
+
severity: leniency(brief),
|
|
262
|
+
})))),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'missing-checklist',
|
|
266
|
+
severity: 'error',
|
|
267
|
+
description: 'A checklist section holds at least one task item.',
|
|
268
|
+
check: live(({ brief, config }) => sectionRules(brief, config)
|
|
269
|
+
.filter((rule) => rule.checklist)
|
|
270
|
+
.flatMap((rule) => sectionsFilling(brief, rule)
|
|
271
|
+
.slice(0, 1)
|
|
272
|
+
.filter((s) => !brief.tasks.some((t) => t.line > s.heading.line && t.line < s.end))
|
|
273
|
+
.map((s) => ({
|
|
274
|
+
line: at(s.heading.line),
|
|
275
|
+
message: `the "${rule.name}" section has no "- [ ]" items`,
|
|
276
|
+
hint: 'write each check as a task item, so it has a state that can be closed',
|
|
277
|
+
severity: leniency(brief),
|
|
278
|
+
})))),
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: 'must-contain',
|
|
282
|
+
severity: 'error',
|
|
283
|
+
description: 'A section contains the text the configuration requires of it.',
|
|
284
|
+
check: live(({ brief, config }) => sectionRules(brief, config).flatMap((rule) => {
|
|
285
|
+
const section = sectionsFilling(brief, rule)[0];
|
|
286
|
+
if (section === undefined)
|
|
287
|
+
return [];
|
|
288
|
+
const body = brief.scan.prose.slice(section.heading.line + 1, section.end).join('\n');
|
|
289
|
+
return rule.mustContain
|
|
290
|
+
.filter((text) => !body.includes(text))
|
|
291
|
+
.map((text) => ({ line: at(section.heading.line), message: `the "${rule.name}" section must contain "${text}"` }));
|
|
292
|
+
})),
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'section-order',
|
|
296
|
+
severity: 'error',
|
|
297
|
+
description: 'Sections appear in the configured order, when the configuration asks for one.',
|
|
298
|
+
check: live(({ brief, config }) => {
|
|
299
|
+
if (!config.sectionOrder)
|
|
300
|
+
return [];
|
|
301
|
+
const placed = sectionRules(brief, config)
|
|
302
|
+
.map((rule) => ({ rule, section: sectionsFilling(brief, rule)[0] }))
|
|
303
|
+
.filter((p) => p.section !== undefined);
|
|
304
|
+
for (let i = 1; i < placed.length; i += 1) {
|
|
305
|
+
const before = placed[i - 1];
|
|
306
|
+
const current = placed[i];
|
|
307
|
+
if (current.section.heading.line < before.section.heading.line) {
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
line: at(current.section.heading.line),
|
|
311
|
+
message: `"${current.rule.name}" comes before "${before.rule.name}"`,
|
|
312
|
+
hint: `the order is ${sectionRules(brief, config)
|
|
313
|
+
.map((r) => r.name)
|
|
314
|
+
.join(', ')}`,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return [];
|
|
320
|
+
}),
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'dependency',
|
|
324
|
+
severity: 'error',
|
|
325
|
+
description: 'Every dependency names another brief that exists.',
|
|
326
|
+
check: live(({ brief, corpus }) => brief.dependsOn.flatMap((dependency) => {
|
|
327
|
+
const line = at(lineOfField(brief, 'dependsOn'));
|
|
328
|
+
if (brief.id !== null && idKey(dependency) === idKey(brief.id)) {
|
|
329
|
+
return [{ line, message: 'depends on itself' }];
|
|
330
|
+
}
|
|
331
|
+
if (resolveDependency(corpus, dependency) === undefined) {
|
|
332
|
+
return [{ line, message: `depends on "${dependency}", which is not a brief here` }];
|
|
333
|
+
}
|
|
334
|
+
return [];
|
|
335
|
+
})),
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: 'dependency-cycle',
|
|
339
|
+
severity: 'error',
|
|
340
|
+
description: 'Dependencies between live briefs form no cycle.',
|
|
341
|
+
check: live(({ brief, shared }) => shared.cycles
|
|
342
|
+
.filter((cycle) => cycle[0] === brief)
|
|
343
|
+
.map((cycle) => ({
|
|
344
|
+
line: at(lineOfField(brief, 'dependsOn')),
|
|
345
|
+
message: `dependencies form a cycle: ${cycle.map((b) => b.id ?? b.name).join(' -> ')}`,
|
|
346
|
+
hint: 'a cycle can never become ready; remove the dependency that is not real',
|
|
347
|
+
}))),
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: 'wave-order',
|
|
351
|
+
severity: 'error',
|
|
352
|
+
description: 'A dependency that is still live runs in an earlier wave.',
|
|
353
|
+
check: live(({ brief, corpus }) => {
|
|
354
|
+
const wave = brief.wave;
|
|
355
|
+
if (wave === null)
|
|
356
|
+
return [];
|
|
357
|
+
return brief.dependsOn.flatMap((dependency) => {
|
|
358
|
+
const target = resolveDependency(corpus, dependency);
|
|
359
|
+
// A self-dependency is reported once, by the dependency rule.
|
|
360
|
+
if (target === undefined || target === brief || target.phase !== 'live' || target.wave === null || target.wave < wave)
|
|
361
|
+
return [];
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
line: at(lineOfField(brief, 'dependsOn')),
|
|
365
|
+
message: `depends on ${target.id ?? target.name}, which is in wave ${target.wave}, not before wave ${wave}`,
|
|
366
|
+
hint: `move this brief to a wave after ${target.wave}, or ${target.id ?? target.name} to one before ${wave}`,
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
});
|
|
370
|
+
}),
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: 'glob',
|
|
374
|
+
severity: 'error',
|
|
375
|
+
description: 'Every scope pattern is a glob spec-brief can read.',
|
|
376
|
+
check: live(({ brief }) => ['affectedFiles', 'protectedFiles'].flatMap((field) => globsOf(brief[field], null)
|
|
377
|
+
.filter((g) => g.error !== null)
|
|
378
|
+
.map((g) => ({ line: at(lineOfField(brief, field)), message: `"${g.pattern}" in ${field}: ${g.error}` })))),
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'scope-contradiction',
|
|
382
|
+
severity: 'error',
|
|
383
|
+
description: 'No file is both in scope and protected.',
|
|
384
|
+
check: live(({ brief, repoFiles }) => {
|
|
385
|
+
const affected = globsOf(brief.affectedFiles, repoFiles).filter((g) => g.glob !== null);
|
|
386
|
+
const protectedGlobs = globsOf(brief.protectedFiles, repoFiles).filter((g) => g.glob !== null);
|
|
387
|
+
const results = [];
|
|
388
|
+
for (const a of affected) {
|
|
389
|
+
for (const p of protectedGlobs) {
|
|
390
|
+
const witness = intersectGlobs(a.glob, p.glob);
|
|
391
|
+
if (witness === null)
|
|
392
|
+
continue;
|
|
393
|
+
results.push({
|
|
394
|
+
line: at(lineOfField(brief, 'protectedFiles')),
|
|
395
|
+
message: `"${a.pattern}" is in scope and "${p.pattern}" is protected, and both cover ${witness}`,
|
|
396
|
+
hint: 'narrow the scope, or the protection, so that no file is both',
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return results;
|
|
401
|
+
}),
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: 'glob-matches-nothing',
|
|
405
|
+
severity: 'note',
|
|
406
|
+
description: 'A scope pattern matches at least one file in the tree.',
|
|
407
|
+
check: live(({ brief, repoFiles }) => {
|
|
408
|
+
if (repoFiles === null)
|
|
409
|
+
return [];
|
|
410
|
+
return ['affectedFiles', 'protectedFiles'].flatMap((field) => globsOf(brief[field], repoFiles)
|
|
411
|
+
.filter((g) => g.glob !== null && !repoFiles.some((file) => matchGlob(g.glob, file)))
|
|
412
|
+
.map((g) => ({
|
|
413
|
+
line: at(lineOfField(brief, field)),
|
|
414
|
+
message: `"${g.pattern}" in ${field} matches no file in the tree`,
|
|
415
|
+
hint: 'expected when the round creates it; otherwise check the spelling',
|
|
416
|
+
})));
|
|
417
|
+
}),
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: 'archive-freeze',
|
|
421
|
+
severity: 'error',
|
|
422
|
+
description: 'An archived brief has not changed since it was archived.',
|
|
423
|
+
check: ({ brief }) => {
|
|
424
|
+
if (brief.phase !== 'archived' || brief.integrity === null)
|
|
425
|
+
return [];
|
|
426
|
+
if (integrityOf(brief.text) === brief.integrity)
|
|
427
|
+
return [];
|
|
428
|
+
return [
|
|
429
|
+
{
|
|
430
|
+
line: at(lineOfField(brief, 'integrity')),
|
|
431
|
+
message: 'has changed since it was archived',
|
|
432
|
+
hint: 'an archived brief is frozen: restore it from git, or unarchive it to reopen the round',
|
|
433
|
+
},
|
|
434
|
+
];
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
];
|
|
438
|
+
/**
|
|
439
|
+
* The rules the collision analysis reports under. They are not run per brief,
|
|
440
|
+
* so they carry no check: they exist so configuration can set their severity
|
|
441
|
+
* and reports can describe them.
|
|
442
|
+
*/
|
|
443
|
+
export const COLLISION_RULES = [
|
|
444
|
+
{ id: 'collision', severity: 'error', description: 'Briefs in one wave do not write the same file.' },
|
|
445
|
+
{ id: 'unscoped', severity: 'note', description: 'A brief sharing a wave declares the files it writes.' },
|
|
446
|
+
{ id: 'shared-directory', severity: 'off', description: 'Briefs in one wave do not write into the same directory.' },
|
|
447
|
+
];
|
|
448
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The built-in rules. Each is a pure function of one brief and the corpus\n * around it, and returns what it found with 1-based lines.\n *\n * Structure is checked on live briefs only. An archived brief is a frozen\n * record of the tree before a round, and holding it to today's schema would\n * ask somebody to edit what must not be edited; the rules that do reach the\n * archive are about identity and the freeze itself.\n */\n\nimport { BUILT_IN_FIELDS, type Brief, knownFields, lineOfField } from './brief.js';\nimport type { Config, SectionRule } from './config.js';\nimport { type Corpus, dependencyCycles, duplicateIds, idKey, resolveDependency } from './corpus.js';\nimport { integrityOf } from './integrity.js';\nimport { type Glob, intersectGlobs, matchGlob, parseGlob } from './glob.js';\nimport { hasContent, type Section } from './markdown.js';\nimport { closest } from './schema.js';\nimport { labelMatches } from './text.js';\nimport type { Severity, SeveritySetting } from './types.js';\n\nexport interface RuleResult {\n /** 1-based. */\n readonly line: number;\n readonly message: string;\n readonly hint?: string | undefined;\n /** Lower than the rule's severity for this one finding, as a draft's unwritten sections are. */\n readonly severity?: Severity | undefined;\n}\n\nexport interface RuleContext {\n readonly brief: Brief;\n readonly corpus: Corpus;\n readonly config: Config;\n /** The files git sees, or every file without git; `null` when unknown. A rule that needs them stays quiet without them. */\n readonly repoFiles: readonly string[] | null;\n /** Analyses computed once per run and shared by every brief. */\n readonly shared: SharedAnalysis;\n /** A plugin's options from the configuration. */\n readonly options?: unknown;\n}\n\n/** What a rule is called, how loud it is by default, and what it checks. */\nexport interface RuleInfo {\n readonly id: string;\n readonly severity: SeveritySetting;\n readonly description: string;\n}\n\nexport interface Rule extends RuleInfo {\n check(context: RuleContext): readonly RuleResult[] | Promise<readonly RuleResult[]>;\n}\n\nexport interface SharedAnalysis {\n readonly cycles: readonly (readonly Brief[])[];\n readonly duplicates: ReadonlyMap<string, readonly Brief[]>;\n}\n\nexport function analyse(corpus: Corpus): SharedAnalysis {\n return { cycles: dependencyCycles(corpus), duplicates: duplicateIds(corpus) };\n}\n\nconst at = (line0: number): number => line0 + 1;\n\n/** The sections a brief must carry: the configured ones, then its type's. */\nexport function sectionRules(brief: Brief, config: Config): SectionRule[] {\n const typed = brief.type === null ? [] : (config.types[brief.type] ?? []);\n return [...config.sections, ...typed];\n}\n\n/** The headings that fill a section rule. */\nexport function sectionsFilling(brief: Brief, rule: SectionRule): Section[] {\n const names = [rule.name, ...rule.aliases];\n return brief.sections.filter((s) => names.some((name) => labelMatches(s.heading.text, name)));\n}\n\n/** A section's own lines that carry content: comments dropped, sub-headings not counted. */\nfunction contentLines(brief: Brief, section: Section): number[] {\n const headingLines = new Set(brief.scan.headings.map((h) => h.line));\n const lines: number[] = [];\n for (let i = section.heading.line + 1; i < section.end; i += 1) {\n if (!headingLines.has(i) && hasContent(brief.scan.prose[i] as string)) lines.push(i);\n }\n return lines;\n}\n\nfunction isPlaceholder(line: string, placeholders: readonly string[]): boolean {\n const text = line\n .replace(/^\\s*(?:[-*+]|\\d{1,9}[.)])\\s+/, '')\n .replace(/^\\[[ xX]\\]\\s*/, '')\n .replace(/[*_]/g, '')\n .trim();\n if (text === '') return true;\n const lower = text.toLowerCase();\n return placeholders.some((p) => {\n const word = p.toLowerCase();\n return lower === word || (lower.startsWith(word) && /^[\\s:.\\-\\u2014]/.test(lower.slice(word.length)));\n });\n}\n\n/** Completeness findings on a draft are reminders, not failures. */\nfunction leniency(brief: Brief): Severity | undefined {\n return brief.status === 'draft' ? 'warning' : undefined;\n}\n\nfunction live(check: (context: RuleContext) => RuleResult[]): (context: RuleContext) => RuleResult[] {\n return (context) => (context.brief.phase === 'live' ? check(context) : []);\n}\n\nfunction globsOf(\n patterns: readonly string[],\n repoFiles: readonly string[] | null,\n): { pattern: string; glob: Glob | null; error: string | null }[] {\n const files = repoFiles === null ? null : new Set(repoFiles);\n return patterns.map((pattern) => {\n const parsed = parseGlob(pattern, { isFile: files === null ? undefined : (path) => files.has(path) });\n return parsed.ok ? { pattern, glob: parsed.glob, error: null } : { pattern, glob: null, error: parsed.error };\n });\n}\n\nexport const RULES: readonly Rule[] = [\n {\n id: 'front-matter',\n severity: 'error',\n description: 'Front matter parses, with no duplicate keys and no YAML beyond the supported subset.',\n check: ({ brief }) =>\n (brief.frontMatter?.problems ?? []).map((p) => ({\n line: at(p.line),\n message: p.message,\n hint: 'front matter is flat \"key: value\" lines, with lists as [a, b] or \"- item\" lines',\n })),\n },\n {\n id: 'field',\n severity: 'error',\n description: 'Fields spec-brief reads hold values of the right shape.',\n check: ({ brief }) => brief.problems.map((p) => ({ line: at(p.line), message: p.message })),\n },\n {\n id: 'unknown-field',\n severity: 'warning',\n description: 'Front-matter keys are ones spec-brief or the configuration knows.',\n check: ({ brief, config }) => {\n const known = knownFields(config);\n return (brief.frontMatter?.entries ?? [])\n .filter((entry) => !known.has(entry.name))\n .map((entry) => {\n const guess = closest(entry.key, BUILT_IN_FIELDS);\n return {\n line: at(entry.line),\n message: `\"${entry.key}\" is not a key spec-brief knows`,\n hint:\n guess === undefined\n ? `if the repository uses it, list it under \"fields\" in the configuration`\n : `did you mean \"${guess}\"? If not, list \"${entry.key}\" under \"fields\" in the configuration`,\n };\n });\n },\n },\n {\n id: 'status',\n severity: 'error',\n description: 'The status agrees with where the brief lives.',\n check: ({ brief, config }) => {\n const field = config.status.field;\n if (field === null) return [];\n const line = at(lineOfField(brief, field));\n const expected = brief.phase === 'archived' ? config.status.archived : config.status.active;\n if (brief.statusWord === null) {\n return [\n {\n line,\n message: `declares no \"${field}\"`,\n hint: `add \"${field}: ${expected}\" to the front matter`,\n },\n ];\n }\n if (brief.status === null) {\n const words = [config.status.draft, config.status.active, config.status.archived].filter((w) => w !== null);\n return [\n {\n line,\n message: `\"${brief.statusWord}\" is not a status here`,\n hint: `use one of ${words.map((w) => `\"${w}\"`).join(', ')}`,\n },\n ];\n }\n if (brief.phase === 'live' && brief.status === 'archived') {\n return [\n {\n line,\n message: `is marked \"${brief.statusWord}\" but lives in ${config.briefs}/`,\n hint: `run \"spec-brief archive ${brief.id ?? brief.name}\", which moves it and writes its banner`,\n },\n ];\n }\n if (brief.phase === 'archived' && brief.status !== 'archived') {\n return [\n {\n line,\n message: `lives in ${config.archive}/ but is marked \"${brief.statusWord}\"`,\n hint: `run \"spec-brief unarchive ${brief.id ?? brief.name}\" to reopen it, or set \"${field}: ${config.status.archived}\"`,\n },\n ];\n }\n return [];\n },\n },\n {\n id: 'id',\n severity: 'error',\n description: 'Every brief has an id with no whitespace or slashes in it.',\n check: ({ brief, config }) => {\n if (brief.id === null) {\n return [\n {\n line: 1,\n message: config.id.source === 'frontmatter' ? 'declares no \"id\"' : 'has no id in its file name',\n hint: config.id.source === 'frontmatter' ? 'add \"id: <id>\" to the front matter' : undefined,\n },\n ];\n }\n if (/[\\s/\\\\]/.test(brief.id)) return [{ line: 1, message: `the id \"${brief.id}\" contains whitespace or a slash` }];\n return [];\n },\n },\n {\n id: 'duplicate-id',\n severity: 'error',\n description: 'No two briefs share an id, live or archived.',\n check: ({ brief, shared }) => {\n if (brief.id === null) return [];\n const group = shared.duplicates.get(idKey(brief.id));\n if (group === undefined || group[0] === brief) return [];\n return [\n {\n line: 1,\n message: `the id \"${brief.id}\" is also ${(group[0] as Brief).file}'s`,\n hint: 'ids are allocated once and never reused; give this brief the next free one',\n },\n ];\n },\n },\n {\n id: 'title',\n severity: 'warning',\n description: 'A brief has a title: a \"title\" field or a level-one heading.',\n check: live(({ brief }) =>\n brief.title === null ? [{ line: at(brief.bodyStart), message: 'has no title', hint: 'start the body with \"# <title>\"' }] : [],\n ),\n },\n {\n id: 'unknown-type',\n severity: 'error',\n description: 'A declared type is one the configuration defines.',\n check: live(({ brief, config }) => {\n if (brief.type === null || config.types[brief.type] !== undefined) return [];\n const names = Object.keys(config.types);\n return [\n {\n line: at(lineOfField(brief, 'type')),\n message: `\"${brief.type}\" is not a brief type here`,\n hint: names.length === 0 ? 'the configuration defines no types' : `use one of ${names.map((n) => `\"${n}\"`).join(', ')}`,\n },\n ];\n }),\n },\n {\n id: 'missing-section',\n severity: 'error',\n description: 'Every required section is present.',\n check: live(({ brief, config }) =>\n sectionRules(brief, config)\n .filter((rule) => !rule.optional && sectionsFilling(brief, rule).length === 0)\n .map((rule) => ({\n line: at(brief.bodyStart),\n message: `has no \"${rule.name}\" section`,\n hint:\n rule.aliases.length === 0\n ? `add a \"## ${rule.name}\" heading`\n : `add a \"## ${rule.name}\" heading (also accepted: ${rule.aliases.map((a) => `\"${a}\"`).join(', ')})`,\n })),\n ),\n },\n {\n id: 'duplicate-section',\n severity: 'warning',\n description: 'A section appears once.',\n check: live(({ brief, config }) =>\n sectionRules(brief, config).flatMap((rule) =>\n sectionsFilling(brief, rule)\n .slice(1)\n .map((s) => ({ line: at(s.heading.line), message: `a second \"${rule.name}\" section`, hint: 'merge the two' })),\n ),\n ),\n },\n {\n id: 'empty-section',\n severity: 'error',\n description: 'A section carries content; a comment alone is not content.',\n check: live(({ brief, config }) =>\n sectionRules(brief, config).flatMap((rule) =>\n sectionsFilling(brief, rule)\n .slice(0, 1)\n .filter((s) => contentLines(brief, s).length === 0)\n .map((s) => ({ line: at(s.heading.line), message: `the \"${rule.name}\" section is empty`, severity: leniency(brief) })),\n ),\n ),\n },\n {\n id: 'placeholder',\n severity: 'error',\n description: 'A section says something: not only \"TBD\", \"TODO\" or an empty box.',\n check: live(({ brief, config }) =>\n sectionRules(brief, config).flatMap((rule) =>\n sectionsFilling(brief, rule)\n .slice(0, 1)\n .filter((s) => {\n const lines = contentLines(brief, s);\n return lines.length > 0 && lines.every((i) => isPlaceholder(brief.scan.prose[i] as string, config.placeholders));\n })\n .map((s) => ({\n line: at(s.heading.line),\n message: `the \"${rule.name}\" section holds only a placeholder`,\n severity: leniency(brief),\n })),\n ),\n ),\n },\n {\n id: 'missing-checklist',\n severity: 'error',\n description: 'A checklist section holds at least one task item.',\n check: live(({ brief, config }) =>\n sectionRules(brief, config)\n .filter((rule) => rule.checklist)\n .flatMap((rule) =>\n sectionsFilling(brief, rule)\n .slice(0, 1)\n .filter((s) => !brief.tasks.some((t) => t.line > s.heading.line && t.line < s.end))\n .map((s) => ({\n line: at(s.heading.line),\n message: `the \"${rule.name}\" section has no \"- [ ]\" items`,\n hint: 'write each check as a task item, so it has a state that can be closed',\n severity: leniency(brief),\n })),\n ),\n ),\n },\n {\n id: 'must-contain',\n severity: 'error',\n description: 'A section contains the text the configuration requires of it.',\n check: live(({ brief, config }) =>\n sectionRules(brief, config).flatMap((rule) => {\n const section = sectionsFilling(brief, rule)[0];\n if (section === undefined) return [];\n const body = brief.scan.prose.slice(section.heading.line + 1, section.end).join('\\n');\n return rule.mustContain\n .filter((text) => !body.includes(text))\n .map((text) => ({ line: at(section.heading.line), message: `the \"${rule.name}\" section must contain \"${text}\"` }));\n }),\n ),\n },\n {\n id: 'section-order',\n severity: 'error',\n description: 'Sections appear in the configured order, when the configuration asks for one.',\n check: live(({ brief, config }) => {\n if (!config.sectionOrder) return [];\n const placed = sectionRules(brief, config)\n .map((rule) => ({ rule, section: sectionsFilling(brief, rule)[0] }))\n .filter((p): p is { rule: SectionRule; section: Section } => p.section !== undefined);\n for (let i = 1; i < placed.length; i += 1) {\n const before = placed[i - 1] as { rule: SectionRule; section: Section };\n const current = placed[i] as { rule: SectionRule; section: Section };\n if (current.section.heading.line < before.section.heading.line) {\n return [\n {\n line: at(current.section.heading.line),\n message: `\"${current.rule.name}\" comes before \"${before.rule.name}\"`,\n hint: `the order is ${sectionRules(brief, config)\n .map((r) => r.name)\n .join(', ')}`,\n },\n ];\n }\n }\n return [];\n }),\n },\n {\n id: 'dependency',\n severity: 'error',\n description: 'Every dependency names another brief that exists.',\n check: live(({ brief, corpus }) =>\n brief.dependsOn.flatMap((dependency) => {\n const line = at(lineOfField(brief, 'dependsOn'));\n if (brief.id !== null && idKey(dependency) === idKey(brief.id)) {\n return [{ line, message: 'depends on itself' }];\n }\n if (resolveDependency(corpus, dependency) === undefined) {\n return [{ line, message: `depends on \"${dependency}\", which is not a brief here` }];\n }\n return [];\n }),\n ),\n },\n {\n id: 'dependency-cycle',\n severity: 'error',\n description: 'Dependencies between live briefs form no cycle.',\n check: live(({ brief, shared }) =>\n shared.cycles\n .filter((cycle) => cycle[0] === brief)\n .map((cycle) => ({\n line: at(lineOfField(brief, 'dependsOn')),\n message: `dependencies form a cycle: ${cycle.map((b) => b.id ?? b.name).join(' -> ')}`,\n hint: 'a cycle can never become ready; remove the dependency that is not real',\n })),\n ),\n },\n {\n id: 'wave-order',\n severity: 'error',\n description: 'A dependency that is still live runs in an earlier wave.',\n check: live(({ brief, corpus }) => {\n const wave = brief.wave;\n if (wave === null) return [];\n return brief.dependsOn.flatMap((dependency) => {\n const target = resolveDependency(corpus, dependency);\n // A self-dependency is reported once, by the dependency rule.\n if (target === undefined || target === brief || target.phase !== 'live' || target.wave === null || target.wave < wave) return [];\n return [\n {\n line: at(lineOfField(brief, 'dependsOn')),\n message: `depends on ${target.id ?? target.name}, which is in wave ${target.wave}, not before wave ${wave}`,\n hint: `move this brief to a wave after ${target.wave}, or ${target.id ?? target.name} to one before ${wave}`,\n },\n ];\n });\n }),\n },\n {\n id: 'glob',\n severity: 'error',\n description: 'Every scope pattern is a glob spec-brief can read.',\n check: live(({ brief }) =>\n (['affectedFiles', 'protectedFiles'] as const).flatMap((field) =>\n globsOf(brief[field], null)\n .filter((g) => g.error !== null)\n .map((g) => ({ line: at(lineOfField(brief, field)), message: `\"${g.pattern}\" in ${field}: ${g.error as string}` })),\n ),\n ),\n },\n {\n id: 'scope-contradiction',\n severity: 'error',\n description: 'No file is both in scope and protected.',\n check: live(({ brief, repoFiles }) => {\n const affected = globsOf(brief.affectedFiles, repoFiles).filter((g) => g.glob !== null);\n const protectedGlobs = globsOf(brief.protectedFiles, repoFiles).filter((g) => g.glob !== null);\n const results: RuleResult[] = [];\n for (const a of affected) {\n for (const p of protectedGlobs) {\n const witness = intersectGlobs(a.glob as Glob, p.glob as Glob);\n if (witness === null) continue;\n results.push({\n line: at(lineOfField(brief, 'protectedFiles')),\n message: `\"${a.pattern}\" is in scope and \"${p.pattern}\" is protected, and both cover ${witness}`,\n hint: 'narrow the scope, or the protection, so that no file is both',\n });\n }\n }\n return results;\n }),\n },\n {\n id: 'glob-matches-nothing',\n severity: 'note',\n description: 'A scope pattern matches at least one file in the tree.',\n check: live(({ brief, repoFiles }) => {\n if (repoFiles === null) return [];\n return (['affectedFiles', 'protectedFiles'] as const).flatMap((field) =>\n globsOf(brief[field], repoFiles)\n .filter((g) => g.glob !== null && !repoFiles.some((file) => matchGlob(g.glob as Glob, file)))\n .map((g) => ({\n line: at(lineOfField(brief, field)),\n message: `\"${g.pattern}\" in ${field} matches no file in the tree`,\n hint: 'expected when the round creates it; otherwise check the spelling',\n })),\n );\n }),\n },\n {\n id: 'archive-freeze',\n severity: 'error',\n description: 'An archived brief has not changed since it was archived.',\n check: ({ brief }) => {\n if (brief.phase !== 'archived' || brief.integrity === null) return [];\n if (integrityOf(brief.text) === brief.integrity) return [];\n return [\n {\n line: at(lineOfField(brief, 'integrity')),\n message: 'has changed since it was archived',\n hint: 'an archived brief is frozen: restore it from git, or unarchive it to reopen the round',\n },\n ];\n },\n },\n];\n\n/**\n * The rules the collision analysis reports under. They are not run per brief,\n * so they carry no check: they exist so configuration can set their severity\n * and reports can describe them.\n */\nexport const COLLISION_RULES: readonly RuleInfo[] = [\n { id: 'collision', severity: 'error', description: 'Briefs in one wave do not write the same file.' },\n { id: 'unscoped', severity: 'note', description: 'A brief sharing a wave declares the files it writes.' },\n { id: 'shared-directory', severity: 'off', description: 'Briefs in one wave do not write into the same directory.' },\n];\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a new brief: the next free id, a file name, and a body with every
|
|
3
|
+
* section the configuration requires, each holding a hint in a comment.
|
|
4
|
+
* Comments are not content, so a freshly scaffolded brief reads as unwritten
|
|
5
|
+
* until somebody writes it.
|
|
6
|
+
*/
|
|
7
|
+
import type { Config } from './config.js';
|
|
8
|
+
import type { Corpus } from './corpus.js';
|
|
9
|
+
export interface NewBrief {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly date: string;
|
|
13
|
+
readonly type?: string | undefined;
|
|
14
|
+
readonly wave?: number | undefined;
|
|
15
|
+
readonly dependsOn?: readonly string[] | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The next id: one more than the highest numeric id, live or archived, at the
|
|
19
|
+
* configured width. Ids are never reused, so an archived brief still holds
|
|
20
|
+
* its number. `null` when the corpus has ids but none of them are numbers.
|
|
21
|
+
*/
|
|
22
|
+
export declare function nextId(corpus: Corpus): string | null;
|
|
23
|
+
export declare function fileNameFor(config: Config, id: string, title: string): string;
|
|
24
|
+
/** The text of a new brief, from the configured template or from the section list. */
|
|
25
|
+
export declare function renderNewBrief(config: Config, brief: NewBrief, template: string | null): string;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a new brief: the next free id, a file name, and a body with every
|
|
3
|
+
* section the configuration requires, each holding a hint in a comment.
|
|
4
|
+
* Comments are not content, so a freshly scaffolded brief reads as unwritten
|
|
5
|
+
* until somebody writes it.
|
|
6
|
+
*/
|
|
7
|
+
import { renderScalar } from './frontmatter.js';
|
|
8
|
+
import { fillTemplate, slugify } from './text.js';
|
|
9
|
+
/**
|
|
10
|
+
* The next id: one more than the highest numeric id, live or archived, at the
|
|
11
|
+
* configured width. Ids are never reused, so an archived brief still holds
|
|
12
|
+
* its number. `null` when the corpus has ids but none of them are numbers.
|
|
13
|
+
*/
|
|
14
|
+
export function nextId(corpus) {
|
|
15
|
+
const ids = corpus.briefs.map((b) => b.id).filter((id) => id !== null);
|
|
16
|
+
const numeric = ids.filter((id) => /^\d+$/.test(id)).map(Number);
|
|
17
|
+
if (numeric.length === 0 && ids.length > 0)
|
|
18
|
+
return null;
|
|
19
|
+
const next = numeric.length === 0 ? 1 : Math.max(...numeric) + 1;
|
|
20
|
+
return String(next).padStart(corpus.config.id.digits, '0');
|
|
21
|
+
}
|
|
22
|
+
export function fileNameFor(config, id, title) {
|
|
23
|
+
const slug = slugify(title);
|
|
24
|
+
if (config.id.source === 'frontmatter')
|
|
25
|
+
return `${slug === '' ? id : slug}.md`;
|
|
26
|
+
return slug === '' ? `${id}.md` : `${id}${config.id.separator}${slug}.md`;
|
|
27
|
+
}
|
|
28
|
+
const HINTS = {
|
|
29
|
+
intent: 'The state of the tree when this round is done, and why it matters. One paragraph.',
|
|
30
|
+
'negative scope': 'What this round must not do, even where it would look helpful.',
|
|
31
|
+
'not empowered': 'Files, interfaces and decisions this round may not change.',
|
|
32
|
+
invariants: 'Checks that must hold before the round is called done, each one a task item.',
|
|
33
|
+
'acceptance criteria': 'What a reviewer checks to accept the result, each one a task item.',
|
|
34
|
+
'the defect, measured': 'How to reproduce the defect, and the measurement that shows it.',
|
|
35
|
+
};
|
|
36
|
+
function hint(rule) {
|
|
37
|
+
return HINTS[rule.name.toLowerCase()] ?? `Write the ${rule.name}.`;
|
|
38
|
+
}
|
|
39
|
+
function frontMatter(config, brief) {
|
|
40
|
+
const lines = ['---'];
|
|
41
|
+
if (config.id.source === 'frontmatter')
|
|
42
|
+
lines.push(`id: ${renderScalar(brief.id)}`);
|
|
43
|
+
if (config.status.field !== null) {
|
|
44
|
+
lines.push(`${config.status.field}: ${renderScalar(config.status.draft ?? config.status.active)}`);
|
|
45
|
+
}
|
|
46
|
+
lines.push(`date: ${brief.date}`);
|
|
47
|
+
if (brief.type !== undefined)
|
|
48
|
+
lines.push(`type: ${renderScalar(brief.type)}`);
|
|
49
|
+
if (brief.wave !== undefined)
|
|
50
|
+
lines.push(`wave: ${brief.wave}`);
|
|
51
|
+
if (brief.dependsOn !== undefined && brief.dependsOn.length > 0) {
|
|
52
|
+
lines.push(`dependsOn: [${brief.dependsOn.map((d) => JSON.stringify(d)).join(', ')}]`);
|
|
53
|
+
}
|
|
54
|
+
lines.push('affectedFiles: []', 'protectedFiles: []', '---');
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
/** The text of a new brief, from the configured template or from the section list. */
|
|
58
|
+
export function renderNewBrief(config, brief, template) {
|
|
59
|
+
if (template !== null) {
|
|
60
|
+
return fillTemplate(template, {
|
|
61
|
+
id: brief.id,
|
|
62
|
+
title: brief.title,
|
|
63
|
+
date: brief.date,
|
|
64
|
+
type: brief.type ?? '',
|
|
65
|
+
wave: brief.wave === undefined ? '' : String(brief.wave),
|
|
66
|
+
status: config.status.draft ?? config.status.active,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const typed = brief.type === undefined ? [] : (config.types[brief.type] ?? []);
|
|
70
|
+
const lines = [...frontMatter(config, brief), '', `# ${brief.id} \u2014 ${brief.title}`];
|
|
71
|
+
for (const rule of [...config.sections, ...typed]) {
|
|
72
|
+
lines.push('', `## ${rule.name}`, '', `<!-- ${hint(rule)} -->`);
|
|
73
|
+
// Text a section must contain is written in, so it is not forgotten.
|
|
74
|
+
if (rule.mustContain.length > 0)
|
|
75
|
+
lines.push('', ...rule.mustContain.map((text) => `- ${text}`));
|
|
76
|
+
if (rule.checklist)
|
|
77
|
+
lines.push('', '- [ ] <!-- one check -->');
|
|
78
|
+
}
|
|
79
|
+
return `${lines.join('\n')}\n`;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=scaffold.js.map
|