qa-engineer 0.9.1 → 0.10.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/COMPATIBILITY.md +50 -26
- package/README.md +40 -32
- package/package.json +12 -20
- package/packages/engine/bin/qa-engine.mjs +336 -0
- package/packages/engine/lib/analysis/branding.mjs +187 -0
- package/packages/engine/lib/analysis/context.mjs +294 -0
- package/packages/{installer/lib/core/schema-validate.mjs → engine/lib/analysis/contracts.mjs} +11 -4
- package/packages/engine/lib/analysis/diff-guard.mjs +425 -0
- package/packages/engine/lib/analysis/discovery.mjs +220 -0
- package/packages/engine/lib/analysis/evidence.mjs +116 -0
- package/packages/engine/lib/analysis/har.mjs +90 -0
- package/packages/engine/lib/analysis/junit.mjs +126 -0
- package/packages/engine/lib/analysis/redaction.mjs +127 -0
- package/packages/engine/lib/analysis/report-html.mjs +763 -0
- package/packages/engine/lib/analysis/taxonomy.mjs +109 -0
- package/packages/engine/lib/analysis/xml.mjs +153 -0
- package/packages/engine/lib/analysis/zip.mjs +107 -0
- package/packages/engine/lib/diagnostics/engine.mjs +165 -0
- package/packages/engine/lib/diagnostics/internal-contracts.mjs +50 -0
- package/packages/engine/lib/diagnostics/prioritization.mjs +99 -0
- package/packages/engine/lib/diagnostics/repair.mjs +73 -0
- package/packages/engine/lib/diagnostics/root-cause.mjs +92 -0
- package/packages/engine/lib/diagnostics/timeline.mjs +101 -0
- package/packages/engine/lib/frameworks/junit-frameworks.mjs +48 -0
- package/packages/engine/lib/frameworks/playwright.mjs +158 -0
- package/packages/engine/package.json +14 -0
- package/packages/installer/lib/agents/registry.mjs +46 -0
- package/packages/installer/lib/commands/doctor.mjs +7 -16
- package/packages/installer/lib/commands/install.mjs +59 -4
- package/packages/installer/lib/core/bundle.mjs +69 -92
- package/packages/installer/lib/core/config.mjs +1 -1
- package/packages/installer/lib/core/lockfile.mjs +1 -1
- package/packages/installer/lib/core/manifest.mjs +48 -54
- package/packages/installer/lib/core/validate-install.mjs +12 -19
- package/packages/installer/package.json +1 -1
- package/shared/tooling/qa-tool.mjs +125 -0
- package/skills/qa-api/SKILL.md +5 -5
- package/skills/qa-api/references/deterministic-tooling.md +34 -30
- package/skills/qa-api/references/evidence-and-reporting.md +29 -4
- package/skills/qa-api/scripts/qa-tool.mjs +126 -0
- package/skills/qa-audit/SKILL.md +5 -5
- package/skills/qa-audit/references/deterministic-tooling.md +34 -30
- package/skills/qa-audit/references/evidence-and-reporting.md +29 -4
- package/skills/qa-audit/scripts/qa-tool.mjs +126 -0
- package/skills/qa-debug/SKILL.md +6 -6
- package/skills/qa-debug/references/deterministic-tooling.md +34 -30
- package/skills/qa-debug/references/diagnostic-engine.md +3 -3
- package/skills/qa-debug/references/evidence-and-reporting.md +29 -4
- package/skills/qa-debug/scripts/qa-tool.mjs +126 -0
- package/skills/qa-explore/SKILL.md +22 -5
- package/skills/qa-explore/contracts/explore-result.schema.json +26 -2
- package/skills/qa-explore/references/deterministic-tooling.md +132 -0
- package/skills/qa-explore/references/evidence-and-reporting.md +29 -4
- package/skills/qa-explore/references/report-pipeline.md +89 -8
- package/skills/qa-explore/scripts/qa-tool.mjs +126 -0
- package/skills/qa-fix/SKILL.md +4 -4
- package/skills/qa-fix/references/deterministic-tooling.md +34 -30
- package/skills/qa-fix/references/diagnostic-engine.md +3 -3
- package/skills/qa-fix/references/evidence-and-reporting.md +29 -4
- package/skills/qa-fix/scripts/qa-tool.mjs +126 -0
- package/skills/qa-flaky/SKILL.md +4 -4
- package/skills/qa-flaky/references/deterministic-tooling.md +34 -30
- package/skills/qa-flaky/references/evidence-and-reporting.md +29 -4
- package/skills/qa-flaky/scripts/qa-tool.mjs +126 -0
- package/skills/qa-generate/references/evidence-and-reporting.md +29 -4
- package/skills/qa-init/SKILL.md +4 -4
- package/skills/qa-init/references/deterministic-tooling.md +34 -30
- package/skills/qa-init/references/evidence-and-reporting.md +29 -4
- package/skills/qa-init/scripts/qa-tool.mjs +126 -0
- package/skills/qa-report/SKILL.md +8 -7
- package/skills/qa-report/references/deterministic-tooling.md +34 -30
- package/skills/qa-report/references/diagnostic-engine.md +3 -3
- package/skills/qa-report/references/evidence-and-reporting.md +29 -4
- package/skills/qa-report/scripts/qa-tool.mjs +126 -0
- package/skills/qa-review/references/evidence-and-reporting.md +29 -4
- package/skills/qa-run/SKILL.md +6 -6
- package/skills/qa-run/references/deterministic-tooling.md +34 -30
- package/skills/qa-run/references/evidence-and-reporting.md +29 -4
- package/skills/qa-run/scripts/qa-tool.mjs +126 -0
- package/shared/analysis/lib/qa_analysis/__init__.py +0 -11
- package/shared/analysis/lib/qa_analysis/branding.py +0 -175
- package/shared/analysis/lib/qa_analysis/cli.py +0 -129
- package/shared/analysis/lib/qa_analysis/context.py +0 -233
- package/shared/analysis/lib/qa_analysis/contracts.py +0 -158
- package/shared/analysis/lib/qa_analysis/diff_guard.py +0 -327
- package/shared/analysis/lib/qa_analysis/discovery.py +0 -113
- package/shared/analysis/lib/qa_analysis/evidence.py +0 -128
- package/shared/analysis/lib/qa_analysis/har.py +0 -58
- package/shared/analysis/lib/qa_analysis/junit.py +0 -80
- package/shared/analysis/lib/qa_analysis/redaction.py +0 -99
- package/shared/analysis/lib/qa_analysis/taxonomy.py +0 -98
- package/shared/diagnostics/lib/qa_diagnostics/__init__.py +0 -13
- package/shared/diagnostics/lib/qa_diagnostics/cli.py +0 -124
- package/shared/diagnostics/lib/qa_diagnostics/engine.py +0 -143
- package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +0 -75
- package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +0 -101
- package/shared/diagnostics/lib/qa_diagnostics/repair.py +0 -72
- package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +0 -89
- package/shared/diagnostics/lib/qa_diagnostics/timeline.py +0 -71
- package/shared/frameworks/cypress/lib/cypress_analysis.py +0 -27
- package/shared/frameworks/playwright/lib/playwright_analysis.py +0 -167
- package/shared/frameworks/selenium/lib/selenium_analysis.py +0 -28
- package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +0 -24
- package/shared/tooling/qa_tool.py +0 -127
- /package/{shared/analysis/lib/qa_analysis → packages/engine/lib/analysis}/branding.json +0 -0
- /package/{shared → packages/engine/lib}/analysis/schemas/context.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/analysis-result.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/diagnosis.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/execution-result-min.schema.json +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Deterministic parsing of `.qa/context.md`.
|
|
2
|
+
//
|
|
3
|
+
// `qa-init` writes the project profile as a Markdown file whose frontmatter holds
|
|
4
|
+
// the machine-readable facts, and every other skill reads it. The contract for
|
|
5
|
+
// those facts is a JSON Schema (shared/analysis/schemas/context.schema.json) — but
|
|
6
|
+
// nothing could check a *real* `.qa/context.md` against it, because the engine
|
|
7
|
+
// takes no dependencies and the frontmatter is YAML. Validation ran against a
|
|
8
|
+
// hand-written JSON fixture instead, so the contract was unenforced exactly where
|
|
9
|
+
// it mattered.
|
|
10
|
+
//
|
|
11
|
+
// This closes that gap without adding a dependency or changing the file format: it
|
|
12
|
+
// parses the **explicit subset** of YAML the context contract uses, then hands the
|
|
13
|
+
// result to the contract validator.
|
|
14
|
+
//
|
|
15
|
+
// ## The supported subset
|
|
16
|
+
//
|
|
17
|
+
// Deliberately small, and everything outside it is an error rather than a guess:
|
|
18
|
+
//
|
|
19
|
+
// - `key: value` mappings, nested by two-space indentation
|
|
20
|
+
// - block sequences (`- item`), including nested under a key
|
|
21
|
+
// - flow collections only when empty: `[]` and `{}`
|
|
22
|
+
// - scalars: double- or single-quoted strings, bare strings, integers, floats,
|
|
23
|
+
// `true`/`false`, `null`/`~`/empty
|
|
24
|
+
// - `#` comments on their own line or after a value
|
|
25
|
+
// - the leading/trailing `---` fences
|
|
26
|
+
//
|
|
27
|
+
// Not supported, and rejected loudly: anchors/aliases, multi-line block scalars
|
|
28
|
+
// (`|`, `>`), non-empty flow collections, multi-document streams, and tabs for
|
|
29
|
+
// indentation. A generator that needs one of those has outgrown the contract, and
|
|
30
|
+
// the right response is to change the contract deliberately — not to have a parser
|
|
31
|
+
// quietly misread it.
|
|
32
|
+
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
|
|
35
|
+
import { validate } from './contracts.mjs';
|
|
36
|
+
|
|
37
|
+
const FENCE = '---';
|
|
38
|
+
const TRUE = new Set(['true', 'True', 'TRUE']);
|
|
39
|
+
const FALSE = new Set(['false', 'False', 'FALSE']);
|
|
40
|
+
const NULL = new Set(['null', 'Null', 'NULL', '~', '']);
|
|
41
|
+
const UNSUPPORTED_SCALARS = new Set(['|', '>']);
|
|
42
|
+
const INT = /^-?\d+$/;
|
|
43
|
+
const FLOAT = /^-?\d+\.\d+$/;
|
|
44
|
+
|
|
45
|
+
export class MalformedContext extends Error {}
|
|
46
|
+
|
|
47
|
+
/** Return `{frontmatter, body}`. Throws if the fences are missing. */
|
|
48
|
+
export function splitFrontmatter(text) {
|
|
49
|
+
const lines = String(text).split('\n');
|
|
50
|
+
let start = null;
|
|
51
|
+
for (const [index, line] of lines.entries()) {
|
|
52
|
+
if (line.trim() === '') continue;
|
|
53
|
+
if (trimEnd(line) === FENCE) start = index;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (start === null) {
|
|
57
|
+
throw new MalformedContext(
|
|
58
|
+
"no frontmatter: the file must open with a '---' fence " +
|
|
59
|
+
'(see the qa-init context template)',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
63
|
+
if (trimEnd(lines[index]) === FENCE) {
|
|
64
|
+
return {
|
|
65
|
+
frontmatter: lines.slice(start + 1, index).join('\n'),
|
|
66
|
+
body: lines.slice(index + 1).join('\n'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new MalformedContext("unterminated frontmatter: no closing '---' fence");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Remove a trailing `#` comment that is not inside quotes. */
|
|
74
|
+
function stripComment(value) {
|
|
75
|
+
let quote = null;
|
|
76
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
77
|
+
const char = value[index];
|
|
78
|
+
if (quote) {
|
|
79
|
+
if (char === quote) quote = null;
|
|
80
|
+
} else if (char === "'" || char === '"') {
|
|
81
|
+
quote = char;
|
|
82
|
+
} else if (char === '#' && (index === 0 || value[index - 1] === ' ' || value[index - 1] === '\t')) {
|
|
83
|
+
return value.slice(0, index);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scalar(raw, lineNumber) {
|
|
90
|
+
const text = stripComment(raw).trim();
|
|
91
|
+
const first = text.slice(0, 1);
|
|
92
|
+
if (UNSUPPORTED_SCALARS.has(first)) {
|
|
93
|
+
throw new MalformedContext(
|
|
94
|
+
`line ${lineNumber}: block scalars ('|', '>') are outside the supported subset`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (first === '&' || first === '*') {
|
|
98
|
+
throw new MalformedContext(
|
|
99
|
+
`line ${lineNumber}: anchors and aliases are outside the supported subset`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (text.length >= 2 && text[0] === text[text.length - 1] && (text[0] === "'" || text[0] === '"')) {
|
|
103
|
+
return text.slice(1, -1);
|
|
104
|
+
}
|
|
105
|
+
if (text === '[]') return [];
|
|
106
|
+
if (text === '{}') return {};
|
|
107
|
+
if (text.startsWith('[') || text.startsWith('{')) {
|
|
108
|
+
throw new MalformedContext(
|
|
109
|
+
`line ${lineNumber}: only empty flow collections ('[]', '{}') are supported; ` +
|
|
110
|
+
'use a block sequence or mapping',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (TRUE.has(text)) return true;
|
|
114
|
+
if (FALSE.has(text)) return false;
|
|
115
|
+
if (NULL.has(text)) return null;
|
|
116
|
+
if (INT.test(text)) return Number.parseInt(text, 10);
|
|
117
|
+
if (FLOAT.test(text)) return Number.parseFloat(text);
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A line must sit at exactly the indent of the container it lands in.
|
|
123
|
+
*
|
|
124
|
+
* Without this, a document YAML itself rejects was silently misread. Given
|
|
125
|
+
*
|
|
126
|
+
* list:
|
|
127
|
+
* - one
|
|
128
|
+
* key: inside a sequence
|
|
129
|
+
*
|
|
130
|
+
* PyYAML reports "expected <block end>, but found '?'" — a mapping key cannot
|
|
131
|
+
* share indentation with a sequence entry in the same block. The parser closed the
|
|
132
|
+
* sequence and put `key` in the *root* mapping, two levels out from where it was
|
|
133
|
+
* written, turning an invalid file into a plausible one.
|
|
134
|
+
*/
|
|
135
|
+
function requireIndentMatches(stack, indent, lineNumber) {
|
|
136
|
+
if (stack[stack.length - 1][0] !== indent) {
|
|
137
|
+
throw new MalformedContext(
|
|
138
|
+
`line ${lineNumber}: indentation ${indent} matches no open block ` +
|
|
139
|
+
`(the enclosing block starts at column ${stack[stack.length - 1][0]})`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse the supported YAML subset into an object.
|
|
146
|
+
*
|
|
147
|
+
* The parser keeps a stack of `[keyIndent, container]` frames. `keyIndent` is the
|
|
148
|
+
* column at which that container's own entries start, so a line's indent alone
|
|
149
|
+
* decides which container it belongs to: pop every frame deeper than the line, and
|
|
150
|
+
* the top of the stack is the target.
|
|
151
|
+
*/
|
|
152
|
+
export function parseFrontmatter(text) {
|
|
153
|
+
const root = {};
|
|
154
|
+
const stack = [[0, root]];
|
|
155
|
+
let pending = null; // [key, indent] — a key whose value is a block that follows
|
|
156
|
+
|
|
157
|
+
const resolvePendingAsNull = () => {
|
|
158
|
+
// A key with no value and no nested block is a null field.
|
|
159
|
+
if (pending !== null) {
|
|
160
|
+
stack[stack.length - 1][1][pending[0]] = null;
|
|
161
|
+
pending = null;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const lines = String(text).split('\n');
|
|
166
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
167
|
+
const rawLine = lines[index];
|
|
168
|
+
const number = index + 1;
|
|
169
|
+
const leading = rawLine.slice(0, rawLine.length - trimStart(rawLine).length);
|
|
170
|
+
if (leading.includes('\t')) {
|
|
171
|
+
throw new MalformedContext(`line ${number}: tab indentation is not supported`);
|
|
172
|
+
}
|
|
173
|
+
const stripped = rawLine.trim();
|
|
174
|
+
if (!stripped || stripped.startsWith('#')) continue;
|
|
175
|
+
if (stripped === FENCE) {
|
|
176
|
+
throw new MalformedContext(`line ${number}: unexpected '---' inside frontmatter`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const indent = rawLine.length - trimStartSpaces(rawLine).length;
|
|
180
|
+
|
|
181
|
+
// --- sequence item ---
|
|
182
|
+
if (stripped.startsWith('- ') || stripped === '-') {
|
|
183
|
+
const item = scalar(stripped.length > 1 ? stripped.slice(2) : '', number);
|
|
184
|
+
if (pending !== null) {
|
|
185
|
+
// A sequence may sit at the key's indent or deeper.
|
|
186
|
+
const parent = stack[stack.length - 1][1];
|
|
187
|
+
parent[pending[0]] = [];
|
|
188
|
+
stack.push([indent, parent[pending[0]]]);
|
|
189
|
+
pending = null;
|
|
190
|
+
}
|
|
191
|
+
while (stack.length > 1 && stack[stack.length - 1][0] > indent) stack.pop();
|
|
192
|
+
const container = stack[stack.length - 1][1];
|
|
193
|
+
if (!Array.isArray(container)) {
|
|
194
|
+
throw new MalformedContext(`line ${number}: sequence item outside a sequence`);
|
|
195
|
+
}
|
|
196
|
+
requireIndentMatches(stack, indent, number);
|
|
197
|
+
container.push(item);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- mapping entry ---
|
|
202
|
+
if (!stripped.includes(':')) {
|
|
203
|
+
throw new MalformedContext(
|
|
204
|
+
`line ${number}: expected 'key: value' or '- item', got '${stripped}'`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (pending !== null) {
|
|
209
|
+
if (indent > pending[1]) {
|
|
210
|
+
// The pending key opens a nested mapping at this indent.
|
|
211
|
+
const parent = stack[stack.length - 1][1];
|
|
212
|
+
parent[pending[0]] = {};
|
|
213
|
+
stack.push([indent, parent[pending[0]]]);
|
|
214
|
+
pending = null;
|
|
215
|
+
} else {
|
|
216
|
+
resolvePendingAsNull();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Leave the frames this line does not belong to. A sequence frame at the same
|
|
221
|
+
// indent as a key also ends here (`packages:` / `- a` / `ci:`).
|
|
222
|
+
while (
|
|
223
|
+
stack.length > 1 &&
|
|
224
|
+
(stack[stack.length - 1][0] > indent ||
|
|
225
|
+
(Array.isArray(stack[stack.length - 1][1]) && stack[stack.length - 1][0] >= indent))
|
|
226
|
+
) {
|
|
227
|
+
stack.pop();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const separator = stripped.indexOf(':');
|
|
231
|
+
const key = stripped.slice(0, separator).trim();
|
|
232
|
+
const value = stripped.slice(separator + 1);
|
|
233
|
+
if (!key) throw new MalformedContext(`line ${number}: empty key`);
|
|
234
|
+
const container = stack[stack.length - 1][1];
|
|
235
|
+
if (Array.isArray(container)) {
|
|
236
|
+
throw new MalformedContext(`line ${number}: mapping key inside a sequence`);
|
|
237
|
+
}
|
|
238
|
+
requireIndentMatches(stack, indent, number);
|
|
239
|
+
|
|
240
|
+
if (stripComment(value).trim() === '') {
|
|
241
|
+
pending = [key, indent]; // a block or a null follows
|
|
242
|
+
} else {
|
|
243
|
+
container[key] = scalar(value, number);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
resolvePendingAsNull();
|
|
248
|
+
return root;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Parse `.qa/context.md` text and, when a schema is given, validate it.
|
|
253
|
+
*
|
|
254
|
+
* A parse failure throws: it is not a validation result. "This file is not
|
|
255
|
+
* parseable" and "this file is parseable but wrong" are different answers, and
|
|
256
|
+
* collapsing them would report a broken file as a merely invalid one.
|
|
257
|
+
*/
|
|
258
|
+
export function parse(text, { schema = null } = {}) {
|
|
259
|
+
const { frontmatter, body } = splitFrontmatter(text);
|
|
260
|
+
const context = parseFrontmatter(frontmatter);
|
|
261
|
+
const result = { context, body, valid: true, errors: [] };
|
|
262
|
+
if (schema !== null) {
|
|
263
|
+
const errors = validate(context, schema);
|
|
264
|
+
result.valid = errors.length === 0;
|
|
265
|
+
result.errors = errors;
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Read and parse a `.qa/context.md` file. */
|
|
271
|
+
export function parseFile(path, { schema = null } = {}) {
|
|
272
|
+
let text;
|
|
273
|
+
try {
|
|
274
|
+
text = fs.readFileSync(path, 'utf8');
|
|
275
|
+
} catch (error) {
|
|
276
|
+
throw new MalformedContext(`could not read ${path}: ${error.message}`);
|
|
277
|
+
}
|
|
278
|
+
return parse(text, { schema });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Python's str.rstrip()/lstrip() strip all whitespace; JavaScript's trimEnd and
|
|
282
|
+
// trimStart do the same, but the indent calculation needs spaces only — a line
|
|
283
|
+
// indented with a form feed is not indented two columns.
|
|
284
|
+
function trimEnd(value) {
|
|
285
|
+
return value.replace(/\s+$/, '');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function trimStart(value) {
|
|
289
|
+
return value.replace(/^\s+/, '');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function trimStartSpaces(value) {
|
|
293
|
+
return value.replace(/^ +/, '');
|
|
294
|
+
}
|
package/packages/{installer/lib/core/schema-validate.mjs → engine/lib/analysis/contracts.mjs}
RENAMED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// A dependency-free validator for the JSON Schema subset this project uses.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
//
|
|
3
|
+
// This began as the JavaScript *twin* of a Python validator, kept in step by a
|
|
4
|
+
// shared corpus. The move to a Node-only engine (ADR-0012) made it the single
|
|
5
|
+
// implementation: the installer's config and lockfile checks and the analysis
|
|
6
|
+
// engine's contract checks now go through this one function, rather than two
|
|
7
|
+
// copies that agreed by test.
|
|
8
|
+
//
|
|
9
|
+
// The corpus outlived the twin. packages/engine/test/corpus/validator-cases.json
|
|
10
|
+
// still runs every case here, because it encodes which subset of JSON Schema the
|
|
11
|
+
// pack's contracts may use — and that is worth pinning whether or not a second
|
|
12
|
+
// validator exists to compare against.
|
|
6
13
|
//
|
|
7
14
|
// Anything outside the subset is a programming error in the schema, reported
|
|
8
15
|
// rather than silently ignored. `allOf`/`if`/`then`/`else` are in the subset
|