@boyingliu01/xp-gate 0.27.6 → 0.28.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/adapter-common.sh +0 -0
- package/adapters/cpp.sh +0 -0
- package/adapters/dart.sh +0 -0
- package/adapters/flutter.sh +0 -0
- package/adapters/gate-9.sh +13 -2
- package/adapters/go.sh +0 -0
- package/adapters/iac.sh +0 -0
- package/adapters/java.sh +0 -0
- package/adapters/kotlin.sh +0 -0
- package/adapters/objectivec.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
- package/adapters/powershell.sh +0 -0
- package/adapters/python.sh +0 -0
- package/adapters/shell.sh +0 -0
- package/adapters/swift.sh +0 -0
- package/adapters/typescript.sh +0 -0
- package/bin/xp-gate.js +0 -0
- package/build-integrity/gate-10.ts +290 -263
- package/gate-10.sh +25 -6
- package/gate-12-file-hygiene.sh +35 -1
- package/gate-3.sh +78 -2
- package/gate-4.sh +0 -0
- package/gate-7.sh +0 -0
- package/gate-8.sh +0 -0
- package/gate-9.sh +13 -2
- package/hooks/adapter-common.sh +0 -0
- package/hooks/gate-10.sh +25 -6
- package/hooks/gate-12-file-hygiene.sh +35 -1
- package/hooks/gate-3.sh +78 -2
- package/hooks/gate-4.sh +0 -0
- package/hooks/gate-7.sh +0 -0
- package/hooks/gate-8.sh +0 -0
- package/hooks/gate-9.sh +13 -2
- package/hooks/lib/audit-sanitize.sh +51 -0
- package/hooks/lib/ccn-ratchet.sh +183 -0
- package/hooks/lib/docs-only.sh +49 -0
- package/hooks/post-merge +0 -0
- package/hooks/pre-commit +74 -15
- package/hooks/pre-push +36 -5
- package/hooks/sprint-gate.sh +0 -0
- package/lib/__tests__/sprint-audit.test.js +207 -207
- package/lib/gate-audit.ts +9 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/retro.js +27 -6
- package/lib/sprint-audit.js +235 -235
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
- package/plugins/claude-code/bin/xp-gate-check +0 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
- package/plugins/qoder/bin/xp-gate-check +0 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/many-exports-reexport.test.ts +40 -0
- package/principles/adapters/typescript.ts +61 -30
- package/principles/boy-scout.ts +22 -17
- package/scripts/delphi-external-review.cjs +195 -132
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +0 -0
- package/plugins/dsh/lib/command.d.ts +0 -32
- package/plugins/dsh/lib/command.d.ts.map +0 -1
- package/plugins/dsh/lib/command.js +0 -81
- package/plugins/dsh/lib/delphi-config.d.ts +0 -75
- package/plugins/dsh/lib/delphi-config.d.ts.map +0 -1
- package/plugins/dsh/lib/delphi-config.js +0 -183
- package/plugins/dsh/lib/delphi-run.d.ts +0 -24
- package/plugins/dsh/lib/delphi-run.d.ts.map +0 -1
- package/plugins/dsh/lib/delphi-run.js +0 -92
- package/plugins/dsh/lib/delphi.d.ts +0 -88
- package/plugins/dsh/lib/delphi.d.ts.map +0 -1
- package/plugins/dsh/lib/delphi.js +0 -161
- package/plugins/dsh/lib/dialect.d.ts +0 -24
- package/plugins/dsh/lib/dialect.d.ts.map +0 -1
- package/plugins/dsh/lib/dialect.js +0 -36
- package/plugins/dsh/lib/gate-runner.d.ts +0 -18
- package/plugins/dsh/lib/gate-runner.d.ts.map +0 -1
- package/plugins/dsh/lib/gate-runner.js +0 -46
- package/plugins/dsh/lib/index.d.ts +0 -5
- package/plugins/dsh/lib/index.d.ts.map +0 -1
- package/plugins/dsh/lib/index.js +0 -162
- package/plugins/dsh/node_modules/.vite/vitest/results.json +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-19
|
|
4
|
+
**Commit:** 9b545de5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.28.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-19
|
|
4
|
+
**Commit:** 9b545de5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.28.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-19
|
|
4
|
+
**Commit:** 9b545de5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.28.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-19
|
|
4
|
+
**Commit:** 9b545de5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.28.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-19
|
|
4
|
+
**Commit:** 9b545de5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.28.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-CCLINT-001
|
|
3
|
+
* @intent a pure re-export barrel (export { … } from './x') forwards names but
|
|
4
|
+
* adds no module surface; counting each forwarded member inflates the
|
|
5
|
+
* many-exports metric and blocks Boy-Scout splits that are exactly what
|
|
6
|
+
* the rule asks for (common.ts → common-process.ts)
|
|
7
|
+
* @covers AC-CCLINT-001-01
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { TypeScriptAdapter } from '../adapters/typescript';
|
|
12
|
+
|
|
13
|
+
vi.mock('fs', () => ({ readFileSync: vi.fn() }));
|
|
14
|
+
|
|
15
|
+
function exportCount(content: string): number {
|
|
16
|
+
(readFileSync as ReturnType<typeof vi.fn>).mockReturnValue(content);
|
|
17
|
+
return new TypeScriptAdapter('t.ts').extractExports().length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('extractExports counts module surface, not forwarded names', () => {
|
|
21
|
+
it('does not count members of an export-from re-export block', () => {
|
|
22
|
+
const content = [
|
|
23
|
+
"export {",
|
|
24
|
+
' type ToolAvailability,',
|
|
25
|
+
' isToolAvailable,',
|
|
26
|
+
' runTool,',
|
|
27
|
+
" getChangedFiles,",
|
|
28
|
+
"} from './common-process';",
|
|
29
|
+
'export function local(): void {}',
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
// One local export only — the six forwarded names are not this module's surface.
|
|
33
|
+
expect(exportCount(content)).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('still counts local export { A, B } blocks without a from clause', () => {
|
|
37
|
+
const content = ['export { alpha, beta };', 'const alpha = 1;', 'const beta = 2;', ''].join('\n');
|
|
38
|
+
expect(exportCount(content)).toBe(2);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -36,49 +36,68 @@ const CONTROL_KEYWORDS = new Set([
|
|
|
36
36
|
'case', 'break', 'continue', 'const', 'let', 'var', 'as', 'satisfies', 'keyof',
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Lizard parse note: these two helpers are laid out so that no declaration
|
|
41
|
+
// carrying a return type annotation is immediately followed by a JSDoc block.
|
|
42
|
+
// lizard 1.21.3 mis-scopes that combination and reports the pairing as one row
|
|
43
|
+
// spanning every following function (measured here: findBodyBrace swallowed the
|
|
44
|
+
// rest of the file at CCN 69). Keep the plain comment style above them.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
// Advance one scan character against the type-literal depth. Returns the new
|
|
48
|
+
// depth plus the index to resume from, so parenthesised groups are skipped whole.
|
|
49
|
+
// The inline typed-return form is avoided deliberately: lizard 1.21.3 loses the
|
|
50
|
+
// function boundary when a return annotation contains a brace literal.
|
|
51
|
+
type ScanStep = { depth: number; resume: number };
|
|
52
|
+
|
|
53
|
+
const stepDepth = (masked: string, c: string, i: number, depth: number): ScanStep => {
|
|
54
|
+
if (c === '(') {
|
|
55
|
+
const { end } = readParenGroup(masked, i);
|
|
56
|
+
return { depth, resume: end - 1 };
|
|
57
|
+
}
|
|
58
|
+
if (c === '<' || c === '[') return { depth: depth + 1, resume: i };
|
|
59
|
+
if (c === '>' || c === ']') return { depth: Math.max(0, depth - 1), resume: i };
|
|
60
|
+
if (c === '}') return { depth: Math.max(0, depth - 1), resume: i };
|
|
61
|
+
return { depth, resume: i };
|
|
62
|
+
};
|
|
63
|
+
|
|
48
64
|
function findBodyBrace(masked: string, parenIndex: number): number {
|
|
49
65
|
if (masked[parenIndex] !== '(') return -1;
|
|
50
66
|
const { end } = readParenGroup(masked, parenIndex);
|
|
51
67
|
let depth = 0;
|
|
52
|
-
|
|
68
|
+
let i = end;
|
|
69
|
+
while (i < masked.length) {
|
|
53
70
|
const c = masked[i];
|
|
54
|
-
if (c === '(') { const g = readParenGroup(masked, i); i = g.end - 1; continue; }
|
|
55
|
-
if (c === '<' || c === '[') { depth++; continue; }
|
|
56
|
-
if (c === '>' || c === ']') { if (depth > 0) depth--; continue; }
|
|
57
71
|
if (c === '{') {
|
|
58
72
|
if (depth === 0) return i;
|
|
59
73
|
depth++;
|
|
74
|
+
i++;
|
|
60
75
|
continue;
|
|
61
76
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
// A line break or semicolon before any brace means this header has no
|
|
78
|
+
// braced body (an interface member or an overload signature).
|
|
79
|
+
if ((c === ';' || c === '\n') && depth === 0) return -1;
|
|
80
|
+
const stepped = stepDepth(masked, c, i, depth);
|
|
81
|
+
depth = stepped.depth;
|
|
82
|
+
i = stepped.resume + 1;
|
|
68
83
|
}
|
|
69
84
|
return -1;
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
|
|
87
|
+
// Index just past the matching closing brace for the block opened at open.
|
|
88
|
+
// Written with explicit braces throughout: an unbraced else-if chain here made
|
|
89
|
+
// lizard 1.21.3 extend the PREVIOUS function's span over this whole region.
|
|
73
90
|
function findBlockEnd(masked: string, open: number): number {
|
|
74
91
|
let depth = 0;
|
|
75
92
|
for (let i = open; i < masked.length; i++) {
|
|
76
93
|
const c = masked[i];
|
|
77
|
-
if (c === '{')
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (depth === 0) return i + 1;
|
|
94
|
+
if (c === '{') {
|
|
95
|
+
depth++;
|
|
96
|
+
continue;
|
|
81
97
|
}
|
|
98
|
+
if (c !== '}') continue;
|
|
99
|
+
depth--;
|
|
100
|
+
if (depth === 0) return i + 1;
|
|
82
101
|
}
|
|
83
102
|
return masked.length;
|
|
84
103
|
}
|
|
@@ -651,14 +670,26 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
|
|
|
651
670
|
});
|
|
652
671
|
}
|
|
653
672
|
|
|
654
|
-
|
|
673
|
+
// A re-export block lists names this module FORWARDS, not names it
|
|
674
|
+
// defines. Counting every forwarded member punishes exactly the barrel
|
|
675
|
+
// split (many-exports → sub-module + `export { … } from`) that the rule
|
|
676
|
+
// recommends, so only local `export { A, B };` blocks count — and each
|
|
677
|
+
// counts once per exported name, as before for single-line blocks.
|
|
678
|
+
const reExportRegex = /^export\s*\{([^}]*)\}(\s+from\s+[^;{]+)?;/gm;
|
|
655
679
|
let reMatch;
|
|
656
680
|
while ((reMatch = reExportRegex.exec(this.fileContent)) !== null) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
681
|
+
if (reMatch[2]) continue; // `export { … } from '…'` forwards: no new surface
|
|
682
|
+
const names = reMatch[1]
|
|
683
|
+
.split(',')
|
|
684
|
+
.map((part) => part.replace(/^type\s+/, '').trim())
|
|
685
|
+
.filter((part) => part.length > 0);
|
|
686
|
+
for (const name of names) {
|
|
687
|
+
exportMatches.push({
|
|
688
|
+
name,
|
|
689
|
+
type: 're-export',
|
|
690
|
+
line: this.getLineNumber(reMatch.index),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
662
693
|
}
|
|
663
694
|
|
|
664
695
|
return exportMatches;
|
package/principles/boy-scout.ts
CHANGED
|
@@ -147,6 +147,11 @@ export function classifyFiles(gitDiffLines: string[]): FileClassification {
|
|
|
147
147
|
* cannot be trusted is worse than no baseline, because the gate still reports
|
|
148
148
|
* "PASSED" and nobody looks.
|
|
149
149
|
*/
|
|
150
|
+
/** True when a stored budget is usable: a real, finite number. */
|
|
151
|
+
function hasFiniteBudget(entry: BaselineEntry): boolean {
|
|
152
|
+
return typeof entry.totalWarnings === 'number' && Number.isFinite(entry.totalWarnings);
|
|
153
|
+
}
|
|
154
|
+
|
|
150
155
|
function assertValidEntry(file: string, entry: unknown): asserts entry is BaselineEntry {
|
|
151
156
|
const malformed = (detail: string): never => {
|
|
152
157
|
throw new Error(
|
|
@@ -159,8 +164,8 @@ function assertValidEntry(file: string, entry: unknown): asserts entry is Baseli
|
|
|
159
164
|
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
160
165
|
malformed(`got ${Array.isArray(entry) ? 'an array' : typeof entry}`);
|
|
161
166
|
}
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
if (!hasFiniteBudget(entry as BaselineEntry)) {
|
|
168
|
+
const total = (entry as BaselineEntry).totalWarnings;
|
|
164
169
|
malformed(`totalWarnings is ${total === undefined ? 'missing' : String(total)}`);
|
|
165
170
|
}
|
|
166
171
|
}
|
|
@@ -190,22 +195,26 @@ export async function loadBaseline(baselinePath: string): Promise<Record<string,
|
|
|
190
195
|
throw new Error(`Cannot read baseline ${baselinePath}: ${reason}`);
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
const parsed = parseBaselineObject(baselineContent, baselinePath);
|
|
199
|
+
for (const [file, entry] of Object.entries(parsed)) {
|
|
200
|
+
assertValidEntry(file, entry);
|
|
201
|
+
}
|
|
202
|
+
return parsed as Record<string, BaselineEntry>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Parse baseline JSON into an object map, or throw a located error. */
|
|
206
|
+
function parseBaselineObject(content: string, baselinePath: string): Record<string, unknown> {
|
|
193
207
|
let parsed: unknown;
|
|
194
208
|
try {
|
|
195
|
-
parsed = JSON.parse(
|
|
209
|
+
parsed = JSON.parse(content);
|
|
196
210
|
} catch (error) {
|
|
197
211
|
const reason = error instanceof Error ? error.message : String(error);
|
|
198
212
|
throw new Error(`Baseline ${baselinePath} is not valid JSON: ${reason}`);
|
|
199
213
|
}
|
|
200
|
-
|
|
201
214
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
202
215
|
throw new Error(`Baseline ${baselinePath} must be a JSON object mapping paths to entries.`);
|
|
203
216
|
}
|
|
204
|
-
|
|
205
|
-
for (const [file, entry] of Object.entries(parsed as Record<string, unknown>)) {
|
|
206
|
-
assertValidEntry(file, entry);
|
|
207
|
-
}
|
|
208
|
-
return parsed as Record<string, BaselineEntry>;
|
|
217
|
+
return parsed as Record<string, unknown>;
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
/**
|
|
@@ -254,17 +263,13 @@ async function readBaselineText(baselinePath: string): Promise<string | null | {
|
|
|
254
263
|
|
|
255
264
|
/** Parse the baseline JSON. Returns the object, or a human-readable failure. */
|
|
256
265
|
function parseBaselineJson(content: string, baselinePath: string): Record<string, unknown> | string {
|
|
257
|
-
|
|
266
|
+
// Same grammar as parseBaselineObject, reported instead of thrown: the
|
|
267
|
+
// repair path must keep going with an empty set where loadBaseline aborts.
|
|
258
268
|
try {
|
|
259
|
-
|
|
269
|
+
return parseBaselineObject(content, baselinePath);
|
|
260
270
|
} catch (error) {
|
|
261
|
-
|
|
262
|
-
return `Baseline ${baselinePath} is not valid JSON: ${reason}`;
|
|
271
|
+
return error instanceof Error ? error.message : String(error);
|
|
263
272
|
}
|
|
264
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
265
|
-
return `Baseline ${baselinePath} must be a JSON object mapping paths to entries.`;
|
|
266
|
-
}
|
|
267
|
-
return parsed as Record<string, unknown>;
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
/** Split entries into the valid ones and the names of the malformed ones. */
|