From 9095603414d5ddf40e8feef8e0274c484d3151c6 Mon Sep 17 00:00:00 2001 From: Sam Mear Date: Sat, 7 Mar 2026 16:50:54 +0000 Subject: [PATCH] check-spec command --- Makefile | 5 + README.md | 1 + build/check-missing-specs.js | 197 +++++++++++++++++++++++++++++++++++ config/row-type-map.yaml | 174 +++++++++++++++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 build/check-missing-specs.js create mode 100644 config/row-type-map.yaml diff --git a/Makefile b/Makefile index 3cfe664be..f9227b5f0 100644 --- a/Makefile +++ b/Makefile @@ -141,3 +141,8 @@ clean-nonet: ${psmk_parse} ${athr_output} .PHONY: development production test clean clean-nonet watch + +# check-spec: run a check for missing rows/members for a given spec +# usage: make check-spec SPEC=Granite-Ridge +check-spec: + @node build/check-missing-specs.js "${SPEC}" diff --git a/README.md b/README.md index 98a9559c8..b35d2f089 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Then, you can view SpecDB at file:///home/markasoftware/whatever/specdb/, which ### Bonus Commands * `make watch`: Start auto-rebuild daemon. You still need to manually. Requires [entr](https://bitbucket.org/eradman/entr) +* `make check-spec`: Display missing specs - some in this list is not mandatory. Example: `make check-spec SPEC=Granite-Ridge`. * `make test`: Run unit tests. If any of these fail, do not commit! Fix them! * `make production`: Build for production. If you previously ran `make` without `production`, run `make clean-nonet` before this. * `make clean`: Destroy all generated files. This will trigger a rescrape the next time you run `make`. diff --git a/build/check-missing-specs.js b/build/check-missing-specs.js new file mode 100644 index 000000000..2febbed62 --- /dev/null +++ b/build/check-missing-specs.js @@ -0,0 +1,197 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const ROW_DATA = path.resolve(__dirname, '../src/js/row-data.js'); +const SPECS_DIR = path.resolve(__dirname, '../specs'); +const CONFIG_DIR = path.resolve(__dirname, '../config'); + +// optional mapping: spec type -> rows specific to that type (moved to config/) +const ROW_TYPE_MAP_FILE = path.join(CONFIG_DIR, 'row-type-map.yaml'); +let rowTypeMap = {}; +if (fs.existsSync(ROW_TYPE_MAP_FILE)) { + try { + rowTypeMap = yaml.load(fs.readFileSync(ROW_TYPE_MAP_FILE, 'utf8')) || {}; + } catch (e) { + console.error('Failed to parse', ROW_TYPE_MAP_FILE, e.message); + rowTypeMap = {}; + } +} + +function findSpecFile(baseName) { + const results = []; + function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const e of entries) { + const p = path.join(dir, e.name); + if (e.isDirectory()) { + walk(p); + } else if (e.isFile() && path.parse(e.name).name === baseName && path.extname(e.name).toLowerCase() === '.yaml') { + results.push(p); + } + } + } + walk(SPECS_DIR); + return results; +} + +function loadYaml(file) { + try { + return yaml.load(fs.readFileSync(file, 'utf8')) || {}; + } catch (e) { + console.error('Failed to parse', file, e.message); + return {}; + } +} + +function main() { + const arg = process.argv[2]; + if (!arg) { + console.error('Usage: node build/check-missing-specs.js '); + process.exit(2); + } + + // load row definitions + let rowDefs; + try { + rowDefs = require(ROW_DATA).sections.map(s => s.rows.map(r => r.name)).flat(); + } catch (e) { + console.error('Failed to load row-data.js:', e.message); + process.exit(3); + } + + // find main spec file(s) + const specFiles = findSpecFile(arg); + if (specFiles.length === 0) { + console.error('Spec', arg, 'not found under specs/'); + process.exit(4); + } + + console.log('Checking spec:', arg); + for (const specFile of specFiles) { + const spec = loadYaml(specFile); + console.log('\nMain file:', specFile); + + // collect members from sections where present + const members = []; + if (Array.isArray(spec.sections)) { + for (const s of spec.sections) { + if (Array.isArray(s.members)) { + for (const m of s.members) members.push(m); + } + } + } + + if (members.length === 0) { + console.log(' No members listed in sections for this spec.'); + continue; + } + + const missingMemberFiles = []; + const memberMissingRows = {}; + + // recursively collect data and types from a spec file following "inherits" + const seenFiles = new Set(); + function collectDataFromSpec(specPath) { + if (!specPath || seenFiles.has(specPath)) return { data: {}, types: [] }; + seenFiles.add(specPath); + const y = loadYaml(specPath); + let combinedData = {}; + let combinedTypes = []; + if (Array.isArray(y.inherits)) { + for (const inh of y.inherits) { + const found = findSpecFile(inh); + if (found.length === 0) continue; + for (const f of found) { + const parent = collectDataFromSpec(f); + combinedData = Object.assign({}, parent.data, combinedData); + combinedTypes = Array.from(new Set([...combinedTypes, ...(parent.types || [])])); + } + } + } + const ownData = y.data || {}; + combinedData = Object.assign({}, combinedData, ownData); + // collect own types (support `types` array or `type` string) + let ownTypes = []; + if (Array.isArray(y.types)) ownTypes = y.types.map(String); + else if (typeof y.types === 'string') ownTypes = [y.types]; + else if (typeof y.type === 'string') ownTypes = [y.type]; + combinedTypes = Array.from(new Set([...combinedTypes, ...ownTypes])); + return { data: combinedData, types: combinedTypes }; + } + + for (const m of members) { + const found = findSpecFile(m); + if (found.length === 0) { + missingMemberFiles.push(m); + continue; + } + // build merged data and types from member and its inheritance chain + seenFiles.clear(); + const merged = collectDataFromSpec(found[0]); + const mergedData = merged.data || {}; + const mergedTypes = merged.types || []; + + // Treat `config/row-type-map.yaml` as authoritative: it maps spec types + // -> rows that are specific to that type. For any given `rowName`: + // - If the row is mapped to one or more types, only check it when the + // part's `types` intersects that mapped set. + // - If the row is not mapped (i.e. generic), always check it. + // This avoids the previous "skip everything the part doesn't have" + // behavior and makes the map define where rows belong. + const ignoreMap = process.env.IGNORE_ROW_TYPE_MAP === '1'; + if (ignoreMap) console.log(' NOTE: IGNORE_ROW_TYPE_MAP=1 — checking all rows from row-data.js'); + + const missing = []; + if (ignoreMap) { + // When overriding, check every row from row-data.js regardless of map. + for (const rowName of rowDefs) { + if (!(rowName in mergedData)) missing.push(rowName); + } + } else { + const rowToTypes = {}; + for (const [t, rows] of Object.entries(rowTypeMap || {})) { + if (!Array.isArray(rows)) continue; + for (const r of rows) { + rowToTypes[r] = rowToTypes[r] || new Set(); + rowToTypes[r].add(t); + } + } + + // Only check rows that are present in the mapping. The mapping is + // authoritative: unmapped rows are ignored entirely. + for (const rowName of rowDefs) { + const mappedTypes = rowToTypes[rowName]; + if (!mappedTypes) continue; // skip unmapped rows + // If the part has no declared types, we skip type-specific rows. + if (!mergedTypes || mergedTypes.length === 0) continue; + const hasIntersection = mergedTypes.some(mt => mappedTypes.has(mt)); + if (!hasIntersection) continue; // not relevant for this part + if (!(rowName in mergedData)) missing.push(rowName); + } + } + if (missing.length) memberMissingRows[m] = missing; + } + + // report + if (missingMemberFiles.length) { + console.log(' Missing member spec files:'); + for (const mm of missingMemberFiles) console.log(' -', mm); + } else { + console.log(' All member spec files found.'); + } + + if (Object.keys(memberMissingRows).length) { + console.log('\n Missing rows in member specs:'); + for (const [member, rows] of Object.entries(memberMissingRows)) { + console.log(' ' + member + ': ' + rows.length + ' missing rows'); + for (const r of rows) console.log(' -', r); + } + } else { + console.log('\n No missing rows detected in member specs (within checked keys).'); + } + } +} + +main(); diff --git a/config/row-type-map.yaml b/config/row-type-map.yaml new file mode 100644 index 000000000..cb9c526c0 --- /dev/null +++ b/config/row-type-map.yaml @@ -0,0 +1,174 @@ +# Map spec types to rows that are specific to that type. Is for the check-spec make command. + +Graphics Card: + - Architecture + - VRAM Capacity + - VRAM Type + - VRAM Frequency + - VRAM Bandwidth + - VRAM Bus Width + - Maximum VRAM Capacity + - Render Output Unit Count + - FP32 Compute + - Shader Processor Count + - Texture Mapping Unit Count + - Ray Tracing Cores + - Tensor Cores + - Pixel Shaders + - GPU Base Frequency + - GPU Boost Frequency + - GPU + - GPU Model + - VRAM Frequency + - VRAM Bandwidth + - Max Displays + - Outputs + - Power Connectors + - DirectX Support + - HLSL Shader Model + - OpenGL Support + - Vulkan Support + - OpenCL Support + - FreeSync Support + - Crossfire Support + - Outputs + - Power Connectors + - Slot Width + - Length + - Width + - Height + - Max Displays + +APU: + - Architecture + - Base Frequency + - Boost Frequency + - Performance-Core Base Frequency + - Efficient-Core Base Frequency + - Performance-Core Boost Frequency + - Efficient-Core Boost Frequency + - Core Count + - Performance-Core Count + - Efficient-Core Count + - Thread Count + - Performance-Thread Count + - Efficient-Thread Count + - TDP + - cTDP Support + - L1 Cache (Data) + - L1 Cache (Instruction) + - L2 Cache (Total) + - L3 Cache (Total) + - Module Count + - FP64 Compute + - XFR Frequency + - AVX/SSE/MMX + - FMA4 + - FMA3 + - BMI + - AES + - SHA + - Other Extensions + - PCIe 5.0 Lanes + - PCIe 4.0 Lanes + - PCIe 3.0 Lanes + - PCIe 2.0 Lanes + - PCIe 1.0 Lanes + - Max Memory Channels + - Memory Type + - Max Memory Frequency + - Compatible Chipsets + - VRAM Capacity + - VRAM Type + - VRAM Frequency + - VRAM Bandwidth + - VRAM Bus Width + - Maximum VRAM Capacity + - Render Output Unit Count + - FP32 Compute + - Shader Processor Count + - Texture Mapping Unit Count + - Ray Tracing Cores + - Tensor Cores + - Pixel Shaders + - GPU Base Frequency + - GPU Boost Frequency + - GPU + - GPU Model + - DirectX Support + - HLSL Shader Model + - OpenGL Support + - Vulkan Support + - OpenCL Support + - FreeSync Support + - Crossfire Support + +CPU: + - Architecture + - Base Frequency + - Boost Frequency + - Performance-Core Base Frequency + - Efficient-Core Base Frequency + - Performance-Core Boost Frequency + - Efficient-Core Boost Frequency + - Core Count + - Performance-Core Count + - Efficient-Core Count + - Thread Count + - Performance-Thread Count + - Efficient-Thread Count + - TDP + - cTDP Support + - L1 Cache (Data) + - L1 Cache (Instruction) + - L2 Cache (Total) + - L3 Cache (Total) + - Module Count + - XFR Frequency + - AVX/SSE/MMX + - FMA4 + - FMA3 + - BMI + - AES + - SHA + - Other Extensions + - PCIe 5.0 Lanes + - PCIe 4.0 Lanes + - PCIe 3.0 Lanes + - PCIe 2.0 Lanes + - PCIe 1.0 Lanes + - Max Memory Channels + - Memory Type + - Max Memory Frequency + - Compatible Chipsets + +"CPU Architecture": + - Lithography + - Release Date + - Sockets + - Codename + - Die Size + - Stepping + - Architecture + - Efficient-Core Architecture + +"Graphics Architecture": + - Lithography + - Release Date + - Codename + - Die Size + - Architecture + - GPU Model + +"APU Architecture": + - Lithography + - Release Date + - Sockets + - Codename + - Die Size + - Architecture + - GPU Model + +"Generic Container": [] +Hidden: [] +