Skip to content

Commit 4757932

Browse files
[ci](scripts): guard the app config against the model (no app↔docs drift)
Now that the app exists, src/config must mirror discovery → requirements → blueprint. Added a third guard, scripts/check-app-config.mjs, that parses src/config/*.ts and diffs it against the executable source of truth (verify-model.mjs) and the canonical sheets: QA/factor order, budget inversion, influence matrix, dimension qaFit, option ids + names (Model Data Sheet Section 4), defaults, preset levels, anti-pattern ids + severities (Section 5), and fitness templates (Option Content Sheet Section 7). All 10 checks pass — the implementation matches the docs. - Wired into ci.yml (runs on app changes) and docs-integrity.yml (runs on docs/config changes), so neither side can drift from the other. - scripts/README documents the new guard. Verification: all three model guards PASS; 35/35 tests; eslint 0; build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 271d739 commit 4757932

4 files changed

Lines changed: 180 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
cache: npm
3939
- name: Install dependencies
4040
run: npm ci
41+
- name: Check the app config mirrors the model (no drift from the docs)
42+
run: node scripts/check-app-config.mjs
4143
- name: Lint
4244
run: npm run lint
4345
- name: Unit tests (scoring engine + exporters)

.github/workflows/docs-integrity.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
name: Docs integrity
22

3-
# Guards the decision model's documentation: the math must be correct and every
4-
# document must agree. Runs only when the model docs or scripts change. This is a
5-
# docs-only planning repo today; the application CI (lint/test/build) arrives in
6-
# Phase 4 as a separate workflow.
3+
# Guards the decision model: the math must be correct, every document must agree, and the
4+
# implemented app config (src/config) must mirror the docs. Runs when the model docs, the
5+
# app config, or the scripts change. The full application CI (lint/test/build) is in ci.yml.
76

87
on:
98
push:
109
paths:
1110
- 'docs/03-blueprint/**'
1211
- 'docs/02-requirement-analysis/**'
1312
- 'docs/specs/**'
13+
- 'src/config/**'
1414
- 'scripts/**'
1515
- '.github/workflows/docs-integrity.yml'
1616
pull_request:
1717
paths:
1818
- 'docs/03-blueprint/**'
1919
- 'docs/02-requirement-analysis/**'
2020
- 'docs/specs/**'
21+
- 'src/config/**'
2122
- 'scripts/**'
2223

2324
permissions:
@@ -35,3 +36,5 @@ jobs:
3536
run: node scripts/verify-model.mjs
3637
- name: Cross-check the documents for mismatches
3738
run: node scripts/cross-check-docs.mjs
39+
- name: Check the app config mirrors the model
40+
run: node scripts/check-app-config.mjs

scripts/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
# scripts/
22

3-
Two Node scripts (no dependencies — Node ≥ 18) guard the decision model. They run in CI via
4-
[`.github/workflows/docs-integrity.yml`](../.github/workflows/docs-integrity.yml) and locally:
3+
Three Node scripts (no dependencies — Node ≥ 18) guard the decision model. They run in CI via
4+
[`.github/workflows/docs-integrity.yml`](../.github/workflows/docs-integrity.yml) (and the
5+
app-config guard also in [`ci.yml`](../.github/workflows/ci.yml)) and locally:
56

67
| Script | What it guarantees | Run |
78
|---|---|---|
89
| **[`verify-model.mjs`](verify-model.mjs)** | The math is **correct**: recomputes the whole scoring pipeline from the [Model Data Sheet](../docs/03-blueprint/model-data-sheet.md) and asserts the fixtures, all 25 preset targets with their margins, largest-remainder display rounding, expert override/lock semantics, and 500 randomized property tests. | `node scripts/verify-model.mjs` |
910
| **[`cross-check-docs.mjs`](cross-check-docs.mjs)** | The documents **agree**: parses the model values out of each document and diffs them — qaFit vectors, influence matrix, preset levels, preset targets (Model Data Sheet ↔ verify-model.mjs ↔ SRS Section 5.3), default levels, anti-pattern rule IDs + severities, fitness-template coverage, EN factor level labels (Section 2.1 vs Build Spec Section 4), option ids + names (Option Content Sheet vs Model Data Sheet Section 4), EN/ID list parity across the option content, and the prototype's qaFit vectors (vs Model Data Sheet Section 4). | `node scripts/cross-check-docs.mjs` |
11+
| **[`check-app-config.mjs`](check-app-config.mjs)** | The **app mirrors the model**: parses `src/config/*.ts` and diffs it against verify-model.mjs + the sheets — QA/factor order, budget inversion, influence matrix, dimension qaFit, option ids + names, defaults, preset levels, anti-pattern ids + severities, and fitness templates. Prevents the implementation drifting from discovery → requirements → blueprint. | `node scripts/check-app-config.mjs` |
1012

11-
Both exit `0` on success and `1` on any failure. Run **both** after any change to the model values
13+
All three exit `0` on success and `1` on any failure. Run them after any change to the model values
1214
([Model Data Sheet](../docs/03-blueprint/model-data-sheet.md)), the computation contract
1315
([Scoring Algorithm Specification](../docs/03-blueprint/scoring-algorithm.md)), the formulation
1416
([Model Formulation](../docs/03-blueprint/model-formulation.md)), or the SRS preset targets — and

scripts/check-app-config.mjs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Architecture Advisor — app-config vs model guard.
4+
*
5+
* The app's src/config/*.ts must mirror the canonical model. This recomputes the values out of
6+
* the config files and diffs them against scripts/verify-model.mjs (the executable source of
7+
* truth, itself machine-verified against the docs) plus the Model Data Sheet / Option Content
8+
* Sheet for names, anti-patterns, and templates — so the implementation can never silently drift
9+
* from discovery → requirements → blueprint. Run: node scripts/check-app-config.mjs (exit 0 = OK).
10+
*/
11+
import { readFileSync } from 'node:fs';
12+
13+
const read = (p) => readFileSync(new URL(`../${p}`, import.meta.url), 'utf8');
14+
const norm = (s) => s.replaceAll('−', '-');
15+
const noComments = (s) => s.replace(/\/\/[^\n]*/g, ''); // strip line comments before value parsing
16+
17+
const MJS = noComments(read('scripts/verify-model.mjs'));
18+
const MDS = norm(read('docs/03-blueprint/model-data-sheet.md'));
19+
const OCS = read('docs/03-blueprint/option-content-sheet.md');
20+
21+
const QA = ['performance','scalability','availability','security','maintainability','deployability','testability','observability','dataConsistency','interoperability','costEfficiency','timeToMarket'];
22+
const FACT = ['team','distribution','ttm','budget','lifespan','scale','dataVolume','async','realtime','domain','consistency','security','legacy','devops'];
23+
24+
const problems = [];
25+
let n = 0;
26+
const ok = (m) => console.log(` [${++n}] ${m} ✓`);
27+
const bad = (m) => { console.log(` [${++n}] ${m} ✗`); problems.push(m); };
28+
const arrEq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
29+
30+
// ---- helpers to pull arrays/objects out of the TS config (by text) ----
31+
const slice = (s, from, to) => s.slice(s.indexOf(from), to ? s.indexOf(to, s.indexOf(from)) : undefined);
32+
const strList = (block) => [...block.matchAll(/'([^']+)'/g)].map((m) => m[1]);
33+
34+
// ---- 1. QA order ----
35+
{
36+
const app = strList(slice(read('src/config/qualityAttributes.ts'), 'QA_ORDER', '];'));
37+
arrEq(app, QA) ? ok('QA order matches canonical (12)') : bad(`QA order differs: ${app.join(',')}`);
38+
}
39+
40+
// ---- 2. Factor order + budget inverted ----
41+
{
42+
const f = read('src/config/factors.ts');
43+
const app = strList(slice(f, 'FACTOR_ORDER', '];'));
44+
arrEq(app, FACT) ? ok('factor order matches canonical (14)') : bad(`factor order differs: ${app.join(',')}`);
45+
// budget must be flagged inverted
46+
const budgetBlock = f.slice(f.indexOf('budget: {'), f.indexOf('lifespan: {'));
47+
budgetBlock.includes('inverted: true') ? ok('budget flagged inverted') : bad('budget missing inverted flag');
48+
}
49+
50+
// ---- 3. influence matrix app vs verify-model.mjs ----
51+
function parseInfluence(text, from, to) {
52+
const blk = text.slice(text.indexOf(from), text.indexOf(to, text.indexOf(from)));
53+
const out = {};
54+
for (const m of blk.matchAll(/(\w+):\s*\{([^}]*)\}/g)) {
55+
const ent = {};
56+
for (const e of m[2].matchAll(/(\w+):\s*(-?\d+)/g)) ent[e[1]] = Number(e[2]);
57+
if (Object.keys(ent).length) out[m[1]] = ent;
58+
}
59+
return out;
60+
}
61+
{
62+
const app = parseInfluence(noComments(read('src/config/factorQaMatrix.ts')), 'INFLUENCE', '};');
63+
const mjs = parseInfluence(MJS, 'const INFLUENCE', '};');
64+
const diff = [...new Set([...Object.keys(app), ...Object.keys(mjs)])].filter((k) => JSON.stringify(app[k]) !== JSON.stringify(mjs[k]));
65+
diff.length === 0 && Object.keys(app).length === 14 ? ok('influence matrix matches verify-model.mjs (14 factors)') : bad(`influence differs at: ${diff.join(', ') || 'count'}`);
66+
}
67+
68+
// ---- 4 & 5. dimension qaFit + option ids/names ----
69+
function appDims() {
70+
const t = noComments(read('src/config/dimensions.ts'));
71+
const out = {};
72+
for (const dm of t.matchAll(/(D[1-5]):\s*\{[\s\S]*?options:\s*\[([\s\S]*?)\],\s*\},/g)) {
73+
const dim = dm[1];
74+
const opts = [...dm[2].matchAll(/\{\s*id:\s*'([^']+)',\s*name:\s*'([^']+)',\s*qaFit:\s*\[([0-9,\s]+)\]/g)].map((m) => ({ id: m[1], name: m[2], fit: m[3].split(',').map((x) => Number(x.trim())) }));
75+
out[dim] = opts;
76+
}
77+
return out;
78+
}
79+
function mjsDims() {
80+
const blk = MJS.slice(MJS.indexOf('const DIMENSIONS'), MJS.indexOf('const DEFAULTS'));
81+
const order = ['D1', 'D2', 'D3', 'D4', 'D5'];
82+
const out = {};
83+
order.forEach((d, i) => {
84+
const start = blk.indexOf(`${d}:[`);
85+
const end = i + 1 < order.length ? blk.indexOf(`${order[i + 1]}:[`) : blk.length;
86+
const seg = blk.slice(start, end);
87+
out[d] = [...seg.matchAll(/\[(\d+(?:,\d+){11})\]/g)].map((m) => m[1].split(',').map(Number));
88+
});
89+
return out;
90+
}
91+
{
92+
const app = appDims(), mjs = mjsDims();
93+
const dims = ['D1', 'D2', 'D3', 'D4', 'D5'];
94+
const fitDiff = dims.filter((d) => !arrEq(app[d]?.map((o) => o.fit), mjs[d]));
95+
fitDiff.length === 0 ? ok('dimension qaFit vectors match verify-model.mjs (5 dimensions)') : bad(`qaFit differs in: ${fitDiff.join(', ')}`);
96+
97+
// option ids + names vs Model Data Sheet Section 4
98+
const mdsSec = MDS.slice(MDS.indexOf('## 4. Dimension'), MDS.indexOf('## 5.'));
99+
const mdsPairs = [...mdsSec.matchAll(/^\|\s*`([a-z0-9-]+)`\s*\|\s*([^|]+?)\s*\|\s*`[0-9,]+`/gm)].map((m) => `${m[1]}=${m[2]}`);
100+
const appPairs = dims.flatMap((d) => app[d].map((o) => `${o.id}=${o.name}`));
101+
arrEq(appPairs.sort(), mdsPairs.sort()) ? ok(`option ids+names match Model Data Sheet Section 4 (${appPairs.length})`) : bad('option ids/names differ from Model Data Sheet Section 4');
102+
}
103+
104+
// ---- 6. defaults ----
105+
{
106+
const d = read('src/config/defaults.ts');
107+
/ttm:\s*1/.test(d) && /budget:\s*2/.test(d) && !/team:|scale:/.test(d) ? ok('defaults = ttm:1, budget:2 (all others 0)') : bad('defaults differ');
108+
}
109+
110+
// ---- 7. presets (full app levels) vs verify-model deltas over defaults ----
111+
function appPresets() {
112+
const t = noComments(read('src/config/presets.ts'));
113+
const out = {};
114+
for (const m of t.matchAll(/id:\s*'([a-z-]+)',[\s\S]*?levels:\s*levels\(\[([0-9,\s]+)\]\)/g)) {
115+
const v = m[2].split(',').map((x) => Number(x.trim()));
116+
const o = {}; FACT.forEach((f, i) => (o[f] = v[i]));
117+
out[m[1]] = o;
118+
}
119+
return out;
120+
}
121+
function mjsPresets() {
122+
const blk = MJS.slice(MJS.indexOf('const PRESETS'), MJS.indexOf('const TARGETS'));
123+
const out = {};
124+
for (const m of blk.matchAll(/"([a-z-]+)":\s*\{([^}]*)\}/g)) {
125+
const o = {}; FACT.forEach((f) => (o[f] = 0)); o.ttm = 1; o.budget = 2;
126+
for (const e of m[2].matchAll(/(\w+):\s*(\d+)/g)) o[e[1]] = Number(e[2]);
127+
out[m[1]] = o;
128+
}
129+
return out;
130+
}
131+
{
132+
const app = appPresets(), mjs = mjsPresets();
133+
const diff = [];
134+
for (const k of new Set([...Object.keys(app), ...Object.keys(mjs)]))
135+
for (const f of FACT) if (app[k]?.[f] !== mjs[k]?.[f]) diff.push(`${k}.${f}`);
136+
diff.length === 0 && Object.keys(app).length === 5 ? ok('preset levels match verify-model.mjs (5 presets × 14)') : bad(`preset levels differ at: ${diff.join(', ') || 'count'}`);
137+
}
138+
139+
// ---- 8. anti-pattern ids + severities vs Model Data Sheet Section 5 ----
140+
{
141+
const t = read('src/config/antiPatterns.ts');
142+
const app = [...t.matchAll(/id:\s*'([a-z-]+)',\s*severity:\s*'(info|warning|danger)'/g)].map((m) => `${m[1]}:${m[2]}`).sort();
143+
const sec = MDS.slice(MDS.indexOf('## 5. Anti-pattern'), MDS.indexOf('## 6.'));
144+
const mds = [...sec.matchAll(/^\|\s*`([a-z-]+)`\s*\|\s*(info|warning|danger)\s*\|/gm)].map((m) => `${m[1]}:${m[2]}`).sort();
145+
arrEq(app, mds) && app.length === 7 ? ok('anti-pattern ids+severities match Model Data Sheet Section 5 (7)') : bad('anti-pattern ids/severities differ from Model Data Sheet Section 5');
146+
}
147+
148+
// ---- 9. fitness templates (EN) vs Option Content Sheet Section 7 ----
149+
{
150+
const t = read('src/config/fitnessFunctions.ts');
151+
const sec = OCS.slice(OCS.indexOf('## 7. Fitness'), OCS.indexOf('\n---', OCS.indexOf('## 7. Fitness')));
152+
let missing = 0;
153+
for (const m of sec.matchAll(/^\|\s*(\w+)\s*\|\s*([^|]+?)\s*\|/gm)) {
154+
const qa = m[1], text = m[2].trim();
155+
if (qa === 'QA' || !QA.includes(qa)) continue;
156+
if (!t.includes(text)) missing++;
157+
}
158+
missing === 0 ? ok('fitness templates (EN) match Option Content Sheet Section 7 (12)') : bad(`${missing} fitness template(s) differ from Option Content Sheet`);
159+
}
160+
161+
console.log('');
162+
if (problems.length) {
163+
console.error(`✗ ${problems.length} app-config mismatch(es) vs the model. Fix src/config to match the canonical docs.`);
164+
process.exit(1);
165+
}
166+
console.log('✓ App config mirrors the model: QA/factor order, influence, qaFit, option names, defaults, presets, anti-patterns, and fitness templates all agree with the docs.');

0 commit comments

Comments
 (0)