@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/engine.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine: one repository's briefs, with the operations the CLI offers and
|
|
3
|
+
* the same ones for a harness or an orchestrator that would rather call a
|
|
4
|
+
* function than parse a terminal.
|
|
5
|
+
*
|
|
6
|
+
* This module and the CLI are where the pieces meet their implementations -
|
|
7
|
+
* the real filesystem, the real git, the plugins a configuration names.
|
|
8
|
+
* Everything they compose is a pure function that takes those as arguments,
|
|
9
|
+
* so an engine can equally run over a `MemoryFileSystem` and no git at all.
|
|
10
|
+
*/
|
|
11
|
+
import { access } from 'node:fs/promises';
|
|
12
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import { applyPlan } from './apply.js';
|
|
14
|
+
import { planArchive, planUnarchive } from './archive.js';
|
|
15
|
+
import { collisionFindings, collisions } from './collisions.js';
|
|
16
|
+
import { DEFAULT_CONFIG, locateConfig, parseConfig } from './config.js';
|
|
17
|
+
import { buildCorpus, findBriefs, isReady, pendingDependencies } from './corpus.js';
|
|
18
|
+
import { canonicalPath, NodeFileSystem } from './fs.js';
|
|
19
|
+
import { NodeGit, pullRequestUrl } from './git.js';
|
|
20
|
+
import { matchGlob, parseGlob } from './glob.js';
|
|
21
|
+
import { normalisePath } from './links.js';
|
|
22
|
+
import { lint } from './lint.js';
|
|
23
|
+
import { loadPlugins } from './plugins.js';
|
|
24
|
+
import { fileNameFor, nextId, renderNewBrief } from './scaffold.js';
|
|
25
|
+
export class EngineError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'EngineError';
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function exists(path) {
|
|
34
|
+
try {
|
|
35
|
+
await access(path);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Today, in UTC. `SOURCE_DATE_EPOCH` wins where it is set, which is what makes
|
|
44
|
+
* a banner written in a reproducible pipeline reproducible.
|
|
45
|
+
*/
|
|
46
|
+
export function today(env = process.env) {
|
|
47
|
+
const epoch = env['SOURCE_DATE_EPOCH'];
|
|
48
|
+
const when = epoch !== undefined && /^\d+$/.test(epoch) ? new Date(Number(epoch) * 1000) : new Date();
|
|
49
|
+
return when.toISOString().slice(0, 10);
|
|
50
|
+
}
|
|
51
|
+
export function isDate(text) {
|
|
52
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(text))
|
|
53
|
+
return false;
|
|
54
|
+
const parsed = new Date(`${text}T00:00:00Z`);
|
|
55
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === text;
|
|
56
|
+
}
|
|
57
|
+
export class BriefEngine {
|
|
58
|
+
root;
|
|
59
|
+
config;
|
|
60
|
+
configFile;
|
|
61
|
+
fs;
|
|
62
|
+
git;
|
|
63
|
+
plugins;
|
|
64
|
+
loaded;
|
|
65
|
+
missingBriefs = false;
|
|
66
|
+
constructor(setup) {
|
|
67
|
+
this.root = setup.root;
|
|
68
|
+
this.config = setup.config;
|
|
69
|
+
this.configFile = setup.configFile;
|
|
70
|
+
this.fs = setup.fs;
|
|
71
|
+
this.git = setup.git;
|
|
72
|
+
this.plugins = setup.plugins;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Finds the configuration, the root it defines and the repository around it,
|
|
76
|
+
* and reads every brief. The root is the directory holding the
|
|
77
|
+
* configuration; without one it is the working tree's top, or `cwd`.
|
|
78
|
+
*/
|
|
79
|
+
static async open(options = {}) {
|
|
80
|
+
// Named as git names them, so the root and the work tree compare.
|
|
81
|
+
const cwd = await canonicalPath(resolve(options.cwd ?? process.cwd()));
|
|
82
|
+
const explicit = options.config === undefined ? undefined : await canonicalPath(resolve(cwd, options.config));
|
|
83
|
+
const configPath = options.noConfig === true ? undefined : (explicit ?? (await locateConfig(cwd, exists)));
|
|
84
|
+
// Asking git costs a process; without git there is nothing to ask.
|
|
85
|
+
const toplevel = options.git === null ? null : await NodeGit.toplevel(cwd);
|
|
86
|
+
const root = configPath === undefined ? (toplevel ?? cwd) : dirname(configPath);
|
|
87
|
+
let config = DEFAULT_CONFIG;
|
|
88
|
+
if (configPath !== undefined) {
|
|
89
|
+
const fs = new NodeFileSystem(dirname(configPath));
|
|
90
|
+
const name = configPath.slice(dirname(configPath).length + 1);
|
|
91
|
+
const text = await fs.read(name);
|
|
92
|
+
if (text === null)
|
|
93
|
+
throw new EngineError('not-found', `${configPath} does not exist`);
|
|
94
|
+
config = parseConfig(text, relative(cwd, configPath));
|
|
95
|
+
}
|
|
96
|
+
const inTree = toplevel !== null && !relative(toplevel, root).startsWith('..');
|
|
97
|
+
const engine = new BriefEngine({
|
|
98
|
+
root,
|
|
99
|
+
config,
|
|
100
|
+
configFile: configPath === undefined ? null : relative(root, configPath).split(sep).join('/'),
|
|
101
|
+
fs: options.fs ?? new NodeFileSystem(root),
|
|
102
|
+
git: options.git === undefined ? (inTree ? new NodeGit(root) : null) : options.git,
|
|
103
|
+
});
|
|
104
|
+
await engine.load();
|
|
105
|
+
return engine;
|
|
106
|
+
}
|
|
107
|
+
get corpus() {
|
|
108
|
+
if (this.loaded === undefined)
|
|
109
|
+
throw new Error('the engine has not loaded its briefs; call load() first');
|
|
110
|
+
return this.loaded;
|
|
111
|
+
}
|
|
112
|
+
/** Reads every brief again, as after an archival. */
|
|
113
|
+
async load() {
|
|
114
|
+
const files = [];
|
|
115
|
+
const accept = parseGlob(this.config.files);
|
|
116
|
+
const reject = this.config.exclude.map((pattern) => parseGlob(pattern));
|
|
117
|
+
const isBrief = (name) => accept.ok && matchGlob(accept.glob, name) && !reject.some((r) => r.ok && matchGlob(r.glob, name));
|
|
118
|
+
const read = async (directory, phase) => {
|
|
119
|
+
const dir = normalisePath(directory);
|
|
120
|
+
const names = await this.fs.list(dir);
|
|
121
|
+
for (const name of names ?? []) {
|
|
122
|
+
if (!isBrief(name))
|
|
123
|
+
continue;
|
|
124
|
+
const path = dir === '' ? name : `${dir}/${name}`;
|
|
125
|
+
const text = await this.fs.read(path);
|
|
126
|
+
// A file listed a moment ago and gone now was removed under the run; there is nothing to read.
|
|
127
|
+
/* v8 ignore next */
|
|
128
|
+
if (text !== null)
|
|
129
|
+
files.push({ path, text, phase });
|
|
130
|
+
}
|
|
131
|
+
return names !== null;
|
|
132
|
+
};
|
|
133
|
+
this.missingBriefs = !(await read(this.config.briefs, 'live'));
|
|
134
|
+
await read(this.config.archive, 'archived');
|
|
135
|
+
this.loaded = buildCorpus(files, this.config);
|
|
136
|
+
return this.loaded;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Refuses to report on a briefs directory that does not exist. A check over
|
|
140
|
+
* nothing looks exactly like a clean one, and the likelier cause is a
|
|
141
|
+
* configuration pointing at the wrong place.
|
|
142
|
+
*/
|
|
143
|
+
requireBriefs() {
|
|
144
|
+
if (this.missingBriefs) {
|
|
145
|
+
throw new EngineError('not-found', `${normalisePath(this.config.briefs)}/ does not exist; run "spec-brief init", or set "briefs" in the configuration`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The plugins the configuration names, loaded once. */
|
|
149
|
+
async loadedPlugins() {
|
|
150
|
+
this.plugins ??= await loadPlugins(this.config.plugins, this.root);
|
|
151
|
+
return this.plugins;
|
|
152
|
+
}
|
|
153
|
+
/** The one brief a reference names: an id, a file name or a path. */
|
|
154
|
+
find(reference) {
|
|
155
|
+
const found = findBriefs(this.corpus, reference);
|
|
156
|
+
if (found.length === 0)
|
|
157
|
+
throw new EngineError('not-found', `no brief is named "${reference}"`);
|
|
158
|
+
if (found.length > 1) {
|
|
159
|
+
throw new EngineError('ambiguous', `"${reference}" names ${found.length} briefs: ${found.map((b) => b.file).join(', ')}`);
|
|
160
|
+
}
|
|
161
|
+
return found[0];
|
|
162
|
+
}
|
|
163
|
+
/** The files git sees, or every file when there is no git; `null` when neither can be read. */
|
|
164
|
+
async repoFiles() {
|
|
165
|
+
try {
|
|
166
|
+
return this.git === null ? await this.fs.walk() : await this.git.files();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async lint(references = []) {
|
|
173
|
+
const only = references.length === 0 ? undefined : references.map((r) => this.find(r));
|
|
174
|
+
return lint(this.corpus, { plugins: await this.loadedPlugins(), repoFiles: await this.repoFiles(), ...(only ? { only } : {}) });
|
|
175
|
+
}
|
|
176
|
+
async collisions(options = {}) {
|
|
177
|
+
const report = collisions(this.corpus, { repoFiles: await this.repoFiles(), ...options });
|
|
178
|
+
return { report, findings: collisionFindings(this.corpus, report) };
|
|
179
|
+
}
|
|
180
|
+
/** Live briefs that can run now: not drafts, every dependency archived. */
|
|
181
|
+
ready() {
|
|
182
|
+
return this.corpus.live.filter((b) => isReady(this.corpus, b));
|
|
183
|
+
}
|
|
184
|
+
/** The live dependencies holding a brief back. */
|
|
185
|
+
waitingOn(brief) {
|
|
186
|
+
return pendingDependencies(this.corpus, brief);
|
|
187
|
+
}
|
|
188
|
+
async planArchive(reference, options = {}) {
|
|
189
|
+
const brief = this.find(reference);
|
|
190
|
+
const date = options.date ?? today();
|
|
191
|
+
if (!isDate(date))
|
|
192
|
+
throw new EngineError('invalid', `"${date}" is not a date written YYYY-MM-DD`);
|
|
193
|
+
if (options.noGit === true && (options.commit !== undefined || options.base !== undefined)) {
|
|
194
|
+
throw new EngineError('invalid', 'a commit or a base needs git; drop --no-git, or drop --commit and --base');
|
|
195
|
+
}
|
|
196
|
+
const git = options.noGit === true ? null : this.git;
|
|
197
|
+
const base = options.base ?? this.config.archiving.base ?? undefined;
|
|
198
|
+
const findings = await this.lint();
|
|
199
|
+
let commit;
|
|
200
|
+
let changes;
|
|
201
|
+
let dirty;
|
|
202
|
+
let pr;
|
|
203
|
+
if (git !== null) {
|
|
204
|
+
const revision = options.commit ?? (base === undefined ? undefined : 'HEAD');
|
|
205
|
+
if (revision !== undefined) {
|
|
206
|
+
commit = (await git.commit(revision)) ?? undefined;
|
|
207
|
+
if (commit === undefined)
|
|
208
|
+
throw new EngineError('not-found', `"${revision}" names no commit`);
|
|
209
|
+
const from = base === undefined ? null : ((await git.mergeBase(base, commit.sha)) ?? base);
|
|
210
|
+
changes = await git.changes(from, commit.sha);
|
|
211
|
+
}
|
|
212
|
+
dirty = await git.dirty();
|
|
213
|
+
}
|
|
214
|
+
if (options.pr !== undefined) {
|
|
215
|
+
pr = { number: options.pr, url: git === null ? null : pullRequestUrl(await git.remoteUrl('origin'), options.pr) };
|
|
216
|
+
}
|
|
217
|
+
return planArchive(this.corpus, brief, {
|
|
218
|
+
date,
|
|
219
|
+
summary: options.summary,
|
|
220
|
+
pr,
|
|
221
|
+
commit,
|
|
222
|
+
changes,
|
|
223
|
+
dirty,
|
|
224
|
+
allowDirty: options.allowDirty,
|
|
225
|
+
strict: options.strict,
|
|
226
|
+
findings,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
planUnarchive(reference) {
|
|
230
|
+
return planUnarchive(this.corpus, this.find(reference));
|
|
231
|
+
}
|
|
232
|
+
/** Executes a plan and reads the briefs again. A refused plan is never applied. */
|
|
233
|
+
async apply(plan) {
|
|
234
|
+
if (plan.blocking.length > 0) {
|
|
235
|
+
throw new EngineError('refused', `${plan.action} of ${plan.brief.file} is refused: ${plan.blocking[0].message}`);
|
|
236
|
+
}
|
|
237
|
+
if (plan.done)
|
|
238
|
+
return;
|
|
239
|
+
await applyPlan(this.fs, plan.ops);
|
|
240
|
+
await this.load();
|
|
241
|
+
}
|
|
242
|
+
/** Scaffolds a brief and writes it; the file is new, or nothing is written. */
|
|
243
|
+
async create(options) {
|
|
244
|
+
const title = options.title.trim();
|
|
245
|
+
if (title === '')
|
|
246
|
+
throw new EngineError('invalid', 'a brief needs a title');
|
|
247
|
+
const id = options.id ?? nextId(this.corpus);
|
|
248
|
+
if (id === null)
|
|
249
|
+
throw new EngineError('invalid', 'the ids here are not numbers, so there is no next one; pass --id');
|
|
250
|
+
if (/[\s/\\]/.test(id) || id.includes(this.config.id.separator)) {
|
|
251
|
+
throw new EngineError('invalid', `"${id}" cannot be an id: no whitespace, slashes or "${this.config.id.separator}"`);
|
|
252
|
+
}
|
|
253
|
+
if (findBriefs(this.corpus, id).some((b) => b.id !== null))
|
|
254
|
+
throw new EngineError('exists', `the id "${id}" is taken`);
|
|
255
|
+
if (options.type !== undefined && this.config.types[options.type] === undefined) {
|
|
256
|
+
const names = Object.keys(this.config.types);
|
|
257
|
+
throw new EngineError('invalid', `"${options.type}" is not a brief type here; use one of ${names.join(', ') || '(none)'}`);
|
|
258
|
+
}
|
|
259
|
+
const date = options.date ?? today();
|
|
260
|
+
if (!isDate(date))
|
|
261
|
+
throw new EngineError('invalid', `"${date}" is not a date written YYYY-MM-DD`);
|
|
262
|
+
const template = this.config.template === null ? null : await this.fs.read(this.config.template);
|
|
263
|
+
if (this.config.template !== null && template === null) {
|
|
264
|
+
throw new EngineError('not-found', `the template ${this.config.template} does not exist`);
|
|
265
|
+
}
|
|
266
|
+
const content = renderNewBrief(this.config, { id, title, date, type: options.type, wave: options.wave, dependsOn: options.dependsOn }, template);
|
|
267
|
+
const dir = normalisePath(this.config.briefs);
|
|
268
|
+
const file = `${dir}/${fileNameFor(this.config, id, title)}`;
|
|
269
|
+
if (await this.fs.exists(file))
|
|
270
|
+
throw new EngineError('exists', `${file} already exists`);
|
|
271
|
+
await this.fs.write(file, content);
|
|
272
|
+
await this.load();
|
|
273
|
+
return { file, content };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,WAAW,EAAa,aAAa,EAAoB,MAAM,cAAc,CAAC;AAEvF,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAA+C,MAAM,iBAAiB,CAAC;AAC7G,OAAO,EAAe,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,WAAW,EAAe,UAAU,EAAE,OAAO,EAAE,mBAAmB,EAAmB,MAAM,aAAa,CAAC;AAClH,OAAO,EAAE,aAAa,EAAmB,cAAc,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,EAA8C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/F,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAe,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpE,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,CAA+D;IAE5E,YAAY,IAAyB,EAAE,OAAe;QACpD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAgDD,KAAK,UAAU,MAAM,CAAC,IAAY;IAChC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,GAAG,GAAiD,OAAO,CAAC,GAAG;IACnF,MAAM,KAAK,GAAG,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IACtG,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACpD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI,YAAY,CAAC,CAAC;IAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC;AACvF,CAAC;AAED,MAAM,OAAO,WAAW;IACb,IAAI,CAAS;IACb,MAAM,CAAS;IACf,UAAU,CAAgB;IAC1B,EAAE,CAAa;IACf,GAAG,CAAa;IACjB,OAAO,CAAgC;IACvC,MAAM,CAAqB;IAC3B,aAAa,GAAG,KAAK,CAAC;IAE9B,YAAY,KAAkB;QAC5B,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;QACnC,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;QACnB,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC/B,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAgB,EAAE;QACzC,kEAAkE;QAClE,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACvE,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9G,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3G,mEAAmE;QACnE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAChF,IAAI,MAAM,GAAG,cAAc,CAAC;QAC5B,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,EAAE,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YACnD,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC9D,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,IAAI,KAAK,IAAI;gBAAE,MAAM,IAAI,WAAW,CAAC,WAAW,EAAE,GAAG,UAAU,iBAAiB,CAAC,CAAC;YACtF,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC;QACxD,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC/E,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC;YAC7B,IAAI;YACJ,MAAM;YACN,UAAU,EAAE,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YAC7F,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,IAAI,cAAc,CAAC,IAAI,CAAC;YAC1C,GAAG,EAAE,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG;SACnF,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,MAAM;QACR,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC1G,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,qDAAqD;IACrD,KAAK,CAAC,IAAI;QACR,MAAM,KAAK,GAAiB,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CACxC,MAAM,CAAC,EAAE,IAAI,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACpG,MAAM,IAAI,GAAG,KAAK,EAAE,SAAiB,EAAE,KAAY,EAAoB,EAAE;YACvE,MAAM,GAAG,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;YACrC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACtC,KAAK,MAAM,IAAI,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;gBAC/B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;oBAAE,SAAS;gBAC7B,MAAM,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;gBAClD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACtC,+FAA+F;gBAC/F,oBAAoB;gBACpB,IAAI,IAAI,KAAK,IAAI;oBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACvD,CAAC;YACD,OAAO,KAAK,KAAK,IAAI,CAAC;QACxB,CAAC,CAAC;QACF,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAC/D,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;OAIG;IACH,aAAa;QACX,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,MAAM,IAAI,WAAW,CAAC,WAAW,EAAE,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,+EAA+E,CAAC,CAAC;QAC1J,CAAC;IACH,CAAC;IAED,wDAAwD;IACxD,KAAK,CAAC,aAAa;QACjB,IAAI,CAAC,OAAO,KAAK,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,qEAAqE;IACrE,IAAI,CAAC,SAAiB;QACpB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QACjD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,WAAW,EAAE,sBAAsB,SAAS,GAAG,CAAC,CAAC;QAC/F,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,WAAW,CAAC,WAAW,EAAE,IAAI,SAAS,WAAW,KAAK,CAAC,MAAM,YAAY,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5H,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,CAAU,CAAC;IAC3B,CAAC;IAED,+FAA+F;IAC/F,KAAK,CAAC,SAAS;QACb,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QAC3E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAU,GAAsB,EAAE;QAC3C,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACvF,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAClI,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAO,GAAqB,EAAE;QAC7C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;QAC1F,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IACtE,CAAC;IAED,2EAA2E;IAC3E,KAAK;QACH,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,kDAAkD;IAClD,SAAS,CAAC,KAAY;QACpB,OAAO,mBAAmB,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,SAAiB,EAAE,OAAO,GAAmB,EAAE;QAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC;QACrC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,IAAI,IAAI,oCAAoC,CAAC,CAAC;QAClG,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,EAAE,CAAC;YAC3F,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,0EAA0E,CAAC,CAAC;QAC/G,CAAC;QACD,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACrD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC;QACrE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAEnC,IAAI,MAA8B,CAAC;QACnC,IAAI,OAAiC,CAAC;QACtC,IAAI,KAA2B,CAAC;QAChC,IAAI,EAA2B,CAAC;QAChC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC7E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,SAAS,CAAC;gBACnD,IAAI,MAAM,KAAK,SAAS;oBAAE,MAAM,IAAI,WAAW,CAAC,WAAW,EAAE,IAAI,QAAQ,mBAAmB,CAAC,CAAC;gBAC9F,MAAM,IAAI,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;gBAC3F,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;YAChD,CAAC;YACD,KAAK,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QAC5B,CAAC;QACD,IAAI,OAAO,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YAC7B,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;QACpH,CAAC;QACD,OAAO,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE;YACrC,IAAI;YACJ,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,EAAE;YACF,MAAM;YACN,OAAO;YACP,KAAK;YACL,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ;SACT,CAAC,CAAC;IACL,CAAC;IAED,aAAa,CAAC,SAAiB;QAC7B,OAAO,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,mFAAmF;IACnF,KAAK,CAAC,KAAK,CAAC,IAAU;QACpB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,gBAAiB,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAChI,CAAC;QACD,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO;QACtB,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,+EAA+E;IAC/E,KAAK,CAAC,MAAM,CAAC,OAAmB;QAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,KAAK,KAAK,EAAE;YAAE,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;QAC5E,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,EAAE,KAAK,IAAI;YAAE,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,kEAAkE,CAAC,CAAC;QACtH,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;YAChE,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,IAAI,EAAE,iDAAiD,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC;QACvH,CAAC;QACD,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;QACvH,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YAChF,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC7C,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,IAAI,OAAO,CAAC,IAAI,0CAA0C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC;QAC7H,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC;QACrC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,IAAI,IAAI,oCAAoC,CAAC,CAAC;QAClG,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACjG,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACvD,MAAM,IAAI,WAAW,CAAC,WAAW,EAAE,gBAAgB,IAAI,CAAC,MAAM,CAAC,QAAQ,iBAAiB,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,OAAO,GAAG,cAAc,CAC5B,IAAI,CAAC,MAAM,EACX,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,EACzF,QAAQ,CACT,CAAC;QACF,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9C,MAAM,IAAI,GAAG,GAAG,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC;QAC7D,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,QAAQ,EAAE,GAAG,IAAI,iBAAiB,CAAC,CAAC;QAC1F,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC3B,CAAC;CACF","sourcesContent":["/**\n * The engine: one repository's briefs, with the operations the CLI offers and\n * the same ones for a harness or an orchestrator that would rather call a\n * function than parse a terminal.\n *\n * This module and the CLI are where the pieces meet their implementations -\n * the real filesystem, the real git, the plugins a configuration names.\n * Everything they compose is a pure function that takes those as arguments,\n * so an engine can equally run over a `MemoryFileSystem` and no git at all.\n */\n\nimport { access } from 'node:fs/promises';\nimport { dirname, relative, resolve, sep } from 'node:path';\n\nimport { applyPlan } from './apply.js';\nimport { planArchive, type Plan, planUnarchive, type PullRequest } from './archive.js';\nimport type { Brief } from './brief.js';\nimport { collisionFindings, collisions, type CollisionOptions, type CollisionReport } from './collisions.js';\nimport { type Config, DEFAULT_CONFIG, locateConfig, parseConfig } from './config.js';\nimport { buildCorpus, type Corpus, findBriefs, isReady, pendingDependencies, type SourceFile } from './corpus.js';\nimport { canonicalPath, type FileSystem, NodeFileSystem } from './fs.js';\nimport { type CommitInfo, type FileChange, type Git, NodeGit, pullRequestUrl } from './git.js';\nimport { matchGlob, parseGlob } from './glob.js';\nimport { normalisePath } from './links.js';\nimport { lint, type Plugin } from './lint.js';\nimport { loadPlugins } from './plugins.js';\nimport { fileNameFor, nextId, renderNewBrief } from './scaffold.js';\nimport type { Finding, Phase } from './types.js';\n\nexport class EngineError extends Error {\n readonly code: 'not-found' | 'ambiguous' | 'invalid' | 'refused' | 'exists';\n\n constructor(code: EngineError['code'], message: string) {\n super(message);\n this.name = 'EngineError';\n this.code = code;\n }\n}\n\nexport interface EngineSetup {\n /** Absolute root: the directory holding the configuration. */\n readonly root: string;\n readonly config: Config;\n /** Repository-relative configuration path, or `null` when the defaults apply. */\n readonly configFile: string | null;\n readonly fs: FileSystem;\n readonly git: Git | null;\n readonly plugins?: readonly Plugin[];\n}\n\nexport interface OpenOptions {\n /** Where discovery starts. Defaults to the process's directory. */\n readonly cwd?: string;\n /** An explicit configuration file; discovery is skipped. */\n readonly config?: string;\n /** Use the defaults even where a configuration file exists. */\n readonly noConfig?: boolean;\n /** `null` runs without git. Defaults to git when the root is inside a working tree. */\n readonly git?: Git | null;\n readonly fs?: FileSystem;\n}\n\nexport interface ArchiveOptions {\n readonly pr?: number | undefined;\n /** The commit the round landed as. */\n readonly commit?: string | undefined;\n /** The branch the round started from; the diff runs from its merge base. */\n readonly base?: string | undefined;\n readonly summary?: string | undefined;\n readonly date?: string | undefined;\n readonly allowDirty?: boolean | undefined;\n readonly strict?: boolean | undefined;\n /** Leave git out, even where there is a repository. */\n readonly noGit?: boolean | undefined;\n}\n\nexport interface NewOptions {\n readonly title: string;\n readonly id?: string | undefined;\n readonly type?: string | undefined;\n readonly wave?: number | undefined;\n readonly dependsOn?: readonly string[] | undefined;\n readonly date?: string | undefined;\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Today, in UTC. `SOURCE_DATE_EPOCH` wins where it is set, which is what makes\n * a banner written in a reproducible pipeline reproducible.\n */\nexport function today(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const epoch = env['SOURCE_DATE_EPOCH'];\n const when = epoch !== undefined && /^\\d+$/.test(epoch) ? new Date(Number(epoch) * 1000) : new Date();\n return when.toISOString().slice(0, 10);\n}\n\nexport function isDate(text: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(text)) return false;\n const parsed = new Date(`${text}T00:00:00Z`);\n return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === text;\n}\n\nexport class BriefEngine {\n readonly root: string;\n readonly config: Config;\n readonly configFile: string | null;\n readonly fs: FileSystem;\n readonly git: Git | null;\n private plugins: readonly Plugin[] | undefined;\n private loaded: Corpus | undefined;\n private missingBriefs = false;\n\n constructor(setup: EngineSetup) {\n this.root = setup.root;\n this.config = setup.config;\n this.configFile = setup.configFile;\n this.fs = setup.fs;\n this.git = setup.git;\n this.plugins = setup.plugins;\n }\n\n /**\n * Finds the configuration, the root it defines and the repository around it,\n * and reads every brief. The root is the directory holding the\n * configuration; without one it is the working tree's top, or `cwd`.\n */\n static async open(options: OpenOptions = {}): Promise<BriefEngine> {\n // Named as git names them, so the root and the work tree compare.\n const cwd = await canonicalPath(resolve(options.cwd ?? process.cwd()));\n const explicit = options.config === undefined ? undefined : await canonicalPath(resolve(cwd, options.config));\n const configPath = options.noConfig === true ? undefined : (explicit ?? (await locateConfig(cwd, exists)));\n // Asking git costs a process; without git there is nothing to ask.\n const toplevel = options.git === null ? null : await NodeGit.toplevel(cwd);\n const root = configPath === undefined ? (toplevel ?? cwd) : dirname(configPath);\n let config = DEFAULT_CONFIG;\n if (configPath !== undefined) {\n const fs = new NodeFileSystem(dirname(configPath));\n const name = configPath.slice(dirname(configPath).length + 1);\n const text = await fs.read(name);\n if (text === null) throw new EngineError('not-found', `${configPath} does not exist`);\n config = parseConfig(text, relative(cwd, configPath));\n }\n const inTree = toplevel !== null && !relative(toplevel, root).startsWith('..');\n const engine = new BriefEngine({\n root,\n config,\n configFile: configPath === undefined ? null : relative(root, configPath).split(sep).join('/'),\n fs: options.fs ?? new NodeFileSystem(root),\n git: options.git === undefined ? (inTree ? new NodeGit(root) : null) : options.git,\n });\n await engine.load();\n return engine;\n }\n\n get corpus(): Corpus {\n if (this.loaded === undefined) throw new Error('the engine has not loaded its briefs; call load() first');\n return this.loaded;\n }\n\n /** Reads every brief again, as after an archival. */\n async load(): Promise<Corpus> {\n const files: SourceFile[] = [];\n const accept = parseGlob(this.config.files);\n const reject = this.config.exclude.map((pattern) => parseGlob(pattern));\n const isBrief = (name: string): boolean =>\n accept.ok && matchGlob(accept.glob, name) && !reject.some((r) => r.ok && matchGlob(r.glob, name));\n const read = async (directory: string, phase: Phase): Promise<boolean> => {\n const dir = normalisePath(directory);\n const names = await this.fs.list(dir);\n for (const name of names ?? []) {\n if (!isBrief(name)) continue;\n const path = dir === '' ? name : `${dir}/${name}`;\n const text = await this.fs.read(path);\n // A file listed a moment ago and gone now was removed under the run; there is nothing to read.\n /* v8 ignore next */\n if (text !== null) files.push({ path, text, phase });\n }\n return names !== null;\n };\n this.missingBriefs = !(await read(this.config.briefs, 'live'));\n await read(this.config.archive, 'archived');\n this.loaded = buildCorpus(files, this.config);\n return this.loaded;\n }\n\n /**\n * Refuses to report on a briefs directory that does not exist. A check over\n * nothing looks exactly like a clean one, and the likelier cause is a\n * configuration pointing at the wrong place.\n */\n requireBriefs(): void {\n if (this.missingBriefs) {\n throw new EngineError('not-found', `${normalisePath(this.config.briefs)}/ does not exist; run \"spec-brief init\", or set \"briefs\" in the configuration`);\n }\n }\n\n /** The plugins the configuration names, loaded once. */\n async loadedPlugins(): Promise<readonly Plugin[]> {\n this.plugins ??= await loadPlugins(this.config.plugins, this.root);\n return this.plugins;\n }\n\n /** The one brief a reference names: an id, a file name or a path. */\n find(reference: string): Brief {\n const found = findBriefs(this.corpus, reference);\n if (found.length === 0) throw new EngineError('not-found', `no brief is named \"${reference}\"`);\n if (found.length > 1) {\n throw new EngineError('ambiguous', `\"${reference}\" names ${found.length} briefs: ${found.map((b) => b.file).join(', ')}`);\n }\n return found[0] as Brief;\n }\n\n /** The files git sees, or every file when there is no git; `null` when neither can be read. */\n async repoFiles(): Promise<string[] | null> {\n try {\n return this.git === null ? await this.fs.walk() : await this.git.files();\n } catch {\n return null;\n }\n }\n\n async lint(references: readonly string[] = []): Promise<Finding[]> {\n const only = references.length === 0 ? undefined : references.map((r) => this.find(r));\n return lint(this.corpus, { plugins: await this.loadedPlugins(), repoFiles: await this.repoFiles(), ...(only ? { only } : {}) });\n }\n\n async collisions(options: CollisionOptions = {}): Promise<{ report: CollisionReport; findings: Finding[] }> {\n const report = collisions(this.corpus, { repoFiles: await this.repoFiles(), ...options });\n return { report, findings: collisionFindings(this.corpus, report) };\n }\n\n /** Live briefs that can run now: not drafts, every dependency archived. */\n ready(): Brief[] {\n return this.corpus.live.filter((b) => isReady(this.corpus, b));\n }\n\n /** The live dependencies holding a brief back. */\n waitingOn(brief: Brief): string[] {\n return pendingDependencies(this.corpus, brief);\n }\n\n async planArchive(reference: string, options: ArchiveOptions = {}): Promise<Plan> {\n const brief = this.find(reference);\n const date = options.date ?? today();\n if (!isDate(date)) throw new EngineError('invalid', `\"${date}\" is not a date written YYYY-MM-DD`);\n if (options.noGit === true && (options.commit !== undefined || options.base !== undefined)) {\n throw new EngineError('invalid', 'a commit or a base needs git; drop --no-git, or drop --commit and --base');\n }\n const git = options.noGit === true ? null : this.git;\n const base = options.base ?? this.config.archiving.base ?? undefined;\n const findings = await this.lint();\n\n let commit: CommitInfo | undefined;\n let changes: FileChange[] | undefined;\n let dirty: string[] | undefined;\n let pr: PullRequest | undefined;\n if (git !== null) {\n const revision = options.commit ?? (base === undefined ? undefined : 'HEAD');\n if (revision !== undefined) {\n commit = (await git.commit(revision)) ?? undefined;\n if (commit === undefined) throw new EngineError('not-found', `\"${revision}\" names no commit`);\n const from = base === undefined ? null : ((await git.mergeBase(base, commit.sha)) ?? base);\n changes = await git.changes(from, commit.sha);\n }\n dirty = await git.dirty();\n }\n if (options.pr !== undefined) {\n pr = { number: options.pr, url: git === null ? null : pullRequestUrl(await git.remoteUrl('origin'), options.pr) };\n }\n return planArchive(this.corpus, brief, {\n date,\n summary: options.summary,\n pr,\n commit,\n changes,\n dirty,\n allowDirty: options.allowDirty,\n strict: options.strict,\n findings,\n });\n }\n\n planUnarchive(reference: string): Plan {\n return planUnarchive(this.corpus, this.find(reference));\n }\n\n /** Executes a plan and reads the briefs again. A refused plan is never applied. */\n async apply(plan: Plan): Promise<void> {\n if (plan.blocking.length > 0) {\n throw new EngineError('refused', `${plan.action} of ${plan.brief.file} is refused: ${(plan.blocking[0] as Finding).message}`);\n }\n if (plan.done) return;\n await applyPlan(this.fs, plan.ops);\n await this.load();\n }\n\n /** Scaffolds a brief and writes it; the file is new, or nothing is written. */\n async create(options: NewOptions): Promise<{ file: string; content: string }> {\n const title = options.title.trim();\n if (title === '') throw new EngineError('invalid', 'a brief needs a title');\n const id = options.id ?? nextId(this.corpus);\n if (id === null) throw new EngineError('invalid', 'the ids here are not numbers, so there is no next one; pass --id');\n if (/[\\s/\\\\]/.test(id) || id.includes(this.config.id.separator)) {\n throw new EngineError('invalid', `\"${id}\" cannot be an id: no whitespace, slashes or \"${this.config.id.separator}\"`);\n }\n if (findBriefs(this.corpus, id).some((b) => b.id !== null)) throw new EngineError('exists', `the id \"${id}\" is taken`);\n if (options.type !== undefined && this.config.types[options.type] === undefined) {\n const names = Object.keys(this.config.types);\n throw new EngineError('invalid', `\"${options.type}\" is not a brief type here; use one of ${names.join(', ') || '(none)'}`);\n }\n const date = options.date ?? today();\n if (!isDate(date)) throw new EngineError('invalid', `\"${date}\" is not a date written YYYY-MM-DD`);\n const template = this.config.template === null ? null : await this.fs.read(this.config.template);\n if (this.config.template !== null && template === null) {\n throw new EngineError('not-found', `the template ${this.config.template} does not exist`);\n }\n const content = renderNewBrief(\n this.config,\n { id, title, date, type: options.type, wave: options.wave, dependsOn: options.dependsOn },\n template,\n );\n const dir = normalisePath(this.config.briefs);\n const file = `${dir}/${fileNameFor(this.config, id, title)}`;\n if (await this.fs.exists(file)) throw new EngineError('exists', `${file} already exists`);\n await this.fs.write(file, content);\n await this.load();\n return { file, content };\n }\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A front-matter reader and editor for the flat subset of YAML briefs use.
|
|
3
|
+
*
|
|
4
|
+
* Supported: `key: value` at the top level; plain, single-quoted and
|
|
5
|
+
* double-quoted scalars; inline `[a, b]` sequences; block `- item` sequences;
|
|
6
|
+
* `#` comments. Plain scalars follow the YAML 1.2 core schema, so `yes` is a
|
|
7
|
+
* word and `035` keeps its leading zero for whoever reads it as text.
|
|
8
|
+
*
|
|
9
|
+
* Anything richer - nested mappings, block scalars, anchors, tags, a value
|
|
10
|
+
* continued onto the next line - is recognised and reported as unsupported
|
|
11
|
+
* rather than guessed at. A YAML library would parse more, and would be the
|
|
12
|
+
* largest dependency of a package that has none; it would also not give back
|
|
13
|
+
* the line of every value, and an edit that must leave every other line of a
|
|
14
|
+
* file untouched needs exactly that.
|
|
15
|
+
*/
|
|
16
|
+
export interface YamlScalar {
|
|
17
|
+
readonly text: string;
|
|
18
|
+
readonly quoted: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type YamlValue = {
|
|
21
|
+
readonly kind: 'scalar';
|
|
22
|
+
readonly scalar: YamlScalar;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'list';
|
|
25
|
+
readonly items: readonly YamlScalar[];
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'unsupported';
|
|
28
|
+
readonly reason: string;
|
|
29
|
+
};
|
|
30
|
+
export interface FrontMatterEntry {
|
|
31
|
+
/** The key as written. */
|
|
32
|
+
readonly key: string;
|
|
33
|
+
/** The key compared: lower case, without `-` or `_`, so `depends-on` is `dependsOn`. */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** 0-based line of the key. */
|
|
36
|
+
readonly line: number;
|
|
37
|
+
/** 0-based line after the entry's last line. */
|
|
38
|
+
readonly end: number;
|
|
39
|
+
readonly value: YamlValue;
|
|
40
|
+
}
|
|
41
|
+
export interface FrontMatterProblem {
|
|
42
|
+
readonly line: number;
|
|
43
|
+
readonly message: string;
|
|
44
|
+
}
|
|
45
|
+
export interface FrontMatter {
|
|
46
|
+
/** 0-based line of the closing delimiter, or -1 when the block is never closed. */
|
|
47
|
+
readonly close: number;
|
|
48
|
+
readonly entries: readonly FrontMatterEntry[];
|
|
49
|
+
readonly problems: readonly FrontMatterProblem[];
|
|
50
|
+
}
|
|
51
|
+
export declare function keyName(key: string): string;
|
|
52
|
+
/** Reads the front matter at the top of `lines`, or `null` when there is none. */
|
|
53
|
+
export declare function readFrontMatter(lines: readonly string[]): FrontMatter | null;
|
|
54
|
+
/** Parses a value written on the key's own line. `text` is trimmed and non-empty. */
|
|
55
|
+
export declare function parseInline(text: string): YamlValue;
|
|
56
|
+
/** Null in the YAML 1.2 core schema: empty, `~` or `null` in any of its three spellings. */
|
|
57
|
+
export declare function isNull(scalar: YamlScalar): boolean;
|
|
58
|
+
/** Renders a string as a scalar that reads back as the same string. */
|
|
59
|
+
export declare function renderScalar(value: string): string;
|
|
60
|
+
export declare function findEntry(frontMatter: FrontMatter | null, key: string): FrontMatterEntry | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Sets one key, leaving every other line as it was. An existing entry keeps the
|
|
63
|
+
* spelling of its key; a new one goes last. A file with no front matter gains
|
|
64
|
+
* a block.
|
|
65
|
+
*/
|
|
66
|
+
export declare function setEntry(lines: readonly string[], frontMatter: FrontMatter | null, key: string, rendered: string): string[];
|
|
67
|
+
/** Removes one key and its value lines; a key that is absent changes nothing. */
|
|
68
|
+
export declare function removeEntry(lines: readonly string[], frontMatter: FrontMatter | null, key: string): string[];
|