Skip to content

Commit 09a1a19

Browse files
sirdeggenclaude
andcommitted
fix(conformance,docs): unblock CI and address Sonar findings
- conformance/schema/vector.schema.json: allow hyphens in id segments so existing ids like auth.brc31-handshake validate cleanly; unblocks 8 vector files that the new ajv-backed validation was rejecting. - conformance/runner/src/runner.js: refactor validateFile into focused helpers (checkTopLevelShape, checkRequiredFields, checkRegressionMetadata, runSchemaValidator) to reduce cognitive complexity (S3776). - scripts/generate-parity-matrix.mjs: split main into describeFile and buildSummary helpers, extract tallyParity/collectTagCategories, switch to replaceAll (S7781), use top-level await (S7785), and reduce cognitive complexity (S3776). - conformance/PARITY_MATRIX.json: regenerate after script refactor. - docs/architecture/conformance.md, docs/conformance/index.md: link PORTING_GUIDE.md by GitHub URL since file lives outside docs/ tree; fixes docs-site check-links.mjs failures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 0c2ec17 commit 09a1a19

6 files changed

Lines changed: 156 additions & 125 deletions

File tree

conformance/PARITY_MATRIX.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"schema_version": "1.0",
3-
"generated_at": "2026-05-14",
3+
"generated_at": "2026-05-19",
44
"source": "ts-stack conformance corpus",
5-
"description": "Machine-readable parity status for cross-language SDK implementations (Go, Rust, Python). Use this to track and drive conformance. Regenerate with: node scripts/generate-parity-matrix.mjs",
5+
"description": "Machine-readable parity status for cross-language SDK implementations (Go, Rust, Python). Use this to track and drive conformance.",
66
"summary": {
77
"total_files": 72,
88
"total_vectors": 6625,

conformance/runner/src/runner.js

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -137,62 +137,75 @@ const REQUIRED_TOP_LEVEL = ['vectors']
137137
const REGRESSION_RECOMMENDED_TOP_LEVEL = ['version', 'domain', 'category', 'description', 'regression']
138138
const REQUIRED_VECTOR_FIELDS = ['id', 'input', 'expected']
139139

140-
function validateFile (path, data) {
141-
const errors = []
142-
143-
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
144-
// Legacy format: bare array of vectors (no top-level envelope)
145-
if (Array.isArray(data)) {
146-
errors.push(`WARN: ${path} uses legacy bare-array format — wrap in { "vectors": [...] } envelope`)
147-
return { errors, vectors: data }
140+
function checkTopLevelShape (path, data) {
141+
if (Array.isArray(data)) {
142+
return {
143+
errors: [`WARN: ${path} uses legacy bare-array format — wrap in { "vectors": [...] } envelope`],
144+
vectors: data,
145+
done: true
148146
}
149-
errors.push(`ERROR: ${path} top level must be an object or array`)
150-
return { errors, vectors: [] }
151147
}
148+
if (typeof data !== 'object' || data === null) {
149+
return {
150+
errors: [`ERROR: ${path} top level must be an object or array`],
151+
vectors: [],
152+
done: true
153+
}
154+
}
155+
return { errors: [], vectors: null, done: false }
156+
}
152157

153-
const isRegression = path.includes('/regressions/')
154-
155-
// Check required top-level fields (lightweight fast check before full schema validation)
158+
function checkRequiredFields (path, data) {
159+
const errors = []
156160
for (const field of REQUIRED_TOP_LEVEL) {
157161
if (!(field in data)) {
158162
errors.push(`ERROR: ${path} missing required top-level field "${field}"`)
159163
}
160164
}
165+
return errors
166+
}
161167

162-
// For regressions we still do some lightweight metadata checks (the regression schema
163-
// is intentionally more permissive at the top level than the standard one).
164-
if (isRegression) {
165-
for (const field of REGRESSION_RECOMMENDED_TOP_LEVEL) {
166-
if (!(field in data)) {
167-
errors.push(`WARN: ${path} missing recommended regression field "${field}"`)
168-
}
169-
}
170-
if (!data.regression || typeof data.regression !== 'object' || !data.regression.issue) {
171-
errors.push(`WARN: ${path} regression file is missing regression.issue metadata`)
168+
function checkRegressionMetadata (path, data) {
169+
const errors = []
170+
for (const field of REGRESSION_RECOMMENDED_TOP_LEVEL) {
171+
if (!(field in data)) {
172+
errors.push(`WARN: ${path} missing recommended regression field "${field}"`)
172173
}
173174
}
174-
175-
const vectors = Array.isArray(data.vectors) ? data.vectors : []
176-
177-
if (!Array.isArray(data.vectors)) {
178-
errors.push(`ERROR: ${path} "vectors" must be an array`)
179-
return { errors, vectors: [] }
175+
if (!data.regression || typeof data.regression !== 'object' || !data.regression.issue) {
176+
errors.push(`WARN: ${path} regression file is missing regression.issue metadata`)
180177
}
178+
return errors
179+
}
181180

182-
// --- Strict JSON Schema validation (always enforced) ---
181+
function runSchemaValidator (path, data, isRegression) {
183182
const validator = isRegression ? validateRegression : validateStandard
184183
const schemaName = isRegression ? 'regression-vector.schema.json' : 'vector.schema.json'
184+
if (!validator || validator(data)) return []
185+
return (validator.errors || []).map(e => {
186+
const instancePath = e.instancePath || '(root)'
187+
return `ERROR: ${path} SCHEMA: ${schemaName} ${instancePath} ${e.message}`
188+
})
189+
}
190+
191+
function validateFile (path, data) {
192+
const shape = checkTopLevelShape(path, data)
193+
if (shape.done) return { errors: shape.errors, vectors: shape.vectors }
185194

186-
if (validator && !validator(data)) {
187-
const ajvErrors = (validator.errors || []).map(e => {
188-
const instancePath = e.instancePath || '(root)'
189-
return `SCHEMA: ${schemaName} ${instancePath} ${e.message}`
190-
})
191-
// Schema violations are fatal — this is the authoritative check for cross-language ports
192-
ajvErrors.forEach(msg => errors.push(`ERROR: ${path} ${msg}`))
195+
const errors = checkRequiredFields(path, data)
196+
const isRegression = path.includes('/regressions/')
197+
198+
if (isRegression) {
199+
errors.push(...checkRegressionMetadata(path, data))
200+
}
201+
202+
if (!Array.isArray(data.vectors)) {
203+
errors.push(`ERROR: ${path} "vectors" must be an array`)
204+
return { errors, vectors: [] }
193205
}
194206

195-
return { errors, vectors }
207+
errors.push(...runSchemaValidator(path, data, isRegression))
208+
return { errors, vectors: data.vectors }
196209
}
197210

198211
function validateVector (filePath, vec, index) {

conformance/schema/vector.schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
"$schema": { "type": "string" },
1010
"id": {
1111
"type": "string",
12-
"pattern": "^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9]*)*$",
13-
"description": "Stable dot-separated identifier, e.g. sdk.crypto.ecdsa"
12+
"pattern": "^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)*$",
13+
"description": "Stable dot-separated identifier with lowercase alphanumerics and hyphens, e.g. sdk.crypto.ecdsa or auth.brc31-handshake"
1414
},
1515
"name": { "type": "string" },
1616
"brc": {

docs/architecture/conformance.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,5 @@ See [Contributing Vectors](../conformance/contributing-vectors.md) for the file
102102

103103
- [Conformance Testing](../conformance/index.md)
104104
- [Vector Catalog](../conformance/vectors.md)
105-
- **[Porting Guide](../conformance/PORTING_GUIDE.md)** — Essential reading when aligning another language implementation
105+
- **[Porting Guide](https://github.com/bsv-blockchain/ts-stack/blob/main/conformance/PORTING_GUIDE.md)** — Essential reading when aligning another language implementation
106106
- [BRC Standards Index](../reference/brc-index.md)

docs/conformance/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,4 @@ For a non-TypeScript SDK or wallet:
101101
- [TypeScript Runner](./runner-ts.md) — Runner commands and limitations
102102
- [Contributing Vectors](./contributing-vectors.md) — Add or refine fixtures
103103
- [Architecture View](../architecture/conformance.md) — High-level overview for port planning teams
104-
- **[Porting Guide](../conformance/PORTING_GUIDE.md)** — Practical guide for aligning Go, Rust, Python, or other language implementations (including known deviations and conformance tiers)
104+
- **[Porting Guide](https://github.com/bsv-blockchain/ts-stack/blob/main/conformance/PORTING_GUIDE.md)** — Practical guide for aligning Go, Rust, Python, or other language implementations (including known deviations and conformance tiers)

scripts/generate-parity-matrix.mjs

Lines changed: 99 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const ROOT = join(__dirname, '..');
1515
const VECTORS_DIR = join(ROOT, 'conformance/vectors');
1616
const OUTPUT = join(ROOT, 'conformance/PARITY_MATRIX.json');
1717

18+
const STATEFUL_TAG_KEYS = ['funded', 'live_overlay', 'state', 'harness'];
19+
1820
async function walk(dir) {
1921
const entries = await readdir(dir, { withFileTypes: true });
2022
const files = [];
@@ -29,93 +31,99 @@ async function walk(dir) {
2931
return files;
3032
}
3133

32-
async function main() {
33-
const jsonFiles = await walk(VECTORS_DIR);
34-
const files = [];
34+
function isStatefulTag(tag) {
35+
return STATEFUL_TAG_KEYS.some(k => tag.includes(k));
36+
}
3537

36-
for (const fullPath of jsonFiles) {
37-
const relPath = relative(VECTORS_DIR, fullPath).replace(/\\/g, '/');
38-
const raw = await readFile(fullPath, 'utf8');
39-
const data = JSON.parse(raw);
40-
41-
const fileId = data.id || null;
42-
const fileLevelParity = data.parity_class || 'required';
43-
const vectors = Array.isArray(data.vectors) ? data.vectors : [];
44-
45-
let required = 0;
46-
let intended = 0;
47-
let skipped = 0;
48-
const skipReasons = new Set();
49-
const categories = new Set();
50-
51-
for (const vec of vectors) {
52-
const p = vec.parity_class || fileLevelParity;
53-
if (p === 'required') required++;
54-
else if (p === 'intended') intended++;
55-
else if (p === 'best-effort') categories.add('best-effort');
56-
57-
if (vec.skip === true) skipped++;
58-
59-
if (vec.skip_reason) skipReasons.add(vec.skip_reason);
60-
if (vec.tags) {
61-
for (const tag of vec.tags) {
62-
if (['funded', 'live_overlay', 'state', 'harness'].some(k => tag.includes(k))) {
63-
categories.add('wallet_stateful_harness');
64-
}
65-
}
66-
}
67-
}
38+
function tallyParity(parity, counts, categories) {
39+
if (parity === 'required') counts.required++;
40+
else if (parity === 'intended') counts.intended++;
41+
else if (parity === 'best-effort') categories.add('best-effort');
42+
}
6843

69-
const total = vectors.length;
70-
let effectiveStatus = 'required';
71-
if (intended > 0) effectiveStatus = intended === total ? 'intended' : 'mixed';
72-
else if (skipped > 0) effectiveStatus = 'mixed';
73-
74-
let reasonCategory = 'fully_supported';
75-
let justification = '';
76-
77-
if (relPath.startsWith('regressions/')) {
78-
reasonCategory = 'historical_regression';
79-
justification = 'Historical cross-SDK bug reproduction vector';
80-
} else if (relPath.includes('wallet/brc100/')) {
81-
if (intended > 0 || skipped > 0) {
82-
reasonCategory = 'wallet_stateful_harness_required';
83-
justification = 'Requires funded UTXOs + realistic fee model, live overlay, or pre-existing wallet state (see COVERAGE.md)';
84-
}
85-
} else if (relPath.includes('sdk/scripts/evaluation')) {
86-
if (intended > 0) {
87-
reasonCategory = 'partial_ts_behavioral_difference';
88-
justification = `${intended} tx_invalid / MINIMALDATA / OP_VER edge cases intentionally differ from reference test vectors`;
89-
}
90-
}
44+
function collectTagCategories(tags, categories) {
45+
if (!tags) return;
46+
for (const tag of tags) {
47+
if (isStatefulTag(tag)) categories.add('wallet_stateful_harness');
48+
}
49+
}
9150

92-
if (skipReasons.size > 0 && !justification) {
93-
justification = Array.from(skipReasons).join(' | ');
94-
}
51+
function aggregateVectors(vectors, fileLevelParity) {
52+
const counts = { required: 0, intended: 0, skipped: 0 };
53+
const skipReasons = new Set();
54+
const categories = new Set();
9555

96-
files.push({
97-
path: relPath,
98-
id: fileId,
99-
total_vectors: total,
100-
file_level_parity: fileLevelParity,
101-
effective_status: effectiveStatus,
102-
required_count: required,
103-
intended_count: intended,
104-
skipped_count: skipped,
105-
reason_category: reasonCategory,
106-
justification: justification || undefined,
107-
categories: Array.from(categories)
108-
});
56+
for (const vec of vectors) {
57+
tallyParity(vec.parity_class || fileLevelParity, counts, categories);
58+
if (vec.skip === true) counts.skipped++;
59+
if (vec.skip_reason) skipReasons.add(vec.skip_reason);
60+
collectTagCategories(vec.tags, categories);
10961
}
11062

111-
// Sort for stability
112-
files.sort((a, b) => a.path.localeCompare(b.path));
63+
return { counts, skipReasons, categories };
64+
}
11365

114-
const totalVectors = files.reduce((sum, f) => sum + f.total_vectors, 0);
66+
function effectiveStatus(counts, total) {
67+
if (counts.intended > 0) return counts.intended === total ? 'intended' : 'mixed';
68+
if (counts.skipped > 0) return 'mixed';
69+
return 'required';
70+
}
11571

72+
function classifyReason(relPath, counts) {
73+
if (relPath.startsWith('regressions/')) {
74+
return {
75+
reasonCategory: 'historical_regression',
76+
justification: 'Historical cross-SDK bug reproduction vector'
77+
};
78+
}
79+
if (relPath.includes('wallet/brc100/') && (counts.intended > 0 || counts.skipped > 0)) {
80+
return {
81+
reasonCategory: 'wallet_stateful_harness_required',
82+
justification: 'Requires funded UTXOs + realistic fee model, live overlay, or pre-existing wallet state (see COVERAGE.md)'
83+
};
84+
}
85+
if (relPath.includes('sdk/scripts/evaluation') && counts.intended > 0) {
86+
return {
87+
reasonCategory: 'partial_ts_behavioral_difference',
88+
justification: `${counts.intended} tx_invalid / MINIMALDATA / OP_VER edge cases intentionally differ from reference test vectors`
89+
};
90+
}
91+
return { reasonCategory: 'fully_supported', justification: '' };
92+
}
93+
94+
async function describeFile(fullPath) {
95+
const relPath = relative(VECTORS_DIR, fullPath).replaceAll('\\', '/');
96+
const raw = await readFile(fullPath, 'utf8');
97+
const data = JSON.parse(raw);
98+
99+
const fileId = data.id || null;
100+
const fileLevelParity = data.parity_class || 'required';
101+
const vectors = Array.isArray(data.vectors) ? data.vectors : [];
102+
103+
const { counts, skipReasons, categories } = aggregateVectors(vectors, fileLevelParity);
104+
const total = vectors.length;
105+
const { reasonCategory, justification: baseJustification } = classifyReason(relPath, counts);
106+
const justification = baseJustification || (skipReasons.size > 0 ? Array.from(skipReasons).join(' | ') : '');
107+
108+
return {
109+
path: relPath,
110+
id: fileId,
111+
total_vectors: total,
112+
file_level_parity: fileLevelParity,
113+
effective_status: effectiveStatus(counts, total),
114+
required_count: counts.required,
115+
intended_count: counts.intended,
116+
skipped_count: counts.skipped,
117+
reason_category: reasonCategory,
118+
justification: justification || undefined,
119+
categories: Array.from(categories)
120+
};
121+
}
122+
123+
function buildSummary(files) {
116124
const summary = {
117125
total_files: files.length,
118-
total_vectors: totalVectors,
126+
total_vectors: files.reduce((sum, f) => sum + f.total_vectors, 0),
119127
fully_required_files: files.filter(f => f.effective_status === 'required').length,
120128
files_with_intended: files.filter(f => f.intended_count > 0).length,
121129
files_with_mixed_status: files.filter(f => f.effective_status === 'mixed').length,
@@ -131,6 +139,18 @@ async function main() {
131139
summary.by_reason_category[f.reason_category] = (summary.by_reason_category[f.reason_category] || 0) + f.total_vectors;
132140
}
133141

142+
return summary;
143+
}
144+
145+
try {
146+
const jsonFiles = await walk(VECTORS_DIR);
147+
const files = [];
148+
for (const fullPath of jsonFiles) {
149+
files.push(await describeFile(fullPath));
150+
}
151+
files.sort((a, b) => a.path.localeCompare(b.path));
152+
153+
const summary = buildSummary(files);
134154
const matrix = {
135155
schema_version: '1.0',
136156
generated_at: new Date().toISOString().split('T')[0],
@@ -145,9 +165,7 @@ async function main() {
145165
console.log(` Files: ${summary.total_files}`);
146166
console.log(` Vectors: ${summary.total_vectors}`);
147167
console.log(` Fully required files: ${summary.fully_required_files}`);
148-
}
149-
150-
main().catch(err => {
168+
} catch (err) {
151169
console.error(err);
152170
process.exit(1);
153-
});
171+
}

0 commit comments

Comments
 (0)