diff --git a/docs/index.html b/docs/index.html
index 2a9bee8..cd15cf7 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1319,8 +1319,9 @@
AI Security Standards Crosswalk
DATA.forEach(function(e) {
e.mappings.forEach(function(m) { FW_SET.add(m.framework); });
});
- // Also include registry-only frameworks (e.g. CoSAI, EU AI Act CoP)
- FW_REGISTRY.forEach(function(fw) { FW_SET.add(fw.name); });
+ // A framework counts once a mapping row cites it. A registry nothing maps yet
+ // (transcribed ahead of its rows) stays out of every count and selector, so
+ // the headline matches the README's "frameworks mapped".
FRAMEWORKS = Array.from(FW_SET).sort();
var ENTRY_MAP = {};
@@ -1807,7 +1808,9 @@ AI Security Standards Crosswalk
// Stats
- var totalRegControls = FW_REGISTRY.reduce(function(s, fw) { return s + (fw.controls || []).length; }, 0);
+ var totalRegControls = FW_REGISTRY.reduce(function(s, fw) {
+ return FW_SET.has(fw.name) ? s + (fw.controls || []).length : s;
+ }, 0);
var stats = [
[FRAMEWORKS.length, 'Frameworks'],
[totalMappings.toLocaleString(), 'Mappings'],
diff --git a/scripts/webapp.test.mjs b/scripts/webapp.test.mjs
index 3ed1264..370ef7d 100644
--- a/scripts/webapp.test.mjs
+++ b/scripts/webapp.test.mjs
@@ -143,6 +143,24 @@ test('an unknown route falls back to a page, not a blank screen', () => {
assert.ok(app.children.length > 0, 'the fallback rendered an empty page');
});
+// A registry can land before any row maps it (NIST AI 600-1 in #187). The home
+// page must not count it as a framework, or add its controls to the headline.
+test('the home page counts mapped frameworks only', () => {
+ const { window } = dom;
+ const stats = JSON.parse(readFileSync(path.join(ROOT, 'data', 'stats.json'), 'utf8'));
+ const mapped = new Set(window.CROSSWALK_DATA.flatMap((e) => e.mappings.map((m) => m.framework)));
+ const controls = window.CROSSWALK_FRAMEWORKS
+ .filter((fw) => mapped.has(fw.name))
+ .reduce((s, fw) => s + (fw.controls || []).length, 0);
+
+ const app = visit('#/');
+ const cards = Object.fromEntries([...app.querySelectorAll('.stat-card')].map((c) => [
+ c.querySelector('.stat-label').textContent, c.querySelector('.stat-num').textContent,
+ ]));
+ assert.equal(cards.Frameworks, String(stats.frameworks.mapped));
+ assert.equal(cards['Registry Controls'], String(controls));
+});
+
// The About page is the one place in the webapp allowed to carry the creator
// credit, and this file is not — so the test checks that the section renders
// with content, never the name itself.