@descent-vtt/spec-harness 0.0.0 → 0.1.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/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +12 -0
- package/CHANGELOG.md +56 -0
- package/README.md +127 -9
- package/bin/spec-harness.js +19 -0
- package/dist/audit.d.ts +69 -0
- package/dist/audit.js +93 -0
- package/dist/audit.js.map +1 -0
- package/dist/branch.d.ts +26 -0
- package/dist/branch.js +100 -0
- package/dist/branch.js.map +1 -0
- package/dist/briefs.d.ts +49 -0
- package/dist/briefs.js +111 -0
- package/dist/briefs.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +224 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +167 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +56 -0
- package/dist/context.js +125 -0
- package/dist/context.js.map +1 -0
- package/dist/document.d.ts +33 -0
- package/dist/document.js +9 -0
- package/dist/document.js.map +1 -0
- package/dist/fs.d.ts +29 -0
- package/dist/fs.js +85 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +57 -0
- package/dist/git.js +129 -0
- package/dist/git.js.map +1 -0
- package/dist/guard.d.ts +43 -0
- package/dist/guard.js +120 -0
- package/dist/guard.js.map +1 -0
- package/dist/hooks.d.ts +50 -0
- package/dist/hooks.js +109 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/junit.d.ts +30 -0
- package/dist/junit.js +166 -0
- package/dist/junit.js.map +1 -0
- package/dist/manifests.d.ts +52 -0
- package/dist/manifests.js +346 -0
- package/dist/manifests.js.map +1 -0
- package/dist/plugin.d.ts +45 -0
- package/dist/plugin.js +79 -0
- package/dist/plugin.js.map +1 -0
- package/dist/premises.d.ts +16 -0
- package/dist/premises.js +71 -0
- package/dist/premises.js.map +1 -0
- package/dist/probe.d.ts +108 -0
- package/dist/probe.js +203 -0
- package/dist/probe.js.map +1 -0
- package/dist/reader.d.ts +8 -0
- package/dist/reader.js +105 -0
- package/dist/reader.js.map +1 -0
- package/dist/round.d.ts +68 -0
- package/dist/round.js +339 -0
- package/dist/round.js.map +1 -0
- package/dist/rulings.d.ts +92 -0
- package/dist/rulings.js +160 -0
- package/dist/rulings.js.map +1 -0
- package/dist/sandbox.d.ts +28 -0
- package/dist/sandbox.js +110 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +32 -0
- package/dist/setup.js +218 -0
- package/dist/setup.js.map +1 -0
- package/dist/siblings.d.ts +66 -0
- package/dist/siblings.js +134 -0
- package/dist/siblings.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/index.d.ts +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +157 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
- 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 +22 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -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 +439 -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 +287 -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/dist/versions.d.ts +42 -0
- package/dist/versions.js +57 -0
- package/dist/versions.js.map +1 -0
- package/dist/workspace.d.ts +77 -0
- package/dist/workspace.js +138 -0
- package/dist/workspace.js.map +1 -0
- package/hooks/hooks.json +28 -0
- package/package.json +85 -6
- package/skills/close-round/SKILL.md +38 -0
- package/skills/draft-brief/SKILL.md +64 -0
- package/skills/run-round/SKILL.md +46 -0
- package/skills/split-goal/SKILL.md +42 -0
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which dependencies a round added, removed or moved, read from the manifests
|
|
3
|
+
* it changed.
|
|
4
|
+
*
|
|
5
|
+
* "No new dependency without saying so" is one of the few audit questions
|
|
6
|
+
* that can be answered exactly, and it matters: a dependency is code nobody in
|
|
7
|
+
* the review read. Each reader here understands the part of its format that
|
|
8
|
+
* declares dependencies and nothing more - no resolver, no lockfile, no
|
|
9
|
+
* network. A manifest it cannot read is reported as unread, never as clean.
|
|
10
|
+
*/
|
|
11
|
+
export type Ecosystem = 'npm' | 'cargo' | 'go' | 'pip' | 'python' | 'nuget' | 'bundler';
|
|
12
|
+
export interface Dependency {
|
|
13
|
+
readonly section: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/** The version or requirement as written; `''` when none is. */
|
|
16
|
+
readonly version: string;
|
|
17
|
+
}
|
|
18
|
+
export interface DependencyChange {
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly ecosystem: Ecosystem;
|
|
21
|
+
readonly section: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/** `null` when the dependency is new. */
|
|
24
|
+
readonly before: string | null;
|
|
25
|
+
/** `null` when the dependency was removed. */
|
|
26
|
+
readonly after: string | null;
|
|
27
|
+
}
|
|
28
|
+
export type ManifestRead = {
|
|
29
|
+
readonly ok: true;
|
|
30
|
+
readonly dependencies: readonly Dependency[];
|
|
31
|
+
} | {
|
|
32
|
+
readonly ok: false;
|
|
33
|
+
readonly error: string;
|
|
34
|
+
};
|
|
35
|
+
/** The ecosystem a manifest file name belongs to, or `null`. */
|
|
36
|
+
export declare function ecosystemOf(path: string): Ecosystem | null;
|
|
37
|
+
/** A predicate over paths for the configured manifest names, matched at any depth. */
|
|
38
|
+
export declare function manifestMatcher(patterns: readonly string[]): (path: string) => boolean;
|
|
39
|
+
/** The name a PEP 508 requirement names, normalised as PEP 503 says. */
|
|
40
|
+
export declare function pythonName(requirement: string): string;
|
|
41
|
+
/** Reads the dependencies a manifest declares. */
|
|
42
|
+
export declare function readManifest(ecosystem: Ecosystem, text: string): ManifestRead;
|
|
43
|
+
/**
|
|
44
|
+
* What changed between two versions of one manifest. `null` text is a file
|
|
45
|
+
* that does not exist on that side. Returns the error instead when either
|
|
46
|
+
* side cannot be read.
|
|
47
|
+
*/
|
|
48
|
+
export declare function diffManifest(file: string, ecosystem: Ecosystem, before: string | null, after: string | null): {
|
|
49
|
+
readonly changes: readonly DependencyChange[];
|
|
50
|
+
} | {
|
|
51
|
+
readonly error: string;
|
|
52
|
+
};
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which dependencies a round added, removed or moved, read from the manifests
|
|
3
|
+
* it changed.
|
|
4
|
+
*
|
|
5
|
+
* "No new dependency without saying so" is one of the few audit questions
|
|
6
|
+
* that can be answered exactly, and it matters: a dependency is code nobody in
|
|
7
|
+
* the review read. Each reader here understands the part of its format that
|
|
8
|
+
* declares dependencies and nothing more - no resolver, no lockfile, no
|
|
9
|
+
* network. A manifest it cannot read is reported as unread, never as clean.
|
|
10
|
+
*/
|
|
11
|
+
import { parseGlob } from './vendor/spec-core/pattern/index.js';
|
|
12
|
+
/** The ecosystem a manifest file name belongs to, or `null`. */
|
|
13
|
+
export function ecosystemOf(path) {
|
|
14
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
15
|
+
if (name === 'package.json')
|
|
16
|
+
return 'npm';
|
|
17
|
+
if (name === 'Cargo.toml')
|
|
18
|
+
return 'cargo';
|
|
19
|
+
if (name === 'go.mod')
|
|
20
|
+
return 'go';
|
|
21
|
+
if (name === 'pyproject.toml')
|
|
22
|
+
return 'python';
|
|
23
|
+
if (/^requirements.*\.txt$/.test(name))
|
|
24
|
+
return 'pip';
|
|
25
|
+
if (name === 'Directory.Packages.props' || /\.(?:cs|fs|vb)proj$/.test(name))
|
|
26
|
+
return 'nuget';
|
|
27
|
+
if (name === 'Gemfile')
|
|
28
|
+
return 'bundler';
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** A predicate over paths for the configured manifest names, matched at any depth. */
|
|
32
|
+
export function manifestMatcher(patterns) {
|
|
33
|
+
const globs = [];
|
|
34
|
+
for (const pattern of patterns) {
|
|
35
|
+
const parsed = parseGlob(pattern, { dialect: 'ripgrep', caseSensitive: true });
|
|
36
|
+
if (parsed.ok)
|
|
37
|
+
globs.push(parsed.glob);
|
|
38
|
+
}
|
|
39
|
+
return (path) => globs.some((glob) => glob.match(path));
|
|
40
|
+
}
|
|
41
|
+
/* --------------------------------------------------------------------- npm */
|
|
42
|
+
const NPM_SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
|
|
43
|
+
function readNpm(text) {
|
|
44
|
+
let value;
|
|
45
|
+
try {
|
|
46
|
+
value = JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { ok: false, error: 'not valid JSON' };
|
|
50
|
+
}
|
|
51
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
52
|
+
return { ok: false, error: 'not a JSON object' };
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const section of NPM_SECTIONS) {
|
|
55
|
+
const table = value[section];
|
|
56
|
+
if (table === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
if (typeof table !== 'object' || table === null || Array.isArray(table))
|
|
59
|
+
return { ok: false, error: `"${section}" is not an object` };
|
|
60
|
+
for (const [name, version] of Object.entries(table))
|
|
61
|
+
out.push({ section, name, version: String(version) });
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, dependencies: out };
|
|
64
|
+
}
|
|
65
|
+
/* -------------------------------------------------------------------- TOML */
|
|
66
|
+
/** A TOML value on one line, or the start of a multi-line array. Comments and quotes read the way TOML reads them. */
|
|
67
|
+
function stripTomlComment(line) {
|
|
68
|
+
let quote = null;
|
|
69
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
70
|
+
const ch = line.charAt(i);
|
|
71
|
+
if (quote !== null) {
|
|
72
|
+
if (ch === '\\' && quote === '"')
|
|
73
|
+
i += 1;
|
|
74
|
+
else if (ch === quote)
|
|
75
|
+
quote = null;
|
|
76
|
+
}
|
|
77
|
+
else if (ch === '"' || ch === "'") {
|
|
78
|
+
quote = ch;
|
|
79
|
+
}
|
|
80
|
+
else if (ch === '#') {
|
|
81
|
+
return line.slice(0, i);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return line;
|
|
85
|
+
}
|
|
86
|
+
function unquote(value) {
|
|
87
|
+
const trimmed = value.trim();
|
|
88
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') || trimmed.startsWith("'")) && trimmed.endsWith(trimmed.charAt(0))) {
|
|
89
|
+
return trimmed.slice(1, -1);
|
|
90
|
+
}
|
|
91
|
+
return trimmed;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The strings of a TOML array, `["a>=1", 'b']`, possibly spread over lines.
|
|
95
|
+
* A string inside an inline table is a value of that table, not an element:
|
|
96
|
+
* `{include-group = "test"}` in a dependency group names a group, not a package.
|
|
97
|
+
*/
|
|
98
|
+
function tomlStrings(text) {
|
|
99
|
+
const out = [];
|
|
100
|
+
let depth = 0;
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < text.length) {
|
|
103
|
+
const ch = text.charAt(i);
|
|
104
|
+
if (ch === '{')
|
|
105
|
+
depth += 1;
|
|
106
|
+
else if (ch === '}')
|
|
107
|
+
depth -= 1;
|
|
108
|
+
if (ch === '"' || ch === "'") {
|
|
109
|
+
let j = i + 1;
|
|
110
|
+
let value = '';
|
|
111
|
+
while (j < text.length && text.charAt(j) !== ch) {
|
|
112
|
+
if (text.charAt(j) === '\\' && ch === '"') {
|
|
113
|
+
value += text.charAt(j + 1);
|
|
114
|
+
j += 2;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
value += text.charAt(j);
|
|
118
|
+
j += 1;
|
|
119
|
+
}
|
|
120
|
+
if (depth === 0)
|
|
121
|
+
out.push(value);
|
|
122
|
+
i = j + 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
i += 1;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** The name a PEP 508 requirement names, normalised as PEP 503 says. */
|
|
130
|
+
export function pythonName(requirement) {
|
|
131
|
+
const match = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(requirement);
|
|
132
|
+
return (match?.[1] ?? requirement.trim()).toLowerCase().replace(/[-_.]+/g, '-');
|
|
133
|
+
}
|
|
134
|
+
/** Top-level `key = value` lines per table header, arrays gathered across lines. Enough for dependency tables. */
|
|
135
|
+
function tomlEntries(text) {
|
|
136
|
+
const out = [];
|
|
137
|
+
let table = '';
|
|
138
|
+
const lines = text.split(/\r?\n/);
|
|
139
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
140
|
+
const line = stripTomlComment(lines[i]).trim();
|
|
141
|
+
if (line === '')
|
|
142
|
+
continue;
|
|
143
|
+
const header = /^\[\[?\s*([^\]]+?)\s*\]\]?$/.exec(line);
|
|
144
|
+
if (header !== null) {
|
|
145
|
+
table = header[1].replace(/\s*\.\s*/g, '.');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const eq = line.indexOf('=');
|
|
149
|
+
if (eq <= 0)
|
|
150
|
+
continue;
|
|
151
|
+
const key = unquote(line.slice(0, eq));
|
|
152
|
+
let value = line.slice(eq + 1).trim();
|
|
153
|
+
if (value.startsWith('[') && !balanced(value)) {
|
|
154
|
+
while (i + 1 < lines.length && !balanced(value)) {
|
|
155
|
+
i += 1;
|
|
156
|
+
value += ` ${stripTomlComment(lines[i]).trim()}`;
|
|
157
|
+
}
|
|
158
|
+
if (!balanced(value))
|
|
159
|
+
return `the array for "${key}" is never closed`;
|
|
160
|
+
}
|
|
161
|
+
out.push({ table, key, value });
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
function balanced(value) {
|
|
166
|
+
let depth = 0;
|
|
167
|
+
let quote = null;
|
|
168
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
169
|
+
const ch = value.charAt(i);
|
|
170
|
+
if (quote !== null) {
|
|
171
|
+
if (ch === '\\' && quote === '"')
|
|
172
|
+
i += 1;
|
|
173
|
+
else if (ch === quote)
|
|
174
|
+
quote = null;
|
|
175
|
+
}
|
|
176
|
+
else if (ch === '"' || ch === "'")
|
|
177
|
+
quote = ch;
|
|
178
|
+
else if (ch === '[')
|
|
179
|
+
depth += 1;
|
|
180
|
+
else if (ch === ']')
|
|
181
|
+
depth -= 1;
|
|
182
|
+
}
|
|
183
|
+
return depth <= 0 && quote === null;
|
|
184
|
+
}
|
|
185
|
+
const CARGO_TABLE = /^(?:target\..+\.)?((?:workspace\.)?(?:dev-|build-)?dependencies)(?:\.(.+))?$/;
|
|
186
|
+
function readCargo(text) {
|
|
187
|
+
const entries = tomlEntries(text);
|
|
188
|
+
if (typeof entries === 'string')
|
|
189
|
+
return { ok: false, error: entries };
|
|
190
|
+
const out = new Map();
|
|
191
|
+
for (const { table, key, value } of entries) {
|
|
192
|
+
const match = CARGO_TABLE.exec(table);
|
|
193
|
+
if (match === null)
|
|
194
|
+
continue;
|
|
195
|
+
const section = match[1];
|
|
196
|
+
const inline = match[2];
|
|
197
|
+
if (inline !== undefined) {
|
|
198
|
+
// [dependencies.serde] then `version = "1"`: the table names the crate.
|
|
199
|
+
const name = unquote(inline);
|
|
200
|
+
const id = `${section}\u0000${name}`;
|
|
201
|
+
const current = out.get(id) ?? { section, name, version: '' };
|
|
202
|
+
out.set(id, key === 'version' ? { ...current, version: unquote(value) } : current);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const version = value.startsWith('{') ? (/version\s*=\s*("[^"]*"|'[^']*')/.exec(value)?.[1] ?? '') : value;
|
|
206
|
+
out.set(`${section}\u0000${key}`, { section, name: key, version: unquote(version) });
|
|
207
|
+
}
|
|
208
|
+
return { ok: true, dependencies: [...out.values()] };
|
|
209
|
+
}
|
|
210
|
+
function readPyproject(text) {
|
|
211
|
+
const entries = tomlEntries(text);
|
|
212
|
+
if (typeof entries === 'string')
|
|
213
|
+
return { ok: false, error: entries };
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const { table, key, value } of entries) {
|
|
216
|
+
if (table === 'project' && key === 'dependencies') {
|
|
217
|
+
for (const requirement of tomlStrings(value))
|
|
218
|
+
out.push({ section: 'project', name: pythonName(requirement), version: requirement });
|
|
219
|
+
}
|
|
220
|
+
else if (table === 'project.optional-dependencies' || table === 'dependency-groups') {
|
|
221
|
+
for (const requirement of tomlStrings(value))
|
|
222
|
+
out.push({ section: `${table}.${key}`, name: pythonName(requirement), version: requirement });
|
|
223
|
+
}
|
|
224
|
+
else if (/^tool\.poetry\.(?:group\.[^.]+\.)?(?:dev-)?dependencies$/.test(table) && key.toLowerCase() !== 'python') {
|
|
225
|
+
out.push({ section: table, name: pythonName(key), version: unquote(value) });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { ok: true, dependencies: out };
|
|
229
|
+
}
|
|
230
|
+
/* ------------------------------------------------------------ line formats */
|
|
231
|
+
function readGoMod(text) {
|
|
232
|
+
const out = [];
|
|
233
|
+
let block = false;
|
|
234
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
235
|
+
const indirect = /\/\/\s*indirect\b/.test(raw);
|
|
236
|
+
const line = raw.replace(/\/\/.*$/, '').trim();
|
|
237
|
+
if (line === '')
|
|
238
|
+
continue;
|
|
239
|
+
if (block) {
|
|
240
|
+
if (line === ')') {
|
|
241
|
+
block = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const [name, version] = line.split(/\s+/);
|
|
245
|
+
if (name !== undefined)
|
|
246
|
+
out.push({ section: indirect ? 'require (indirect)' : 'require', name, version: version ?? '' });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (/^require\s*\($/.test(line)) {
|
|
250
|
+
block = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const single = /^require\s+(\S+)\s+(\S+)/.exec(line);
|
|
254
|
+
if (single !== null)
|
|
255
|
+
out.push({ section: indirect ? 'require (indirect)' : 'require', name: single[1], version: single[2] });
|
|
256
|
+
}
|
|
257
|
+
if (block)
|
|
258
|
+
return { ok: false, error: 'a require block is never closed' };
|
|
259
|
+
return { ok: true, dependencies: out };
|
|
260
|
+
}
|
|
261
|
+
function readRequirements(text) {
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
264
|
+
const line = raw.replace(/(^|\s)#.*$/, '').trim();
|
|
265
|
+
if (line === '' || line.startsWith('-'))
|
|
266
|
+
continue;
|
|
267
|
+
out.push({ section: 'requirements', name: pythonName(line), version: line });
|
|
268
|
+
}
|
|
269
|
+
return { ok: true, dependencies: out };
|
|
270
|
+
}
|
|
271
|
+
function readNuget(text) {
|
|
272
|
+
const out = [];
|
|
273
|
+
const element = /<(PackageReference|PackageVersion)\b([^>]*?)\/?>/g;
|
|
274
|
+
for (let match = element.exec(text); match !== null; match = element.exec(text)) {
|
|
275
|
+
const attributes = match[2];
|
|
276
|
+
const name = /\bInclude\s*=\s*"([^"]*)"/.exec(attributes)?.[1] ?? /\bUpdate\s*=\s*"([^"]*)"/.exec(attributes)?.[1];
|
|
277
|
+
if (name === undefined)
|
|
278
|
+
continue;
|
|
279
|
+
const version = /\bVersion\s*=\s*"([^"]*)"/.exec(attributes)?.[1] ?? '';
|
|
280
|
+
out.push({ section: match[1], name, version });
|
|
281
|
+
}
|
|
282
|
+
return { ok: true, dependencies: out };
|
|
283
|
+
}
|
|
284
|
+
function readGemfile(text) {
|
|
285
|
+
const out = [];
|
|
286
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
287
|
+
const match = /^\s*gem\s+(["'])([^"']+)\1\s*(?:,\s*(["'])([^"']*)\3)?/.exec(raw);
|
|
288
|
+
if (match !== null)
|
|
289
|
+
out.push({ section: 'gem', name: match[2], version: match[4] ?? '' });
|
|
290
|
+
}
|
|
291
|
+
return { ok: true, dependencies: out };
|
|
292
|
+
}
|
|
293
|
+
/** Reads the dependencies a manifest declares. */
|
|
294
|
+
export function readManifest(ecosystem, text) {
|
|
295
|
+
switch (ecosystem) {
|
|
296
|
+
case 'npm':
|
|
297
|
+
return readNpm(text);
|
|
298
|
+
case 'cargo':
|
|
299
|
+
return readCargo(text);
|
|
300
|
+
case 'go':
|
|
301
|
+
return readGoMod(text);
|
|
302
|
+
case 'python':
|
|
303
|
+
return readPyproject(text);
|
|
304
|
+
case 'pip':
|
|
305
|
+
return readRequirements(text);
|
|
306
|
+
case 'nuget':
|
|
307
|
+
return readNuget(text);
|
|
308
|
+
case 'bundler':
|
|
309
|
+
return readGemfile(text);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* What changed between two versions of one manifest. `null` text is a file
|
|
314
|
+
* that does not exist on that side. Returns the error instead when either
|
|
315
|
+
* side cannot be read.
|
|
316
|
+
*/
|
|
317
|
+
export function diffManifest(file, ecosystem, before, after) {
|
|
318
|
+
const read = (text, side) => {
|
|
319
|
+
if (text === null)
|
|
320
|
+
return [];
|
|
321
|
+
const result = readManifest(ecosystem, text);
|
|
322
|
+
return result.ok ? result.dependencies : `${file} (${side}): ${result.error}`;
|
|
323
|
+
};
|
|
324
|
+
const old = read(before, 'before');
|
|
325
|
+
if (typeof old === 'string')
|
|
326
|
+
return { error: old };
|
|
327
|
+
const now = read(after, 'after');
|
|
328
|
+
if (typeof now === 'string')
|
|
329
|
+
return { error: now };
|
|
330
|
+
const key = (d) => `${d.section}\u0000${d.name}`;
|
|
331
|
+
const was = new Map(old.map((d) => [key(d), d]));
|
|
332
|
+
const is = new Map(now.map((d) => [key(d), d]));
|
|
333
|
+
const changes = [];
|
|
334
|
+
for (const [id, dependency] of is) {
|
|
335
|
+
const previous = was.get(id);
|
|
336
|
+
if (previous === undefined || previous.version !== dependency.version) {
|
|
337
|
+
changes.push({ file, ecosystem, section: dependency.section, name: dependency.name, before: previous?.version ?? null, after: dependency.version });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const [id, dependency] of was) {
|
|
341
|
+
if (!is.has(id))
|
|
342
|
+
changes.push({ file, ecosystem, section: dependency.section, name: dependency.name, before: dependency.version, after: null });
|
|
343
|
+
}
|
|
344
|
+
return { changes };
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=manifests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifests.js","sourceRoot":"","sources":["../src/manifests.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which dependencies a round added, removed or moved, read from the manifests\n * it changed.\n *\n * \"No new dependency without saying so\" is one of the few audit questions\n * that can be answered exactly, and it matters: a dependency is code nobody in\n * the review read. Each reader here understands the part of its format that\n * declares dependencies and nothing more - no resolver, no lockfile, no\n * network. A manifest it cannot read is reported as unread, never as clean.\n */\n\nimport { parseGlob, type Glob } from './vendor/spec-core/pattern/index.js';\n\nexport type Ecosystem = 'npm' | 'cargo' | 'go' | 'pip' | 'python' | 'nuget' | 'bundler';\n\nexport interface Dependency {\n readonly section: string;\n readonly name: string;\n /** The version or requirement as written; `''` when none is. */\n readonly version: string;\n}\n\nexport interface DependencyChange {\n readonly file: string;\n readonly ecosystem: Ecosystem;\n readonly section: string;\n readonly name: string;\n /** `null` when the dependency is new. */\n readonly before: string | null;\n /** `null` when the dependency was removed. */\n readonly after: string | null;\n}\n\nexport type ManifestRead = { readonly ok: true; readonly dependencies: readonly Dependency[] } | { readonly ok: false; readonly error: string };\n\n/** The ecosystem a manifest file name belongs to, or `null`. */\nexport function ecosystemOf(path: string): Ecosystem | null {\n const name = path.slice(path.lastIndexOf('/') + 1);\n if (name === 'package.json') return 'npm';\n if (name === 'Cargo.toml') return 'cargo';\n if (name === 'go.mod') return 'go';\n if (name === 'pyproject.toml') return 'python';\n if (/^requirements.*\\.txt$/.test(name)) return 'pip';\n if (name === 'Directory.Packages.props' || /\\.(?:cs|fs|vb)proj$/.test(name)) return 'nuget';\n if (name === 'Gemfile') return 'bundler';\n return null;\n}\n\n/** A predicate over paths for the configured manifest names, matched at any depth. */\nexport function manifestMatcher(patterns: readonly string[]): (path: string) => boolean {\n const globs: Glob[] = [];\n for (const pattern of patterns) {\n const parsed = parseGlob(pattern, { dialect: 'ripgrep', caseSensitive: true });\n if (parsed.ok) globs.push(parsed.glob);\n }\n return (path) => globs.some((glob) => glob.match(path));\n}\n\n/* --------------------------------------------------------------------- npm */\n\nconst NPM_SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];\n\nfunction readNpm(text: string): ManifestRead {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return { ok: false, error: 'not valid JSON' };\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return { ok: false, error: 'not a JSON object' };\n const out: Dependency[] = [];\n for (const section of NPM_SECTIONS) {\n const table = (value as Record<string, unknown>)[section];\n if (table === undefined) continue;\n if (typeof table !== 'object' || table === null || Array.isArray(table)) return { ok: false, error: `\"${section}\" is not an object` };\n for (const [name, version] of Object.entries(table)) out.push({ section, name, version: String(version) });\n }\n return { ok: true, dependencies: out };\n}\n\n/* -------------------------------------------------------------------- TOML */\n\n/** A TOML value on one line, or the start of a multi-line array. Comments and quotes read the way TOML reads them. */\nfunction stripTomlComment(line: string): string {\n let quote: string | null = null;\n for (let i = 0; i < line.length; i += 1) {\n const ch = line.charAt(i);\n if (quote !== null) {\n if (ch === '\\\\' && quote === '\"') i += 1;\n else if (ch === quote) quote = null;\n } else if (ch === '\"' || ch === \"'\") {\n quote = ch;\n } else if (ch === '#') {\n return line.slice(0, i);\n }\n }\n return line;\n}\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n if (trimmed.length >= 2 && (trimmed.startsWith('\"') || trimmed.startsWith(\"'\")) && trimmed.endsWith(trimmed.charAt(0))) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * The strings of a TOML array, `[\"a>=1\", 'b']`, possibly spread over lines.\n * A string inside an inline table is a value of that table, not an element:\n * `{include-group = \"test\"}` in a dependency group names a group, not a package.\n */\nfunction tomlStrings(text: string): string[] {\n const out: string[] = [];\n let depth = 0;\n let i = 0;\n while (i < text.length) {\n const ch = text.charAt(i);\n if (ch === '{') depth += 1;\n else if (ch === '}') depth -= 1;\n if (ch === '\"' || ch === \"'\") {\n let j = i + 1;\n let value = '';\n while (j < text.length && text.charAt(j) !== ch) {\n if (text.charAt(j) === '\\\\' && ch === '\"') {\n value += text.charAt(j + 1);\n j += 2;\n continue;\n }\n value += text.charAt(j);\n j += 1;\n }\n if (depth === 0) out.push(value);\n i = j + 1;\n continue;\n }\n i += 1;\n }\n return out;\n}\n\n/** The name a PEP 508 requirement names, normalised as PEP 503 says. */\nexport function pythonName(requirement: string): string {\n const match = /^\\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(requirement);\n return (match?.[1] ?? requirement.trim()).toLowerCase().replace(/[-_.]+/g, '-');\n}\n\ninterface TomlLine {\n readonly table: string;\n readonly key: string;\n readonly value: string;\n}\n\n/** Top-level `key = value` lines per table header, arrays gathered across lines. Enough for dependency tables. */\nfunction tomlEntries(text: string): TomlLine[] | string {\n const out: TomlLine[] = [];\n let table = '';\n const lines = text.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i += 1) {\n const line = stripTomlComment(lines[i] as string).trim();\n if (line === '') continue;\n const header = /^\\[\\[?\\s*([^\\]]+?)\\s*\\]\\]?$/.exec(line);\n if (header !== null) {\n table = (header[1] as string).replace(/\\s*\\.\\s*/g, '.');\n continue;\n }\n const eq = line.indexOf('=');\n if (eq <= 0) continue;\n const key = unquote(line.slice(0, eq));\n let value = line.slice(eq + 1).trim();\n if (value.startsWith('[') && !balanced(value)) {\n while (i + 1 < lines.length && !balanced(value)) {\n i += 1;\n value += ` ${stripTomlComment(lines[i] as string).trim()}`;\n }\n if (!balanced(value)) return `the array for \"${key}\" is never closed`;\n }\n out.push({ table, key, value });\n }\n return out;\n}\n\nfunction balanced(value: string): boolean {\n let depth = 0;\n let quote: string | null = null;\n for (let i = 0; i < value.length; i += 1) {\n const ch = value.charAt(i);\n if (quote !== null) {\n if (ch === '\\\\' && quote === '\"') i += 1;\n else if (ch === quote) quote = null;\n } else if (ch === '\"' || ch === \"'\") quote = ch;\n else if (ch === '[') depth += 1;\n else if (ch === ']') depth -= 1;\n }\n return depth <= 0 && quote === null;\n}\n\nconst CARGO_TABLE = /^(?:target\\..+\\.)?((?:workspace\\.)?(?:dev-|build-)?dependencies)(?:\\.(.+))?$/;\n\nfunction readCargo(text: string): ManifestRead {\n const entries = tomlEntries(text);\n if (typeof entries === 'string') return { ok: false, error: entries };\n const out = new Map<string, Dependency>();\n for (const { table, key, value } of entries) {\n const match = CARGO_TABLE.exec(table);\n if (match === null) continue;\n const section = match[1] as string;\n const inline = match[2];\n if (inline !== undefined) {\n // [dependencies.serde] then `version = \"1\"`: the table names the crate.\n const name = unquote(inline);\n const id = `${section}\\u0000${name}`;\n const current = out.get(id) ?? { section, name, version: '' };\n out.set(id, key === 'version' ? { ...current, version: unquote(value) } : current);\n continue;\n }\n const version = value.startsWith('{') ? (/version\\s*=\\s*(\"[^\"]*\"|'[^']*')/.exec(value)?.[1] ?? '') : value;\n out.set(`${section}\\u0000${key}`, { section, name: key, version: unquote(version) });\n }\n return { ok: true, dependencies: [...out.values()] };\n}\n\nfunction readPyproject(text: string): ManifestRead {\n const entries = tomlEntries(text);\n if (typeof entries === 'string') return { ok: false, error: entries };\n const out: Dependency[] = [];\n for (const { table, key, value } of entries) {\n if (table === 'project' && key === 'dependencies') {\n for (const requirement of tomlStrings(value)) out.push({ section: 'project', name: pythonName(requirement), version: requirement });\n } else if (table === 'project.optional-dependencies' || table === 'dependency-groups') {\n for (const requirement of tomlStrings(value)) out.push({ section: `${table}.${key}`, name: pythonName(requirement), version: requirement });\n } else if (/^tool\\.poetry\\.(?:group\\.[^.]+\\.)?(?:dev-)?dependencies$/.test(table) && key.toLowerCase() !== 'python') {\n out.push({ section: table, name: pythonName(key), version: unquote(value) });\n }\n }\n return { ok: true, dependencies: out };\n}\n\n/* ------------------------------------------------------------ line formats */\n\nfunction readGoMod(text: string): ManifestRead {\n const out: Dependency[] = [];\n let block = false;\n for (const raw of text.split(/\\r?\\n/)) {\n const indirect = /\\/\\/\\s*indirect\\b/.test(raw);\n const line = raw.replace(/\\/\\/.*$/, '').trim();\n if (line === '') continue;\n if (block) {\n if (line === ')') {\n block = false;\n continue;\n }\n const [name, version] = line.split(/\\s+/);\n if (name !== undefined) out.push({ section: indirect ? 'require (indirect)' : 'require', name, version: version ?? '' });\n continue;\n }\n if (/^require\\s*\\($/.test(line)) {\n block = true;\n continue;\n }\n const single = /^require\\s+(\\S+)\\s+(\\S+)/.exec(line);\n if (single !== null) out.push({ section: indirect ? 'require (indirect)' : 'require', name: single[1] as string, version: single[2] as string });\n }\n if (block) return { ok: false, error: 'a require block is never closed' };\n return { ok: true, dependencies: out };\n}\n\nfunction readRequirements(text: string): ManifestRead {\n const out: Dependency[] = [];\n for (const raw of text.split(/\\r?\\n/)) {\n const line = raw.replace(/(^|\\s)#.*$/, '').trim();\n if (line === '' || line.startsWith('-')) continue;\n out.push({ section: 'requirements', name: pythonName(line), version: line });\n }\n return { ok: true, dependencies: out };\n}\n\nfunction readNuget(text: string): ManifestRead {\n const out: Dependency[] = [];\n const element = /<(PackageReference|PackageVersion)\\b([^>]*?)\\/?>/g;\n for (let match = element.exec(text); match !== null; match = element.exec(text)) {\n const attributes = match[2] as string;\n const name = /\\bInclude\\s*=\\s*\"([^\"]*)\"/.exec(attributes)?.[1] ?? /\\bUpdate\\s*=\\s*\"([^\"]*)\"/.exec(attributes)?.[1];\n if (name === undefined) continue;\n const version = /\\bVersion\\s*=\\s*\"([^\"]*)\"/.exec(attributes)?.[1] ?? '';\n out.push({ section: match[1] as string, name, version });\n }\n return { ok: true, dependencies: out };\n}\n\nfunction readGemfile(text: string): ManifestRead {\n const out: Dependency[] = [];\n for (const raw of text.split(/\\r?\\n/)) {\n const match = /^\\s*gem\\s+([\"'])([^\"']+)\\1\\s*(?:,\\s*([\"'])([^\"']*)\\3)?/.exec(raw);\n if (match !== null) out.push({ section: 'gem', name: match[2] as string, version: match[4] ?? '' });\n }\n return { ok: true, dependencies: out };\n}\n\n/** Reads the dependencies a manifest declares. */\nexport function readManifest(ecosystem: Ecosystem, text: string): ManifestRead {\n switch (ecosystem) {\n case 'npm':\n return readNpm(text);\n case 'cargo':\n return readCargo(text);\n case 'go':\n return readGoMod(text);\n case 'python':\n return readPyproject(text);\n case 'pip':\n return readRequirements(text);\n case 'nuget':\n return readNuget(text);\n case 'bundler':\n return readGemfile(text);\n }\n}\n\n/**\n * What changed between two versions of one manifest. `null` text is a file\n * that does not exist on that side. Returns the error instead when either\n * side cannot be read.\n */\nexport function diffManifest(\n file: string,\n ecosystem: Ecosystem,\n before: string | null,\n after: string | null,\n): { readonly changes: readonly DependencyChange[] } | { readonly error: string } {\n const read = (text: string | null, side: string): readonly Dependency[] | string => {\n if (text === null) return [];\n const result = readManifest(ecosystem, text);\n return result.ok ? result.dependencies : `${file} (${side}): ${result.error}`;\n };\n const old = read(before, 'before');\n if (typeof old === 'string') return { error: old };\n const now = read(after, 'after');\n if (typeof now === 'string') return { error: now };\n const key = (d: Dependency): string => `${d.section}\\u0000${d.name}`;\n const was = new Map(old.map((d) => [key(d), d]));\n const is = new Map(now.map((d) => [key(d), d]));\n const changes: DependencyChange[] = [];\n for (const [id, dependency] of is) {\n const previous = was.get(id);\n if (previous === undefined || previous.version !== dependency.version) {\n changes.push({ file, ecosystem, section: dependency.section, name: dependency.name, before: previous?.version ?? null, after: dependency.version });\n }\n }\n for (const [id, dependency] of was) {\n if (!is.has(id)) changes.push({ file, ecosystem, section: dependency.section, name: dependency.name, before: dependency.version, after: null });\n }\n return { changes };\n}\n"]}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spec-brief plugin: signed rulings, read where spec-brief decides.
|
|
3
|
+
*
|
|
4
|
+
* spec-brief's archive refuses a round that changed a file its brief protects.
|
|
5
|
+
* When a person has ruled that the round may change it, and signed the ruling
|
|
6
|
+
* (ADR-0006), the refusal is lifted - but spec-brief does not read signatures,
|
|
7
|
+
* and should not: the tool that defines a format ships the check for it
|
|
8
|
+
* (spec-brief ADR-0007). So the check ships here, and spec-brief asks it
|
|
9
|
+
* through its plugin `waive` hook:
|
|
10
|
+
*
|
|
11
|
+
* ```json
|
|
12
|
+
* { "plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"] }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* It waives a `protected-file` refusal for a path a verified `allow` ruling
|
|
16
|
+
* names, and nothing else. A ruling that does not verify waives nothing.
|
|
17
|
+
*/
|
|
18
|
+
export interface WaiveContext {
|
|
19
|
+
readonly root: string;
|
|
20
|
+
/** spec-brief passes a `null` id for a brief it could not name; its file still identifies it. */
|
|
21
|
+
readonly brief: {
|
|
22
|
+
readonly id: string | null;
|
|
23
|
+
readonly file: string;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
};
|
|
26
|
+
readonly findings: readonly {
|
|
27
|
+
readonly rule: string;
|
|
28
|
+
readonly path?: string | undefined;
|
|
29
|
+
readonly paths?: readonly string[] | undefined;
|
|
30
|
+
}[];
|
|
31
|
+
readonly base: string | null;
|
|
32
|
+
readonly commit: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface Waiver {
|
|
35
|
+
readonly rule: string;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function waive(context: WaiveContext): Promise<Waiver[]>;
|
|
40
|
+
/** The plugin, as spec-brief loads it: a function of its options returning `{ name, rules, waive }`. */
|
|
41
|
+
export default function specHarnessPlugin(): {
|
|
42
|
+
name: string;
|
|
43
|
+
rules: readonly never[];
|
|
44
|
+
waive: typeof waive;
|
|
45
|
+
};
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spec-brief plugin: signed rulings, read where spec-brief decides.
|
|
3
|
+
*
|
|
4
|
+
* spec-brief's archive refuses a round that changed a file its brief protects.
|
|
5
|
+
* When a person has ruled that the round may change it, and signed the ruling
|
|
6
|
+
* (ADR-0006), the refusal is lifted - but spec-brief does not read signatures,
|
|
7
|
+
* and should not: the tool that defines a format ships the check for it
|
|
8
|
+
* (spec-brief ADR-0007). So the check ships here, and spec-brief asks it
|
|
9
|
+
* through its plugin `waive` hook:
|
|
10
|
+
*
|
|
11
|
+
* ```json
|
|
12
|
+
* { "plugins": ["@descent-vtt/spec-harness/spec-brief-plugin"] }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* It waives a `protected-file` refusal for a path a verified `allow` ruling
|
|
16
|
+
* names, and nothing else. A ruling that does not verify waives nothing.
|
|
17
|
+
*/
|
|
18
|
+
import { loadConfig } from './fs.js';
|
|
19
|
+
import { commonDirectory, currentBranch } from './git.js';
|
|
20
|
+
import { createReader } from './reader.js';
|
|
21
|
+
import { checkRulings, resolveBase } from './round.js';
|
|
22
|
+
import { createSiblings } from './siblings.js';
|
|
23
|
+
import { parseGlob } from './vendor/spec-core/pattern/index.js';
|
|
24
|
+
function covers(ruling, path) {
|
|
25
|
+
return ruling.paths.some((pattern) => {
|
|
26
|
+
const parsed = parseGlob(pattern, { dialect: 'path', caseSensitive: true, literal: 'either' });
|
|
27
|
+
return parsed.ok && parsed.glob.match(path);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Paths a finding names, in either of the shapes spec-brief reports them. */
|
|
31
|
+
function pathsOf(finding) {
|
|
32
|
+
if (finding.path !== undefined)
|
|
33
|
+
return [finding.path];
|
|
34
|
+
return [...(finding.paths ?? [])];
|
|
35
|
+
}
|
|
36
|
+
export async function waive(context) {
|
|
37
|
+
const protectedPaths = context.findings.filter((finding) => finding.rule === 'protected-file').flatMap(pathsOf);
|
|
38
|
+
if (protectedPaths.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const { config } = await loadConfig(context.root);
|
|
41
|
+
const workspace = {
|
|
42
|
+
root: context.root,
|
|
43
|
+
commonDir: (await commonDirectory(context.root)) ?? `${context.root}/.git`,
|
|
44
|
+
config,
|
|
45
|
+
siblings: createSiblings(context.root, config),
|
|
46
|
+
branch: await currentBranch(context.root),
|
|
47
|
+
cwd: context.root,
|
|
48
|
+
};
|
|
49
|
+
const base = await resolveBase(workspace, context.base ?? undefined);
|
|
50
|
+
const brief = {
|
|
51
|
+
id: context.brief.id ?? context.brief.file,
|
|
52
|
+
file: context.brief.file,
|
|
53
|
+
title: null,
|
|
54
|
+
phase: 'live',
|
|
55
|
+
status: null,
|
|
56
|
+
type: null,
|
|
57
|
+
wave: null,
|
|
58
|
+
dependsOn: [],
|
|
59
|
+
affectedFiles: [],
|
|
60
|
+
protectedFiles: [],
|
|
61
|
+
tasks: { total: 0, checked: 0 },
|
|
62
|
+
ready: false,
|
|
63
|
+
waitingOn: [],
|
|
64
|
+
};
|
|
65
|
+
const { verified } = await checkRulings(workspace, brief, context.brief.text, createReader(), base);
|
|
66
|
+
const waivers = [];
|
|
67
|
+
for (const path of protectedPaths) {
|
|
68
|
+
const ruling = verified.find((candidate) => covers(candidate, path));
|
|
69
|
+
if (ruling !== undefined) {
|
|
70
|
+
waivers.push({ rule: 'protected-file', path, reason: `ruling ${ruling.id}, signed by ${ruling.signer}, allows it` });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return waivers;
|
|
74
|
+
}
|
|
75
|
+
/** The plugin, as spec-brief loads it: a function of its options returning `{ name, rules, waive }`. */
|
|
76
|
+
export default function specHarnessPlugin() {
|
|
77
|
+
return { name: 'spec-harness', rules: [], waive };
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The spec-brief plugin: signed rulings, read where spec-brief decides.\n *\n * spec-brief's archive refuses a round that changed a file its brief protects.\n * When a person has ruled that the round may change it, and signed the ruling\n * (ADR-0006), the refusal is lifted - but spec-brief does not read signatures,\n * and should not: the tool that defines a format ships the check for it\n * (spec-brief ADR-0007). So the check ships here, and spec-brief asks it\n * through its plugin `waive` hook:\n *\n * ```json\n * { \"plugins\": [\"@descent-vtt/spec-harness/spec-brief-plugin\"] }\n * ```\n *\n * It waives a `protected-file` refusal for a path a verified `allow` ruling\n * names, and nothing else. A ruling that does not verify waives nothing.\n */\n\nimport { loadConfig } from './fs.js';\nimport { commonDirectory, currentBranch } from './git.js';\nimport type { VerifiedRuling } from './guard.js';\nimport { createReader } from './reader.js';\nimport { checkRulings, resolveBase } from './round.js';\nimport { createSiblings } from './siblings.js';\nimport type { BriefRow } from './types.js';\nimport { parseGlob } from './vendor/spec-core/pattern/index.js';\nimport type { Workspace } from './workspace.js';\n\nexport interface WaiveContext {\n readonly root: string;\n /** spec-brief passes a `null` id for a brief it could not name; its file still identifies it. */\n readonly brief: { readonly id: string | null; readonly file: string; readonly text: string };\n readonly findings: readonly { readonly rule: string; readonly path?: string | undefined; readonly paths?: readonly string[] | undefined }[];\n readonly base: string | null;\n readonly commit: string | null;\n}\n\nexport interface Waiver {\n readonly rule: string;\n readonly path: string;\n readonly reason: string;\n}\n\nfunction covers(ruling: VerifiedRuling, path: string): boolean {\n return ruling.paths.some((pattern) => {\n const parsed = parseGlob(pattern, { dialect: 'path', caseSensitive: true, literal: 'either' });\n return parsed.ok && parsed.glob.match(path);\n });\n}\n\n/** Paths a finding names, in either of the shapes spec-brief reports them. */\nfunction pathsOf(finding: WaiveContext['findings'][number]): string[] {\n if (finding.path !== undefined) return [finding.path];\n return [...(finding.paths ?? [])];\n}\n\nexport async function waive(context: WaiveContext): Promise<Waiver[]> {\n const protectedPaths = context.findings.filter((finding) => finding.rule === 'protected-file').flatMap(pathsOf);\n if (protectedPaths.length === 0) return [];\n const { config } = await loadConfig(context.root);\n const workspace: Workspace = {\n root: context.root,\n commonDir: (await commonDirectory(context.root)) ?? `${context.root}/.git`,\n config,\n siblings: createSiblings(context.root, config),\n branch: await currentBranch(context.root),\n cwd: context.root,\n };\n const base = await resolveBase(workspace, context.base ?? undefined);\n const brief: BriefRow = {\n id: context.brief.id ?? context.brief.file,\n file: context.brief.file,\n title: null,\n phase: 'live',\n status: null,\n type: null,\n wave: null,\n dependsOn: [],\n affectedFiles: [],\n protectedFiles: [],\n tasks: { total: 0, checked: 0 },\n ready: false,\n waitingOn: [],\n };\n const { verified } = await checkRulings(workspace, brief, context.brief.text, createReader(), base);\n const waivers: Waiver[] = [];\n for (const path of protectedPaths) {\n const ruling = verified.find((candidate) => covers(candidate, path));\n if (ruling !== undefined) {\n waivers.push({ rule: 'protected-file', path, reason: `ruling ${ruling.id}, signed by ${ruling.signer}, allows it` });\n }\n }\n return waivers;\n}\n\n/** The plugin, as spec-brief loads it: a function of its options returning `{ name, rules, waive }`. */\nexport default function specHarnessPlugin(): { name: string; rules: readonly never[]; waive: typeof waive } {\n return { name: 'spec-harness', rules: [], waive };\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec-harness premises`: is every live brief still about something true?
|
|
3
|
+
*
|
|
4
|
+
* A brief written against a defect states the defect as a premise - an
|
|
5
|
+
* assertion under a section such as "The Defect, Measured" that holds while
|
|
6
|
+
* the defect does. When the premise stops holding before the round has run,
|
|
7
|
+
* the brief is stale: someone fixed the defect another way, or the code it
|
|
8
|
+
* describes is gone. A repository that keeps its premises executable can say
|
|
9
|
+
* so on every build, and ask for the brief to be archived or rewritten before
|
|
10
|
+
* an agent is sent to fix what is already fixed.
|
|
11
|
+
*
|
|
12
|
+
* Goals are the audit's (`audit`), not this: a goal fails until its round is
|
|
13
|
+
* done, and a build that failed for that would fail for every brief in flight.
|
|
14
|
+
*/
|
|
15
|
+
import { type CliIO, type Options } from './workspace.js';
|
|
16
|
+
export declare function premisesCommand(options: Options, io: CliIO): Promise<number>;
|