@descent-vtt/spec-brief 0.1.0 → 0.2.1
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 +301 -0
- package/README.md +92 -22
- package/dist/archive.d.ts +52 -2
- package/dist/archive.js +205 -37
- package/dist/archive.js.map +1 -1
- package/dist/brief.d.ts +2 -0
- package/dist/brief.js +5 -1
- package/dist/brief.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +48 -14
- package/dist/cli.js.map +1 -1
- package/dist/collisions.d.ts +43 -12
- package/dist/collisions.js +90 -58
- package/dist/collisions.js.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.js +66 -11
- package/dist/config.js.map +1 -1
- package/dist/corpus.d.ts +21 -1
- package/dist/corpus.js +51 -2
- package/dist/corpus.js.map +1 -1
- package/dist/engine.d.ts +33 -3
- package/dist/engine.js +112 -9
- package/dist/engine.js.map +1 -1
- package/dist/frontmatter.d.ts +18 -61
- package/dist/frontmatter.js +31 -300
- package/dist/frontmatter.js.map +1 -1
- package/dist/glob.d.ts +77 -54
- package/dist/glob.js +104 -433
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +1 -1
- package/dist/links.js +3 -4
- package/dist/links.js.map +1 -1
- package/dist/lint.d.ts +33 -2
- package/dist/lint.js +9 -2
- package/dist/lint.js.map +1 -1
- package/dist/markdown.d.ts +27 -23
- package/dist/markdown.js +66 -235
- package/dist/markdown.js.map +1 -1
- package/dist/plugins.d.ts +14 -0
- package/dist/plugins.js +142 -6
- package/dist/plugins.js.map +1 -1
- package/dist/report.d.ts +40 -3
- package/dist/report.js +227 -11
- package/dist/report.js.map +1 -1
- package/dist/rules.d.ts +22 -4
- package/dist/rules.js +175 -45
- package/dist/rules.js.map +1 -1
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +10 -9
- package/dist/scaffold.js.map +1 -1
- package/dist/schedule.d.ts +133 -0
- package/dist/schedule.js +265 -0
- package/dist/schedule.js.map +1 -0
- package/dist/scope.d.ts +52 -0
- package/dist/scope.js +56 -0
- package/dist/scope.js.map +1 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +23 -0
- package/dist/text.js.map +1 -1
- package/dist/trigger.d.ts +17 -0
- package/dist/trigger.js +49 -0
- package/dist/trigger.js.map +1 -0
- package/dist/types.d.ts +14 -5
- package/dist/types.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +25 -0
- package/dist/vendor/spec-core/markdown/links.js +394 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
- package/dist/vendor/spec-core/markdown/scan.js +463 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +290 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +83 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +2 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +72 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/package.json +4 -3
- package/schema.json +17 -0
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/dist/collisions.js
CHANGED
|
@@ -4,63 +4,70 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Scopes are compared as globs, not as strings. `src/auth/**` and
|
|
6
6
|
* `src/**\/session.ts` share no prefix a string comparison would see and both
|
|
7
|
-
* cover `src/auth/session.ts`; the
|
|
8
|
-
*
|
|
9
|
-
* is
|
|
7
|
+
* cover `src/auth/session.ts`; the witness search finds that, and names the
|
|
8
|
+
* file. What a brief protects it will not write, so a file either brief of a
|
|
9
|
+
* pair protects is no collision. A brief that declares no scope cannot be
|
|
10
|
+
* proven apart from anything, so it is reported as unscoped rather than
|
|
11
|
+
* silently counted as safe, and a pair the search could not decide is reported
|
|
12
|
+
* as undecided rather than as either.
|
|
13
|
+
*
|
|
14
|
+
* Two briefs that collide are one defect however many of their patterns
|
|
15
|
+
* meet: they are fixed together, by a wave, a dependency or a narrower scope.
|
|
16
|
+
* So a pair is one collision, carrying every pair of patterns that meets.
|
|
10
17
|
*/
|
|
11
18
|
import { lineOfField } from './brief.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return parsed.ok ? [{ pattern, glob: parsed.glob }] : [];
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
function matrix(wave, briefs, isFile) {
|
|
19
|
+
import { waitingOnDeferred } from './corpus.js';
|
|
20
|
+
import { globBases, readingIn, WITNESS_BUDGET } from './glob.js';
|
|
21
|
+
import { configuredSeverity } from './lint.js';
|
|
22
|
+
import { meet, scopeOf } from './scope.js';
|
|
23
|
+
import { inWords } from './text.js';
|
|
24
|
+
function matrix(wave, briefs, scopes, budget) {
|
|
22
25
|
const collisions = [];
|
|
26
|
+
const undecided = [];
|
|
23
27
|
const shared = [];
|
|
24
|
-
const scoped = briefs.map((brief) =>
|
|
28
|
+
const scoped = briefs.map((brief) => scopes.get(brief));
|
|
25
29
|
for (let i = 0; i < scoped.length; i += 1) {
|
|
26
30
|
for (let j = i + 1; j < scoped.length; j += 1) {
|
|
27
31
|
const left = scoped[i];
|
|
28
32
|
const right = scoped[j];
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (witness !== null)
|
|
34
|
-
found.push({ a: left.brief, b: right.brief, patterns: [x.pattern, y.pattern], witness });
|
|
35
|
-
}
|
|
33
|
+
const meeting = meet(left, right, budget);
|
|
34
|
+
if (meeting.overlaps.length > 0) {
|
|
35
|
+
collisions.push({ a: left.brief, b: right.brief, overlaps: meeting.overlaps });
|
|
36
|
+
continue;
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (meeting.undecided.length > 0) {
|
|
39
|
+
undecided.push({ a: left.brief, b: right.brief, patterns: meeting.undecided });
|
|
39
40
|
continue;
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
}
|
|
42
|
+
const leftDirs = new Set(left.affected.flatMap((s) => globBases(s.glob)).filter((d) => d !== ''));
|
|
43
|
+
const directories = [...new Set(right.affected.flatMap((s) => globBases(s.glob)))].filter((d) => leftDirs.has(d)).sort();
|
|
44
|
+
if (directories.length > 0)
|
|
45
|
+
shared.push({ a: left.brief, b: right.brief, directories });
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
const unscoped = briefs.length > 1 ? scoped.filter((s) => s.
|
|
47
|
-
return { wave, briefs, collisions, shared, unscoped };
|
|
48
|
+
const unscoped = briefs.length > 1 ? scoped.filter((s) => s.affected.length === 0).map((s) => s.brief) : [];
|
|
49
|
+
return { wave, briefs, collisions, undecided, shared, unscoped };
|
|
48
50
|
}
|
|
49
51
|
export function collisions(corpus, options = {}) {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
+
const reading = readingIn(options.repoFiles ?? null);
|
|
53
|
+
const budget = options.budget ?? WITNESS_BUDGET;
|
|
54
|
+
const deferred = corpus.live.filter((b) => b.status === 'deferred');
|
|
55
|
+
const waiting = waitingOnDeferred(corpus);
|
|
56
|
+
const held = new Set(waiting.map((w) => w.brief));
|
|
57
|
+
const live = corpus.live.filter((b) => b.status !== 'deferred' && !held.has(b));
|
|
58
|
+
const scopes = new Map(live.map((brief) => [brief, scopeOf(brief, reading)]));
|
|
52
59
|
if (options.all === true)
|
|
53
|
-
return { waves: [matrix(null,
|
|
60
|
+
return { waves: [matrix(null, live, scopes, budget)], unscheduled: [], deferred, waiting };
|
|
54
61
|
const byWave = new Map();
|
|
55
62
|
const unscheduled = [];
|
|
56
|
-
for (const brief of
|
|
63
|
+
for (const brief of live) {
|
|
57
64
|
if (brief.wave === null)
|
|
58
65
|
unscheduled.push(brief);
|
|
59
66
|
else
|
|
60
67
|
byWave.set(brief.wave, [...(byWave.get(brief.wave) ?? []), brief]);
|
|
61
68
|
}
|
|
62
|
-
const waves = [...byWave.keys()].sort((a, b) => a - b).map((wave) => matrix(wave, byWave.get(wave),
|
|
63
|
-
return { waves, unscheduled };
|
|
69
|
+
const waves = [...byWave.keys()].sort((a, b) => a - b).map((wave) => matrix(wave, byWave.get(wave), scopes, budget));
|
|
70
|
+
return { waves, unscheduled, deferred, waiting };
|
|
64
71
|
}
|
|
65
72
|
function label(brief) {
|
|
66
73
|
return brief.id ?? brief.name;
|
|
@@ -68,27 +75,44 @@ function label(brief) {
|
|
|
68
75
|
function where(wave) {
|
|
69
76
|
return wave === null ? 'among the live briefs' : `in wave ${wave}`;
|
|
70
77
|
}
|
|
78
|
+
function collisionMessage(c, wave) {
|
|
79
|
+
if (c.overlaps.length === 1) {
|
|
80
|
+
const only = c.overlaps[0];
|
|
81
|
+
return `"${only.patterns[1]}" overlaps ${label(c.a)}'s "${only.patterns[0]}" ${where(wave)}; both cover ${only.witness}`;
|
|
82
|
+
}
|
|
83
|
+
const each = c.overlaps.map((o) => `"${o.patterns[1]}" and ${label(c.a)}'s "${o.patterns[0]}" both cover ${o.witness}`);
|
|
84
|
+
return `overlaps ${label(c.a)} ${where(wave)} through ${c.overlaps.length} pairs of patterns: ${each.join('; ')}`;
|
|
85
|
+
}
|
|
86
|
+
/** A pair whose collision the search could not decide, as one finding on the later brief. */
|
|
87
|
+
export function undecidedFinding(severity, a, b, patterns, wave) {
|
|
88
|
+
const pairs = patterns.map(([x, y]) => `"${y}" and ${label(a)}'s "${x}"`);
|
|
89
|
+
return {
|
|
90
|
+
rule: 'collision-undecided',
|
|
91
|
+
severity,
|
|
92
|
+
message: `whether it can write a file ${label(a)} writes ${where(wave)} is undecided: the search met its budget for ${inWords(pairs)}`,
|
|
93
|
+
file: b.file,
|
|
94
|
+
line: lineOfField(b, 'affectedFiles') + 1,
|
|
95
|
+
brief: b.id ?? undefined,
|
|
96
|
+
hint: 'narrow one of the patterns, or run the two in different waves',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
71
99
|
/**
|
|
72
|
-
* The report as findings. One
|
|
73
|
-
* brief of the pair, which is usually the one still being written.
|
|
100
|
+
* The report as findings. One pair of briefs is one finding, placed on the
|
|
101
|
+
* later brief of the pair, which is usually the one still being written.
|
|
74
102
|
*/
|
|
75
103
|
export function collisionFindings(corpus, report) {
|
|
76
|
-
const severity = (id) => {
|
|
77
|
-
const rule = COLLISION_RULES.find((r) => r.id === id);
|
|
78
|
-
const setting = severityOf(corpus, id, rule.severity);
|
|
79
|
-
return setting === 'off' ? null : setting;
|
|
80
|
-
};
|
|
81
104
|
const findings = [];
|
|
82
|
-
const collision =
|
|
83
|
-
const
|
|
84
|
-
const
|
|
105
|
+
const collision = configuredSeverity(corpus, 'collision');
|
|
106
|
+
const undecided = configuredSeverity(corpus, 'collision-undecided');
|
|
107
|
+
const unscoped = configuredSeverity(corpus, 'unscoped');
|
|
108
|
+
const sharedDirectory = configuredSeverity(corpus, 'shared-directory');
|
|
85
109
|
for (const wave of report.waves) {
|
|
86
110
|
if (collision !== null) {
|
|
87
111
|
for (const c of wave.collisions) {
|
|
88
112
|
findings.push({
|
|
89
113
|
rule: 'collision',
|
|
90
114
|
severity: collision,
|
|
91
|
-
message:
|
|
115
|
+
message: collisionMessage(c, wave.wave),
|
|
92
116
|
file: c.b.file,
|
|
93
117
|
line: lineOfField(c.b, 'affectedFiles') + 1,
|
|
94
118
|
brief: c.b.id ?? undefined,
|
|
@@ -96,32 +120,40 @@ export function collisionFindings(corpus, report) {
|
|
|
96
120
|
});
|
|
97
121
|
}
|
|
98
122
|
}
|
|
123
|
+
if (undecided !== null) {
|
|
124
|
+
for (const u of wave.undecided)
|
|
125
|
+
findings.push(undecidedFinding(undecided, u.a, u.b, u.patterns, wave.wave));
|
|
126
|
+
}
|
|
99
127
|
if (sharedDirectory !== null) {
|
|
100
128
|
for (const s of wave.shared) {
|
|
101
129
|
findings.push({
|
|
102
130
|
rule: 'shared-directory',
|
|
103
131
|
severity: sharedDirectory,
|
|
104
|
-
message: `writes into ${s.
|
|
132
|
+
message: `writes into ${inWords(s.directories.map((d) => `${d}/`))}, as ${label(s.a)} does ${where(wave.wave)}`,
|
|
105
133
|
file: s.b.file,
|
|
106
134
|
line: lineOfField(s.b, 'affectedFiles') + 1,
|
|
107
135
|
brief: s.b.id ?? undefined,
|
|
136
|
+
hint: 'check that the two do not depend on one decision in that directory; if they do, order them',
|
|
108
137
|
});
|
|
109
138
|
}
|
|
110
139
|
}
|
|
111
140
|
if (unscoped !== null) {
|
|
112
|
-
for (const brief of wave.unscoped)
|
|
113
|
-
findings.push({
|
|
114
|
-
rule: 'unscoped',
|
|
115
|
-
severity: unscoped,
|
|
116
|
-
message: `declares no affectedFiles, so it cannot be checked against the ${wave.briefs.length - 1} other brief(s) ${where(wave.wave)}`,
|
|
117
|
-
file: brief.file,
|
|
118
|
-
line: 1,
|
|
119
|
-
brief: brief.id ?? undefined,
|
|
120
|
-
hint: 'list the files or globs this round writes under "affectedFiles"',
|
|
121
|
-
});
|
|
122
|
-
}
|
|
141
|
+
for (const brief of wave.unscoped)
|
|
142
|
+
findings.push(unscopedFinding(unscoped, brief, `the ${wave.briefs.length - 1} other brief(s) ${where(wave.wave)}`));
|
|
123
143
|
}
|
|
124
144
|
}
|
|
125
145
|
return findings;
|
|
126
146
|
}
|
|
147
|
+
/** A brief with no scope, which nothing can be proved apart from. */
|
|
148
|
+
export function unscopedFinding(severity, brief, others) {
|
|
149
|
+
return {
|
|
150
|
+
rule: 'unscoped',
|
|
151
|
+
severity,
|
|
152
|
+
message: `declares no affectedFiles, so it cannot be checked against ${others}`,
|
|
153
|
+
file: brief.file,
|
|
154
|
+
line: 1,
|
|
155
|
+
brief: brief.id ?? undefined,
|
|
156
|
+
hint: 'list the files or globs this round writes under "affectedFiles"',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
127
159
|
//# sourceMappingURL=collisions.js.map
|
package/dist/collisions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collisions.js","sourceRoot":"","sources":["../src/collisions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The collision matrix: which briefs scheduled to run side by side declare\n * scopes that can name the same file.\n *\n * Scopes are compared as globs, not as strings. `src/auth/**` and\n * `src/**\\/session.ts` share no prefix a string comparison would see and both\n * cover `src/auth/session.ts`; the intersection finds that, and names the file.\n * A brief that declares no scope cannot be proven apart from anything, so it\n * is reported as unscoped rather than silently counted as safe.\n */\n\nimport type { Brief } from './brief.js';\nimport { lineOfField } from './brief.js';\nimport type { Corpus } from './corpus.js';\nimport { type Glob, globBase, intersectGlobs, parseGlob } from './glob.js';\nimport { severityOf } from './lint.js';\nimport { COLLISION_RULES, type RuleInfo } from './rules.js';\nimport type { Finding, Severity } from './types.js';\n\nexport interface Collision {\n readonly a: Brief;\n readonly b: Brief;\n readonly patterns: readonly [string, string];\n /** A path both scopes cover. */\n readonly witness: string;\n}\n\nexport interface SharedDirectory {\n readonly a: Brief;\n readonly b: Brief;\n readonly directory: string;\n}\n\nexport interface WaveMatrix {\n /** `null` when every live brief is compared regardless of wave. */\n readonly wave: number | null;\n readonly briefs: readonly Brief[];\n readonly collisions: readonly Collision[];\n readonly shared: readonly SharedDirectory[];\n readonly unscoped: readonly Brief[];\n}\n\nexport interface CollisionReport {\n readonly waves: readonly WaveMatrix[];\n /** Live briefs with no wave, which the matrix does not place. */\n readonly unscheduled: readonly Brief[];\n}\n\nexport interface CollisionOptions {\n /** Compare every live brief with every other, whatever its wave. */\n readonly all?: boolean;\n /** Tracked files, which tell a literal file path from a directory. */\n readonly repoFiles?: readonly string[] | null;\n}\n\nfunction scopes(brief: Brief, isFile: ((path: string) => boolean) | undefined): { pattern: string; glob: Glob }[] {\n return brief.affectedFiles.flatMap((pattern) => {\n const parsed = parseGlob(pattern, { isFile });\n return parsed.ok ? [{ pattern, glob: parsed.glob }] : [];\n });\n}\n\nfunction matrix(wave: number | null, briefs: readonly Brief[], isFile: ((path: string) => boolean) | undefined): WaveMatrix {\n const collisions: Collision[] = [];\n const shared: SharedDirectory[] = [];\n const scoped = briefs.map((brief) => ({ brief, scopes: scopes(brief, isFile) }));\n for (let i = 0; i < scoped.length; i += 1) {\n for (let j = i + 1; j < scoped.length; j += 1) {\n const left = scoped[i] as (typeof scoped)[number];\n const right = scoped[j] as (typeof scoped)[number];\n const found: Collision[] = [];\n for (const x of left.scopes) {\n for (const y of right.scopes) {\n const witness = intersectGlobs(x.glob, y.glob);\n if (witness !== null) found.push({ a: left.brief, b: right.brief, patterns: [x.pattern, y.pattern], witness });\n }\n }\n collisions.push(...found);\n if (found.length > 0) continue;\n const leftDirs = new Set(left.scopes.map((s) => globBase(s.glob)).filter((d) => d !== ''));\n const common = [...new Set(right.scopes.map((s) => globBase(s.glob)))].filter((d) => leftDirs.has(d)).sort();\n for (const directory of common) shared.push({ a: left.brief, b: right.brief, directory });\n }\n }\n const unscoped = briefs.length > 1 ? scoped.filter((s) => s.scopes.length === 0).map((s) => s.brief) : [];\n return { wave, briefs, collisions, shared, unscoped };\n}\n\nexport function collisions(corpus: Corpus, options: CollisionOptions = {}): CollisionReport {\n const files = options.repoFiles === undefined || options.repoFiles === null ? null : new Set(options.repoFiles);\n const isFile = files === null ? undefined : (path: string): boolean => files.has(path);\n if (options.all === true) return { waves: [matrix(null, corpus.live, isFile)], unscheduled: [] };\n const byWave = new Map<number, Brief[]>();\n const unscheduled: Brief[] = [];\n for (const brief of corpus.live) {\n if (brief.wave === null) unscheduled.push(brief);\n else byWave.set(brief.wave, [...(byWave.get(brief.wave) ?? []), brief]);\n }\n const waves = [...byWave.keys()].sort((a, b) => a - b).map((wave) => matrix(wave, byWave.get(wave) as Brief[], isFile));\n return { waves, unscheduled };\n}\n\nfunction label(brief: Brief): string {\n return brief.id ?? brief.name;\n}\n\nfunction where(wave: number | null): string {\n return wave === null ? 'among the live briefs' : `in wave ${wave}`;\n}\n\n/**\n * The report as findings. One collision is one finding, placed on the later\n * brief of the pair, which is usually the one still being written.\n */\nexport function collisionFindings(corpus: Corpus, report: CollisionReport): Finding[] {\n const severity = (id: string): Severity | null => {\n const rule = COLLISION_RULES.find((r) => r.id === id) as RuleInfo;\n const setting = severityOf(corpus, id, rule.severity);\n return setting === 'off' ? null : setting;\n };\n const findings: Finding[] = [];\n const collision = severity('collision');\n const unscoped = severity('unscoped');\n const sharedDirectory = severity('shared-directory');\n for (const wave of report.waves) {\n if (collision !== null) {\n for (const c of wave.collisions) {\n findings.push({\n rule: 'collision',\n severity: collision,\n message: `\"${c.patterns[1]}\" overlaps ${label(c.a)}'s \"${c.patterns[0]}\" ${where(wave.wave)}; both cover ${c.witness}`,\n file: c.b.file,\n line: lineOfField(c.b, 'affectedFiles') + 1,\n brief: c.b.id ?? undefined,\n hint: `run them in different waves, make one depend on the other, or narrow a scope`,\n });\n }\n }\n if (sharedDirectory !== null) {\n for (const s of wave.shared) {\n findings.push({\n rule: 'shared-directory',\n severity: sharedDirectory,\n message: `writes into ${s.directory}/, as ${label(s.a)} does ${where(wave.wave)}`,\n file: s.b.file,\n line: lineOfField(s.b, 'affectedFiles') + 1,\n brief: s.b.id ?? undefined,\n });\n }\n }\n if (unscoped !== null) {\n for (const brief of wave.unscoped) {\n findings.push({\n rule: 'unscoped',\n severity: unscoped,\n message: `declares no affectedFiles, so it cannot be checked against the ${wave.briefs.length - 1} other brief(s) ${where(wave.wave)}`,\n file: brief.file,\n line: 1,\n brief: brief.id ?? undefined,\n hint: 'list the files or globs this round writes under \"affectedFiles\"',\n });\n }\n }\n }\n return findings;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"collisions.js","sourceRoot":"","sources":["../src/collisions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The collision matrix: which briefs scheduled to run side by side declare\n * scopes that can name the same file.\n *\n * Scopes are compared as globs, not as strings. `src/auth/**` and\n * `src/**\\/session.ts` share no prefix a string comparison would see and both\n * cover `src/auth/session.ts`; the witness search finds that, and names the\n * file. What a brief protects it will not write, so a file either brief of a\n * pair protects is no collision. A brief that declares no scope cannot be\n * proven apart from anything, so it is reported as unscoped rather than\n * silently counted as safe, and a pair the search could not decide is reported\n * as undecided rather than as either.\n *\n * Two briefs that collide are one defect however many of their patterns\n * meet: they are fixed together, by a wave, a dependency or a narrower scope.\n * So a pair is one collision, carrying every pair of patterns that meets.\n */\n\nimport type { Brief } from './brief.js';\nimport { lineOfField } from './brief.js';\nimport { type Corpus, waitingOnDeferred, type Waiting } from './corpus.js';\nimport { globBases, readingIn, WITNESS_BUDGET } from './glob.js';\nimport { configuredSeverity } from './lint.js';\nimport { meet, type Overlap, type Scope, scopeOf } from './scope.js';\nimport { inWords } from './text.js';\nimport type { Finding, Severity } from './types.js';\n\nexport type { Overlap } from './scope.js';\n\nexport interface Collision {\n readonly a: Brief;\n readonly b: Brief;\n /** Every pair of patterns that meets, `a`'s first; never empty. */\n readonly overlaps: readonly Overlap[];\n}\n\n/** Two briefs no pair of whose patterns is known to meet, and some pair the search could not decide. */\nexport interface UndecidedPair {\n readonly a: Brief;\n readonly b: Brief;\n /** The pairs of patterns, `a`'s first; never empty. */\n readonly patterns: readonly (readonly [string, string])[];\n}\n\nexport interface SharedDirectory {\n readonly a: Brief;\n readonly b: Brief;\n /** The directories both write into, sorted; never empty. */\n readonly directories: readonly string[];\n}\n\nexport interface WaveMatrix {\n /** `null` when every live brief is compared regardless of wave. */\n readonly wave: number | null;\n readonly briefs: readonly Brief[];\n readonly collisions: readonly Collision[];\n readonly undecided: readonly UndecidedPair[];\n readonly shared: readonly SharedDirectory[];\n readonly unscoped: readonly Brief[];\n}\n\nexport interface CollisionReport {\n readonly waves: readonly WaveMatrix[];\n /** Live briefs with no wave, which the matrix does not place. */\n readonly unscheduled: readonly Brief[];\n /** Deferred briefs, which wait for their trigger and run in no wave. */\n readonly deferred: readonly Brief[];\n /**\n * Briefs that depend on a deferred brief, or on one that waits on one. They\n * cannot start before the deferred work, so they run in no wave either, and\n * are compared with nothing - as `schedule` places them nowhere. By id.\n */\n readonly waiting: readonly Waiting[];\n}\n\nexport interface CollisionOptions {\n /** Compare every live brief with every other, whatever its wave. */\n readonly all?: boolean;\n /** The files the tree holds, which say whether a literal path is a file or a directory. */\n readonly repoFiles?: readonly string[] | null;\n /** How many states one witness search may visit before it answers `undecided`. */\n readonly budget?: number;\n}\n\nfunction matrix(wave: number | null, briefs: readonly Brief[], scopes: ReadonlyMap<Brief, Scope>, budget: number): WaveMatrix {\n const collisions: Collision[] = [];\n const undecided: UndecidedPair[] = [];\n const shared: SharedDirectory[] = [];\n const scoped = briefs.map((brief) => scopes.get(brief) as Scope);\n for (let i = 0; i < scoped.length; i += 1) {\n for (let j = i + 1; j < scoped.length; j += 1) {\n const left = scoped[i] as Scope;\n const right = scoped[j] as Scope;\n const meeting = meet(left, right, budget);\n if (meeting.overlaps.length > 0) {\n collisions.push({ a: left.brief, b: right.brief, overlaps: meeting.overlaps });\n continue;\n }\n if (meeting.undecided.length > 0) {\n undecided.push({ a: left.brief, b: right.brief, patterns: meeting.undecided });\n continue;\n }\n const leftDirs = new Set(left.affected.flatMap((s) => globBases(s.glob)).filter((d) => d !== ''));\n const directories = [...new Set(right.affected.flatMap((s) => globBases(s.glob)))].filter((d) => leftDirs.has(d)).sort();\n if (directories.length > 0) shared.push({ a: left.brief, b: right.brief, directories });\n }\n }\n const unscoped = briefs.length > 1 ? scoped.filter((s) => s.affected.length === 0).map((s) => s.brief) : [];\n return { wave, briefs, collisions, undecided, shared, unscoped };\n}\n\nexport function collisions(corpus: Corpus, options: CollisionOptions = {}): CollisionReport {\n const reading = readingIn(options.repoFiles ?? null);\n const budget = options.budget ?? WITNESS_BUDGET;\n const deferred = corpus.live.filter((b) => b.status === 'deferred');\n const waiting = waitingOnDeferred(corpus);\n const held = new Set(waiting.map((w) => w.brief));\n const live = corpus.live.filter((b) => b.status !== 'deferred' && !held.has(b));\n const scopes = new Map(live.map((brief) => [brief, scopeOf(brief, reading)]));\n if (options.all === true) return { waves: [matrix(null, live, scopes, budget)], unscheduled: [], deferred, waiting };\n const byWave = new Map<number, Brief[]>();\n const unscheduled: Brief[] = [];\n for (const brief of live) {\n if (brief.wave === null) unscheduled.push(brief);\n else byWave.set(brief.wave, [...(byWave.get(brief.wave) ?? []), brief]);\n }\n const waves = [...byWave.keys()].sort((a, b) => a - b).map((wave) => matrix(wave, byWave.get(wave) as Brief[], scopes, budget));\n return { waves, unscheduled, deferred, waiting };\n}\n\nfunction label(brief: Brief): string {\n return brief.id ?? brief.name;\n}\n\nfunction where(wave: number | null): string {\n return wave === null ? 'among the live briefs' : `in wave ${wave}`;\n}\n\nfunction collisionMessage(c: Collision, wave: number | null): string {\n if (c.overlaps.length === 1) {\n const only = c.overlaps[0] as Overlap;\n return `\"${only.patterns[1]}\" overlaps ${label(c.a)}'s \"${only.patterns[0]}\" ${where(wave)}; both cover ${only.witness}`;\n }\n const each = c.overlaps.map((o) => `\"${o.patterns[1]}\" and ${label(c.a)}'s \"${o.patterns[0]}\" both cover ${o.witness}`);\n return `overlaps ${label(c.a)} ${where(wave)} through ${c.overlaps.length} pairs of patterns: ${each.join('; ')}`;\n}\n\n/** A pair whose collision the search could not decide, as one finding on the later brief. */\nexport function undecidedFinding(severity: Severity, a: Brief, b: Brief, patterns: readonly (readonly [string, string])[], wave: number | null): Finding {\n const pairs = patterns.map(([x, y]) => `\"${y}\" and ${label(a)}'s \"${x}\"`);\n return {\n rule: 'collision-undecided',\n severity,\n message: `whether it can write a file ${label(a)} writes ${where(wave)} is undecided: the search met its budget for ${inWords(pairs)}`,\n file: b.file,\n line: lineOfField(b, 'affectedFiles') + 1,\n brief: b.id ?? undefined,\n hint: 'narrow one of the patterns, or run the two in different waves',\n };\n}\n\n/**\n * The report as findings. One pair of briefs is one finding, placed on the\n * later brief of the pair, which is usually the one still being written.\n */\nexport function collisionFindings(corpus: Corpus, report: CollisionReport): Finding[] {\n const findings: Finding[] = [];\n const collision = configuredSeverity(corpus, 'collision');\n const undecided = configuredSeverity(corpus, 'collision-undecided');\n const unscoped = configuredSeverity(corpus, 'unscoped');\n const sharedDirectory = configuredSeverity(corpus, 'shared-directory');\n for (const wave of report.waves) {\n if (collision !== null) {\n for (const c of wave.collisions) {\n findings.push({\n rule: 'collision',\n severity: collision,\n message: collisionMessage(c, wave.wave),\n file: c.b.file,\n line: lineOfField(c.b, 'affectedFiles') + 1,\n brief: c.b.id ?? undefined,\n hint: `run them in different waves, make one depend on the other, or narrow a scope`,\n });\n }\n }\n if (undecided !== null) {\n for (const u of wave.undecided) findings.push(undecidedFinding(undecided, u.a, u.b, u.patterns, wave.wave));\n }\n if (sharedDirectory !== null) {\n for (const s of wave.shared) {\n findings.push({\n rule: 'shared-directory',\n severity: sharedDirectory,\n message: `writes into ${inWords(s.directories.map((d) => `${d}/`))}, as ${label(s.a)} does ${where(wave.wave)}`,\n file: s.b.file,\n line: lineOfField(s.b, 'affectedFiles') + 1,\n brief: s.b.id ?? undefined,\n hint: 'check that the two do not depend on one decision in that directory; if they do, order them',\n });\n }\n }\n if (unscoped !== null) {\n for (const brief of wave.unscoped) findings.push(unscopedFinding(unscoped, brief, `the ${wave.briefs.length - 1} other brief(s) ${where(wave.wave)}`));\n }\n }\n return findings;\n}\n\n/** A brief with no scope, which nothing can be proved apart from. */\nexport function unscopedFinding(severity: Severity, brief: Brief, others: string): Finding {\n return {\n rule: 'unscoped',\n severity,\n message: `declares no affectedFiles, so it cannot be checked against ${others}`,\n file: brief.file,\n line: 1,\n brief: brief.id ?? undefined,\n hint: 'list the files or globs this round writes under \"affectedFiles\"',\n };\n}\n"]}
|
package/dist/config.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface SectionRule {
|
|
|
22
22
|
/** The section must hold at least one task item. */
|
|
23
23
|
readonly checklist: boolean;
|
|
24
24
|
readonly optional: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* What the section must answer. A new brief carries it as a comment under
|
|
27
|
+
* the heading, a finding that the section is missing or unwritten carries it
|
|
28
|
+
* as its hint, and `list` and `lint` report it, so a tool that helps write a
|
|
29
|
+
* brief can ask for each section without knowing its name in advance.
|
|
30
|
+
*/
|
|
31
|
+
readonly hint?: string | undefined;
|
|
25
32
|
}
|
|
26
33
|
export interface PluginReference {
|
|
27
34
|
readonly module: string;
|
|
@@ -47,6 +54,8 @@ export interface Config {
|
|
|
47
54
|
readonly field: string | null;
|
|
48
55
|
readonly draft: string | null;
|
|
49
56
|
readonly active: string;
|
|
57
|
+
/** The word for work put off until an event its `trigger` names; `null` when a repository has none. */
|
|
58
|
+
readonly deferred: string | null;
|
|
50
59
|
readonly archived: string;
|
|
51
60
|
};
|
|
52
61
|
readonly sections: readonly SectionRule[];
|
|
@@ -69,6 +78,13 @@ export declare const CONFIG_FILES: readonly string[];
|
|
|
69
78
|
/** The holes a banner line may use. */
|
|
70
79
|
export declare const BANNER_PLACEHOLDERS: readonly string[];
|
|
71
80
|
export declare const SCHEMA_URL = "https://raw.githubusercontent.com/DescentVTT/spec-brief/main/schema.json";
|
|
81
|
+
/**
|
|
82
|
+
* The `$schema` `init` writes, the path the README recommends: the schema of
|
|
83
|
+
* the version installed, which is the one that loads the file. `SCHEMA_URL`
|
|
84
|
+
* follows main, and would flag a key the installed version accepts, or pass
|
|
85
|
+
* one it refuses, as soon as the two differ.
|
|
86
|
+
*/
|
|
87
|
+
export declare const SCHEMA_PATH = "./node_modules/@descent-vtt/spec-brief/schema.json";
|
|
72
88
|
export declare const CONFIG_SCHEMA: Schema;
|
|
73
89
|
export declare const DEFAULT_CONFIG: Config;
|
|
74
90
|
export declare class ConfigError extends Error {
|
|
@@ -81,6 +97,8 @@ export declare class ConfigError extends Error {
|
|
|
81
97
|
* names its own sections means those sections and not the defaults as well.
|
|
82
98
|
*/
|
|
83
99
|
export declare function resolveConfig(raw: unknown, file?: string): Config;
|
|
100
|
+
/** The status words a repository uses, in lifecycle order. */
|
|
101
|
+
export declare function statusWords(config: Config): string[];
|
|
84
102
|
/** Parses the text of a configuration file. */
|
|
85
103
|
export declare function parseConfig(text: string, file: string): Config;
|
|
86
104
|
/**
|
package/dist/config.js
CHANGED
|
@@ -16,7 +16,7 @@ import { dirname, join } from 'node:path';
|
|
|
16
16
|
import { parseGlob } from './glob.js';
|
|
17
17
|
import { normalisePath } from './links.js';
|
|
18
18
|
import { toJsonSchema, validate } from './schema.js';
|
|
19
|
-
import { templateHoles } from './text.js';
|
|
19
|
+
import { inWords, templateHoles } from './text.js';
|
|
20
20
|
export const CONFIG_FILES = ['.spec-brief.json', 'spec-brief.json'];
|
|
21
21
|
/** The holes a banner line may use. */
|
|
22
22
|
export const BANNER_PLACEHOLDERS = [
|
|
@@ -31,6 +31,13 @@ export const BANNER_PLACEHOLDERS = [
|
|
|
31
31
|
'author',
|
|
32
32
|
];
|
|
33
33
|
export const SCHEMA_URL = 'https://raw.githubusercontent.com/DescentVTT/spec-brief/main/schema.json';
|
|
34
|
+
/**
|
|
35
|
+
* The `$schema` `init` writes, the path the README recommends: the schema of
|
|
36
|
+
* the version installed, which is the one that loads the file. `SCHEMA_URL`
|
|
37
|
+
* follows main, and would flag a key the installed version accepts, or pass
|
|
38
|
+
* one it refuses, as soon as the two differ.
|
|
39
|
+
*/
|
|
40
|
+
export const SCHEMA_PATH = './node_modules/@descent-vtt/spec-brief/schema.json';
|
|
34
41
|
const SEVERITY = { type: 'string', enum: ['off', 'note', 'warning', 'error'] };
|
|
35
42
|
const STRINGS = { type: 'array', items: { type: 'string', minLength: 1 } };
|
|
36
43
|
const SECTION = {
|
|
@@ -47,6 +54,11 @@ const SECTION = {
|
|
|
47
54
|
mustContain: { ...STRINGS, description: 'Literal text the section must contain.' },
|
|
48
55
|
checklist: { type: 'boolean', description: 'The section must hold at least one "- [ ]" task item.' },
|
|
49
56
|
optional: { type: 'boolean', description: 'Checked when present, not required.' },
|
|
57
|
+
hint: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
minLength: 1,
|
|
60
|
+
description: 'What the section must answer: a comment under the heading in a new brief, and the hint of a finding that it is missing or unwritten.',
|
|
61
|
+
},
|
|
50
62
|
},
|
|
51
63
|
},
|
|
52
64
|
],
|
|
@@ -74,6 +86,11 @@ export const CONFIG_SCHEMA = {
|
|
|
74
86
|
field: { type: 'string', nullable: true, description: 'Front-matter key holding the status; null uses location only.' },
|
|
75
87
|
draft: { type: 'string', nullable: true, description: 'The word for a brief still being written, or null.' },
|
|
76
88
|
active: { type: 'string', minLength: 1, description: 'The word for a live brief.' },
|
|
89
|
+
deferred: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
nullable: true,
|
|
92
|
+
description: 'The word for a live brief put off until the event its "trigger" names, or null. A deferred brief is never ready and runs in no wave.',
|
|
93
|
+
},
|
|
77
94
|
archived: { type: 'string', minLength: 1, description: 'The word for an archived brief.' },
|
|
78
95
|
},
|
|
79
96
|
},
|
|
@@ -133,19 +150,36 @@ export const DEFAULT_CONFIG = {
|
|
|
133
150
|
exclude: [],
|
|
134
151
|
template: null,
|
|
135
152
|
id: { source: 'filename', separator: '_', digits: 3 },
|
|
136
|
-
status: { field: 'status', draft: 'draft', active: 'active', archived: 'archived' },
|
|
153
|
+
status: { field: 'status', draft: 'draft', active: 'active', deferred: 'deferred', archived: 'archived' },
|
|
137
154
|
sections: [
|
|
138
|
-
section('Intent', {
|
|
155
|
+
section('Intent', {
|
|
156
|
+
aliases: ["Commander's Intent", 'Mission', 'Objective'],
|
|
157
|
+
hint: 'The state of the tree when this round is done, and why it matters. One paragraph.',
|
|
158
|
+
}),
|
|
139
159
|
section('Negative Scope', {
|
|
140
160
|
aliases: ['Out of Scope', 'Non-Goals', 'Not in Scope', 'What this round is NOT', 'What this brief does NOT do'],
|
|
161
|
+
hint: 'What this round must not do, even where it would look helpful.',
|
|
162
|
+
}),
|
|
163
|
+
section('Not Empowered', {
|
|
164
|
+
aliases: ['Non-Empowerment', 'Non-Empowerment List'],
|
|
165
|
+
optional: true,
|
|
166
|
+
hint: 'Files, interfaces and decisions this round may not change.',
|
|
167
|
+
}),
|
|
168
|
+
section('Invariants', {
|
|
169
|
+
aliases: ['Invariant Checklist', 'Definition of Done'],
|
|
170
|
+
checklist: true,
|
|
171
|
+
hint: 'Checks that must hold before the round is called done, each one a task item.',
|
|
141
172
|
}),
|
|
142
|
-
section('Not Empowered', { aliases: ['Non-Empowerment', 'Non-Empowerment List'], optional: true }),
|
|
143
|
-
section('Invariants', { aliases: ['Invariant Checklist', 'Definition of Done'], checklist: true }),
|
|
144
173
|
],
|
|
145
174
|
sectionOrder: false,
|
|
146
175
|
types: {
|
|
147
|
-
feature: [section('Acceptance Criteria', { checklist: true })],
|
|
148
|
-
defect: [
|
|
176
|
+
feature: [section('Acceptance Criteria', { checklist: true, hint: 'What a reviewer checks to accept the result, each one a task item.' })],
|
|
177
|
+
defect: [
|
|
178
|
+
section('The Defect, Measured', {
|
|
179
|
+
aliases: ['Reproduction'],
|
|
180
|
+
hint: 'How to reproduce the defect, and the measurement that shows it.',
|
|
181
|
+
}),
|
|
182
|
+
],
|
|
149
183
|
refactor: [],
|
|
150
184
|
chore: [],
|
|
151
185
|
},
|
|
@@ -178,6 +212,7 @@ function sectionFrom(raw) {
|
|
|
178
212
|
mustContain: r['mustContain'] ?? [],
|
|
179
213
|
checklist: r['checklist'] ?? false,
|
|
180
214
|
optional: r['optional'] ?? false,
|
|
215
|
+
hint: r['hint'],
|
|
181
216
|
});
|
|
182
217
|
}
|
|
183
218
|
function pluginFrom(raw) {
|
|
@@ -234,6 +269,11 @@ export function resolveConfig(raw, file = 'configuration') {
|
|
|
234
269
|
throw new ConfigError(file, semantic);
|
|
235
270
|
return config;
|
|
236
271
|
}
|
|
272
|
+
/** The status words a repository uses, in lifecycle order. */
|
|
273
|
+
export function statusWords(config) {
|
|
274
|
+
const { draft, active, deferred, archived } = config.status;
|
|
275
|
+
return [draft, active, deferred, archived].filter((w) => w !== null);
|
|
276
|
+
}
|
|
237
277
|
/** What the schema cannot say: relationships between values. */
|
|
238
278
|
function checkConfig(config) {
|
|
239
279
|
const problems = [];
|
|
@@ -250,9 +290,22 @@ function checkConfig(config) {
|
|
|
250
290
|
const archive = inside('archive', config.archive);
|
|
251
291
|
if (briefs !== null && briefs === archive)
|
|
252
292
|
problems.push('"briefs" and "archive" must be different directories');
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
293
|
+
// Named by the statuses that share a word: a 0.1 configuration that spelt
|
|
294
|
+
// another status "deferred" meets the default for the new one here.
|
|
295
|
+
const sharing = new Map();
|
|
296
|
+
for (const key of ['draft', 'active', 'deferred', 'archived']) {
|
|
297
|
+
const word = config.status[key];
|
|
298
|
+
if (word === null)
|
|
299
|
+
continue;
|
|
300
|
+
const group = sharing.get(word.toLowerCase()) ?? { word, keys: [] };
|
|
301
|
+
group.keys.push(key);
|
|
302
|
+
sharing.set(word.toLowerCase(), group);
|
|
303
|
+
}
|
|
304
|
+
for (const { word, keys } of sharing.values()) {
|
|
305
|
+
if (keys.length < 2)
|
|
306
|
+
continue;
|
|
307
|
+
const orNull = keys.includes('deferred') ? ', or set "status.deferred" to null where no work is deferred' : '';
|
|
308
|
+
problems.push(`the status words must differ: "${word}" is the word for ${inWords(keys)}; give ${keys.length === 2 ? 'one of them' : 'all but one'} another word${orNull}`);
|
|
256
309
|
}
|
|
257
310
|
// A pattern that does not parse would match nothing, and a run over no briefs reads as clean.
|
|
258
311
|
for (const [key, pattern] of [['files', config.files], ...config.exclude.map((e) => ['exclude', e])]) {
|
|
@@ -331,10 +384,12 @@ export function initialConfig(briefs, archive) {
|
|
|
331
384
|
out['checklist'] = true;
|
|
332
385
|
if (s.optional)
|
|
333
386
|
out['optional'] = true;
|
|
387
|
+
if (s.hint !== undefined)
|
|
388
|
+
out['hint'] = s.hint;
|
|
334
389
|
return Object.keys(out).length === 1 ? s.name : out;
|
|
335
390
|
};
|
|
336
391
|
return {
|
|
337
|
-
$schema:
|
|
392
|
+
$schema: SCHEMA_PATH,
|
|
338
393
|
briefs,
|
|
339
394
|
archive,
|
|
340
395
|
files: d.files,
|