simt-dev 0.2.1 → 0.3.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 +239 -0
- package/bin/simt-dev.mjs +21 -2
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
- package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
- package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
- package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
- package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
- package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
- package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
- package/package.json +2 -1
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +2 -0
- package/rules/stacks/deno.md +2 -0
- package/rules/stacks/dotnet.md +2 -0
- package/rules/stacks/elixir.md +2 -0
- package/rules/stacks/go.md +2 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +2 -0
- package/rules/stacks/rust.md +2 -0
- package/rules/stacks/swift.md +2 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/agenthooks/decide.mjs +123 -0
- package/src/agenthooks/policy.mjs +78 -0
- package/src/agenthooks/render.mjs +240 -0
- package/src/argv.mjs +55 -1
- package/src/bench/ablate.mjs +630 -0
- package/src/bench/adapters.json +149 -0
- package/src/bench/grade.mjs +229 -0
- package/src/bench/mine.mjs +811 -0
- package/src/bench/probes.mjs +1350 -0
- package/src/bench/report.mjs +899 -0
- package/src/bench/run.mjs +1002 -0
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +359 -0
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +45 -21
- package/src/commands/harness.mjs +301 -0
- package/src/commands/help.mjs +73 -2
- package/src/commands/index.mjs +67 -25
- package/src/commands/init.mjs +12 -8
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/plan.mjs +99 -0
- package/src/commands/rules.mjs +42 -57
- package/src/commands/skills.mjs +67 -92
- package/src/commands/skillsinstall.mjs +17 -2
- package/src/commands/sync.mjs +62 -6
- package/src/commands/tokens.mjs +169 -0
- package/src/compare/row.mjs +73 -2
- package/src/decisions/index.mjs +23 -5
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/dispatcher.mjs +35 -3
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/mcp/resources.mjs +157 -0
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +228 -20
- package/src/mcp/tools.mjs +372 -1
- package/src/measure/context.mjs +542 -0
- package/src/measure/loaderView.mjs +435 -0
- package/src/measure/session.mjs +267 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +78 -2
- package/src/packs.mjs +182 -0
- package/src/plan/index.mjs +650 -0
- package/src/plan/template.mjs +331 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/manifest.mjs +114 -3
- package/src/projection/project.mjs +140 -9
- package/src/projection/rules.mjs +365 -22
- package/src/projection/surfaces.mjs +155 -23
- package/src/projection/targets.mjs +37 -0
- package/src/toolwords.mjs +69 -0
- package/templates/plan/tracker.json +36 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// The tracker's shape is data, because it belongs to the repository being read.
|
|
2
|
+
//
|
|
3
|
+
// Hardcoding docs/plans, the word "review", or the column State would index
|
|
4
|
+
// this package's own documents and misread everyone else's. The file
|
|
5
|
+
// templates/plan/tracker.json is the format delivered with the tool. A
|
|
6
|
+
// repository replaces any section of it with the `plans` key in its manifest.
|
|
7
|
+
// A section it leaves out stays as the delivered file has it. init keeps that
|
|
8
|
+
// key, because a key this tool does not write belongs to the repository.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import { CLI } from '../name.mjs';
|
|
15
|
+
import { manifestPath, readManifest } from '../projection/manifest.mjs';
|
|
16
|
+
|
|
17
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
18
|
+
const BUILTIN_DIR = join(PACKAGE_ROOT, 'templates', 'plan');
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_TEMPLATE = 'tracker';
|
|
21
|
+
|
|
22
|
+
const KNOWN = new Set(['$comment', 'name', 'directories', 'id', 'columns', 'states', 'ready', 'dependency', 'wave', 'section']);
|
|
23
|
+
const ID_KEYS = new Set(['pattern', 'headers']);
|
|
24
|
+
const COLUMN_KEYS = new Set(['state', 'depends', 'title', 'evidence']);
|
|
25
|
+
const DIRECTORY_KEYS = new Set(['kind', 'word', 'path']);
|
|
26
|
+
const STATE_KEYS = new Set(['id', 'write', 'read', 'satisfies']);
|
|
27
|
+
const DEPENDENCY_KEYS = new Set(['empty', 'separators', 'notes', 'group']);
|
|
28
|
+
const WAVE_KEYS = new Set(['label', 'members']);
|
|
29
|
+
const SECTION_KEYS = new Set(['depth']);
|
|
30
|
+
|
|
31
|
+
// Read once, at load. Help and the parser have to name the same directories,
|
|
32
|
+
// and a second copy of this JSON in code would be the one that drifted.
|
|
33
|
+
const DEFAULT_BODY = readJson(join(BUILTIN_DIR, `${DEFAULT_TEMPLATE}.json`));
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_DIRECTORIES = Array.isArray(DEFAULT_BODY?.directories)
|
|
36
|
+
? DEFAULT_BODY.directories.map((dir) => dir.path)
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
function readJson(path) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`the ${DEFAULT_TEMPLATE} plan template could not be read (${error.message})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function remedyFor(source) {
|
|
48
|
+
if (source.endsWith(' plans')) {
|
|
49
|
+
const file = source.slice(0, -' plans'.length);
|
|
50
|
+
return `fix the plans key in ${file}, or remove it to use the ${DEFAULT_TEMPLATE} template`;
|
|
51
|
+
}
|
|
52
|
+
if (source.endsWith('.json')) return `fix ${source} so it parses`;
|
|
53
|
+
return `run: ${CLI} --help`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bad(source, message) {
|
|
57
|
+
return { error: `${source}: ${message}`, remedy: remedyFor(source) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function overlay(base, extra) {
|
|
61
|
+
const out = structuredClone(base);
|
|
62
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
63
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && out[key] && typeof out[key] === 'object' && !Array.isArray(out[key])) {
|
|
64
|
+
out[key] = overlay(out[key], value);
|
|
65
|
+
} else {
|
|
66
|
+
out[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function unknownKeys(value, allowed) {
|
|
73
|
+
return Object.keys(value).filter((key) => !allowed.has(key));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function headers(source, value, field, { allowEmpty = false } = {}) {
|
|
77
|
+
if (!Array.isArray(value)) return bad(source, `${field} must be a list of column headers`);
|
|
78
|
+
const names = [];
|
|
79
|
+
for (const header of value) {
|
|
80
|
+
if (typeof header !== 'string' || header.trim() === '') return bad(source, `${field} has a header that is not text`);
|
|
81
|
+
const name = header.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
82
|
+
if (/[|\n\r]/.test(name)) return bad(source, `${field} has a header containing a pipe or a newline`);
|
|
83
|
+
names.push(name);
|
|
84
|
+
}
|
|
85
|
+
if (!allowEmpty && names.length === 0) return bad(source, `${field} needs at least one header`);
|
|
86
|
+
return { names };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pattern(source, value, field, flags = '') {
|
|
90
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 200) {
|
|
91
|
+
return bad(source, `${field} must be a pattern of at most 200 characters`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return { re: new RegExp(value, flags) };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return bad(source, `${field} is not a valid pattern (${error.message})`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function directoryPath(root, value) {
|
|
101
|
+
if (typeof value !== 'string' || value === '' || isAbsolute(value)) return null;
|
|
102
|
+
const rel = value.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
103
|
+
const parts = rel.split('/');
|
|
104
|
+
if (parts.some((part) => part === '' || part === '.' || part === '..')) return null;
|
|
105
|
+
const abs = resolve(root, rel);
|
|
106
|
+
const back = relative(resolve(root), abs);
|
|
107
|
+
if (back === '' || back.startsWith('..') || isAbsolute(back)) return null;
|
|
108
|
+
return back.split(sep).join('/');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function singular(kind) {
|
|
112
|
+
return kind.endsWith('s') && kind.length > 1 ? kind.slice(0, -1) : kind;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function compileDirectories(source, value, root) {
|
|
116
|
+
if (!Array.isArray(value) || value.length === 0) return bad(source, 'directories must list at least one directory');
|
|
117
|
+
const directories = [];
|
|
118
|
+
const kinds = new Set();
|
|
119
|
+
const words = new Set();
|
|
120
|
+
const paths = new Set();
|
|
121
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
122
|
+
const entry = value[i];
|
|
123
|
+
const where = `directories[${i}]`;
|
|
124
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return bad(source, `${where} must be an object`);
|
|
125
|
+
const extra = unknownKeys(entry, DIRECTORY_KEYS);
|
|
126
|
+
if (extra.length > 0) return bad(source, `${where} has unknown ${extra.join(', ')}`);
|
|
127
|
+
if (typeof entry.kind !== 'string' || !/^[a-z][a-z0-9-]*$/.test(entry.kind)) {
|
|
128
|
+
return bad(source, `${where}.kind must be a lower-case word`);
|
|
129
|
+
}
|
|
130
|
+
const path = directoryPath(root, entry.path);
|
|
131
|
+
if (!path) return bad(source, `${where}.path must be a directory inside the repository`);
|
|
132
|
+
const word = entry.word == null ? singular(entry.kind) : String(entry.word).toLowerCase();
|
|
133
|
+
if (!/^[a-z][a-z0-9-]*$/.test(word)) return bad(source, `${where}.word must be a lower-case word`);
|
|
134
|
+
if (kinds.has(entry.kind)) return bad(source, `kind ${entry.kind} is listed twice`);
|
|
135
|
+
if (words.has(word)) return bad(source, `word ${word} is listed twice`);
|
|
136
|
+
if (paths.has(path)) return bad(source, `path ${path} is listed twice`);
|
|
137
|
+
kinds.add(entry.kind);
|
|
138
|
+
words.add(word);
|
|
139
|
+
paths.add(path);
|
|
140
|
+
directories.push({ kind: entry.kind, word, path });
|
|
141
|
+
}
|
|
142
|
+
return { directories };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function compileStates(source, value) {
|
|
146
|
+
if (!Array.isArray(value) || value.length === 0) return bad(source, 'states must list at least one state');
|
|
147
|
+
const read = new Map();
|
|
148
|
+
const write = new Map();
|
|
149
|
+
const known = new Set();
|
|
150
|
+
const satisfies = new Set();
|
|
151
|
+
const order = [];
|
|
152
|
+
const stateNames = [];
|
|
153
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
154
|
+
const entry = value[i];
|
|
155
|
+
const where = `states[${i}]`;
|
|
156
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return bad(source, `${where} must be an object`);
|
|
157
|
+
const extra = unknownKeys(entry, STATE_KEYS);
|
|
158
|
+
if (extra.length > 0) return bad(source, `${where} has unknown ${extra.join(', ')}`);
|
|
159
|
+
if (typeof entry.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(entry.id)) return bad(source, `${where}.id must be a lower-case word`);
|
|
160
|
+
if (typeof entry.write !== 'string' || entry.write.trim() === '' || /[|\n\r]/.test(entry.write)) {
|
|
161
|
+
return bad(source, `${where}.write must be text without a pipe or a newline`);
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(entry.read) || entry.read.length === 0) return bad(source, `${where}.read must list the words that mean this state`);
|
|
164
|
+
if (known.has(entry.id)) return bad(source, `state ${entry.id} is listed twice`);
|
|
165
|
+
const display = entry.write.trim();
|
|
166
|
+
const aliases = new Set([entry.id, display.toLowerCase()]);
|
|
167
|
+
for (const alias of entry.read) {
|
|
168
|
+
if (typeof alias !== 'string' || alias.trim() === '') return bad(source, `${where}.read has a word that is not text`);
|
|
169
|
+
aliases.add(alias.trim().toLowerCase());
|
|
170
|
+
}
|
|
171
|
+
for (const alias of aliases) {
|
|
172
|
+
if (read.has(alias) || write.has(alias)) return bad(source, `the word ${alias} is already a state`);
|
|
173
|
+
}
|
|
174
|
+
for (const alias of aliases) {
|
|
175
|
+
read.set(alias, entry.id);
|
|
176
|
+
write.set(alias, display);
|
|
177
|
+
}
|
|
178
|
+
known.add(entry.id);
|
|
179
|
+
order.push(entry.id);
|
|
180
|
+
stateNames.push(display);
|
|
181
|
+
if (entry.satisfies === true) satisfies.add(entry.id);
|
|
182
|
+
else if (entry.satisfies != null && entry.satisfies !== false) return bad(source, `${where}.satisfies must be true or false`);
|
|
183
|
+
}
|
|
184
|
+
if (satisfies.size === 0) return bad(source, 'one state must have satisfies true, or nothing can ever be ready');
|
|
185
|
+
return { read, write, known, satisfies, order, stateNames };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function compile(body, source, root) {
|
|
189
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return bad(source, 'the template must be a JSON object');
|
|
190
|
+
const extra = unknownKeys(body, KNOWN);
|
|
191
|
+
if (extra.length > 0) return bad(source, `unknown ${extra.join(', ')}`);
|
|
192
|
+
for (const [key, allowed] of [
|
|
193
|
+
['id', ID_KEYS],
|
|
194
|
+
['columns', COLUMN_KEYS],
|
|
195
|
+
['dependency', DEPENDENCY_KEYS],
|
|
196
|
+
['wave', WAVE_KEYS],
|
|
197
|
+
['section', SECTION_KEYS],
|
|
198
|
+
]) {
|
|
199
|
+
const value = body[key];
|
|
200
|
+
if (value == null) return bad(source, `${key} is missing`);
|
|
201
|
+
if (typeof value !== 'object' || Array.isArray(value)) return bad(source, `${key} must be an object`);
|
|
202
|
+
const unexpected = unknownKeys(value, allowed);
|
|
203
|
+
if (unexpected.length > 0) return bad(source, `${key} has unknown ${unexpected.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const directories = compileDirectories(source, body.directories, root);
|
|
207
|
+
if (directories.error) return directories;
|
|
208
|
+
const states = compileStates(source, body.states);
|
|
209
|
+
if (states.error) return states;
|
|
210
|
+
|
|
211
|
+
const idHeaders = headers(source, body.id.headers, 'id.headers');
|
|
212
|
+
if (idHeaders.error) return idHeaders;
|
|
213
|
+
const stateHeaders = headers(source, body.columns.state, 'columns.state');
|
|
214
|
+
if (stateHeaders.error) return stateHeaders;
|
|
215
|
+
const dependsHeaders = headers(source, body.columns.depends, 'columns.depends', { allowEmpty: true });
|
|
216
|
+
if (dependsHeaders.error) return dependsHeaders;
|
|
217
|
+
const titleHeaders = headers(source, body.columns.title, 'columns.title', { allowEmpty: true });
|
|
218
|
+
if (titleHeaders.error) return titleHeaders;
|
|
219
|
+
const evidenceHeaders = headers(source, body.columns.evidence, 'columns.evidence', { allowEmpty: true });
|
|
220
|
+
if (evidenceHeaders.error) return evidenceHeaders;
|
|
221
|
+
const waveLabel = headers(source, body.wave.label, 'wave.label');
|
|
222
|
+
if (waveLabel.error) return waveLabel;
|
|
223
|
+
const waveMembers = headers(source, body.wave.members, 'wave.members');
|
|
224
|
+
if (waveMembers.error) return waveMembers;
|
|
225
|
+
|
|
226
|
+
const idRe = pattern(source, body.id.pattern, 'id.pattern');
|
|
227
|
+
if (idRe.error) return idRe;
|
|
228
|
+
const groupRe = pattern(source, body.dependency.group, 'dependency.group', 'i');
|
|
229
|
+
if (groupRe.error) return groupRe;
|
|
230
|
+
|
|
231
|
+
if (!Array.isArray(body.dependency.empty)) return bad(source, 'dependency.empty must be a list of words');
|
|
232
|
+
const empty = new Set();
|
|
233
|
+
for (const token of body.dependency.empty) {
|
|
234
|
+
if (typeof token !== 'string' || token.trim() === '') return bad(source, 'dependency.empty has a word that is not text');
|
|
235
|
+
empty.add(token.trim().toLowerCase());
|
|
236
|
+
}
|
|
237
|
+
if (typeof body.dependency.separators !== 'string' || body.dependency.separators.length === 0 || body.dependency.separators.length > 8) {
|
|
238
|
+
return bad(source, 'dependency.separators must be one to eight characters');
|
|
239
|
+
}
|
|
240
|
+
if (/[A-Za-z0-9\s\\]/.test(body.dependency.separators)) {
|
|
241
|
+
return bad(source, 'dependency.separators must be punctuation');
|
|
242
|
+
}
|
|
243
|
+
if (typeof body.dependency.notes !== 'boolean') return bad(source, 'dependency.notes must be true or false');
|
|
244
|
+
|
|
245
|
+
const depth = body.section.depth;
|
|
246
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > 6) return bad(source, 'section.depth must be from 1 to 6');
|
|
247
|
+
const ready = body.ready;
|
|
248
|
+
if (typeof ready !== 'string' || !states.known.has(ready)) return bad(source, 'ready must name a state');
|
|
249
|
+
|
|
250
|
+
const name = typeof body.name === 'string' && body.name.trim() !== '' ? body.name.trim() : source;
|
|
251
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,40}$/.test(name)) return bad(source, 'name must be a short word');
|
|
252
|
+
|
|
253
|
+
// Longer words first, so a word that starts another (`plan` and `planning`)
|
|
254
|
+
// is not cut off at the shorter one.
|
|
255
|
+
const words = [...directories.directories].sort((a, b) => b.word.length - a.word.length).map((dir) => dir.word);
|
|
256
|
+
let crossRe;
|
|
257
|
+
try {
|
|
258
|
+
crossRe = new RegExp(`^(${words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})s?\\s+(\\d+)\\s+(.+)$`, 'i');
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return bad(source, `the directory words are not a valid reference (${error.message})`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const byWord = new Map(directories.directories.map((dir) => [dir.word, dir.kind]));
|
|
264
|
+
const doneWrite = body.states.find((entry) => entry.satisfies === true).write.trim();
|
|
265
|
+
const { read, write } = states;
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
format: {
|
|
269
|
+
name,
|
|
270
|
+
source,
|
|
271
|
+
directories: directories.directories,
|
|
272
|
+
byWord,
|
|
273
|
+
order: states.order,
|
|
274
|
+
ready,
|
|
275
|
+
doneWrite,
|
|
276
|
+
stateNames: states.stateNames,
|
|
277
|
+
satisfies: states.satisfies,
|
|
278
|
+
known: states.known,
|
|
279
|
+
empty,
|
|
280
|
+
separators: body.dependency.separators,
|
|
281
|
+
notes: body.dependency.notes,
|
|
282
|
+
idRe: idRe.re,
|
|
283
|
+
idHeaders: idHeaders.names,
|
|
284
|
+
stateHeaders: stateHeaders.names,
|
|
285
|
+
dependsHeaders: dependsHeaders.names,
|
|
286
|
+
titleHeaders: titleHeaders.names,
|
|
287
|
+
evidenceHeaders: evidenceHeaders.names,
|
|
288
|
+
waveLabel: waveLabel.names,
|
|
289
|
+
waveMembers: waveMembers.names,
|
|
290
|
+
crossRe,
|
|
291
|
+
groupRe: groupRe.re,
|
|
292
|
+
headingRe: new RegExp(`^(#{1,${depth}})\\s+(.*\\S)\\s*$`),
|
|
293
|
+
readState(raw) {
|
|
294
|
+
const plain = String(raw ?? '').replace(/\*\*/g, '').trim().toLowerCase();
|
|
295
|
+
if (read.has(plain)) return read.get(plain);
|
|
296
|
+
const token = plain.split(/[\s(]/)[0];
|
|
297
|
+
return read.get(token) ?? (token || 'unknown');
|
|
298
|
+
},
|
|
299
|
+
canonical(input) {
|
|
300
|
+
return write.get(String(input ?? '').trim().toLowerCase()) ?? null;
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The format for this repository.
|
|
308
|
+
*
|
|
309
|
+
* The delivered template applies when the manifest has no `plans` key, and
|
|
310
|
+
* when there is no manifest. The key overlays the delivered template: a
|
|
311
|
+
* section it names replaces that section. A key that cannot be read is an
|
|
312
|
+
* error. Falling back to the delivered template would index the wrong
|
|
313
|
+
* directories and mark the wrong row.
|
|
314
|
+
*
|
|
315
|
+
* @returns {Promise<{format: object}|{error: string, remedy: string}>}
|
|
316
|
+
*/
|
|
317
|
+
export async function loadFormat(root) {
|
|
318
|
+
if (!DEFAULT_BODY || typeof DEFAULT_BODY !== 'object') {
|
|
319
|
+
return bad(`the ${DEFAULT_TEMPLATE} plan template`, 'is not a JSON object');
|
|
320
|
+
}
|
|
321
|
+
const { manifest, exists, error } = await readManifest(root);
|
|
322
|
+
if (!error && (!exists || manifest?.plans == null)) {
|
|
323
|
+
return compile(DEFAULT_BODY, `the ${DEFAULT_TEMPLATE} plan template`, root);
|
|
324
|
+
}
|
|
325
|
+
const file = basename(await manifestPath(root));
|
|
326
|
+
if (error) return bad(file, error);
|
|
327
|
+
if (typeof manifest.plans !== 'object' || Array.isArray(manifest.plans)) {
|
|
328
|
+
return bad(`${file} plans`, 'must be an object');
|
|
329
|
+
}
|
|
330
|
+
return compile(overlay(DEFAULT_BODY, manifest.plans), `${file} plans`, root);
|
|
331
|
+
}
|