bmad-plus 0.12.0 → 0.12.2
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/CHANGELOG.md +612 -554
- package/README.md +557 -577
- package/osint-agent-package/agents/osint-investigator.md +12 -0
- package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +491 -482
- package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +126 -126
- package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -48
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -48
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
- package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
- package/package.json +104 -91
- package/readme-international/README.de.md +596 -589
- package/readme-international/README.es.md +613 -606
- package/readme-international/README.fr.md +611 -604
- package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
- package/src/bmad-plus/data/role-triggers.yaml +52 -0
- package/src/bmad-plus/module.yaml +283 -283
- package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
- package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
- package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
- package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
- package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
- package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
- package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
- package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
- package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
- package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +14 -0
- package/src/bmad-plus/packs/pack-seo/SKILL.md +171 -171
- package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
- package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
- package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
- package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
- package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
- package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
- package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
- package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
- package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
- package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
- package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +304 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +410 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
- package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
- package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
- package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
- package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
- package/src/bmad-plus/packs/pack-shield/README.md +6 -6
- package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
- package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
- package/tools/build/check-counts.js +628 -0
- package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
- package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
- package/tools/build/generated-adapters/AGENTS.md +1 -1
- package/tools/build/generated-adapters/CLAUDE.md +1 -1
- package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
- package/tools/build/generated-adapters/GEMINI.md +1 -1
- package/tools/cli/commands/autoconfig.js +470 -472
- package/tools/cli/commands/doctor.js +233 -233
- package/tools/cli/commands/install.js +598 -495
- package/tools/cli/commands/memory-journal-cmd.js +311 -311
- package/tools/cli/commands/memory.js +195 -195
- package/tools/cli/commands/scan.js +348 -364
- package/tools/cli/commands/uninstall.js +101 -101
- package/tools/cli/commands/update.js +134 -134
- package/tools/cli/i18n.js +845 -845
- package/tools/cli/lib/README-memory-journal.md +125 -125
- package/tools/cli/lib/ide-config.js +267 -259
- package/tools/cli/lib/memory-init.js +204 -113
- package/tools/cli/lib/packs.js +209 -209
- package/tools/cli/lib/path-hash.js +39 -0
- package/tools/cli/lib/python-provision.js +508 -508
- package/tools/cli/lib/stack-detect.js +102 -102
- package/tools/cli/lib/validate.js +50 -50
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BMAD+ Build — hand-typed count drift checker (audit NS-01 / ARCH-04 / DOC-06)
|
|
4
|
+
*
|
|
5
|
+
* Every "N agents" / "N workflows" / "N packs" claim in a shipped surface is a
|
|
6
|
+
* fact that MUST be derivable from registry.yaml (+ the files on disk). This
|
|
7
|
+
* script derives the truth, scans the tracked truth surfaces for hand-typed
|
|
8
|
+
* numbers that contradict it, and exits non-zero listing every drift.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: `- **Shield** (GRC) — 38 compliance agents` shipped into
|
|
11
|
+
* every generated user config for months. 38 was not a typo — it was
|
|
12
|
+
* 27 agents + 11 workflows summed into one dimension. A definition error
|
|
13
|
+
* re-drifts on every pack change, so it needs a gate, not a one-off correction.
|
|
14
|
+
*
|
|
15
|
+
* DIMENSIONS ARE THE POINT. 38 is a legitimate number in this repo (Dev Studio
|
|
16
|
+
* has 38 workflows); it is only wrong when attached to `agents`. Every rule
|
|
17
|
+
* below therefore checks a (subject, dimension) pair, never a bare integer.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node tools/build/check-counts.js # verify (exit 1 on drift)
|
|
21
|
+
* node tools/build/check-counts.js --print # print the derived truth and exit 0
|
|
22
|
+
*
|
|
23
|
+
* The test-count claim ("N tests") is the one number here that cannot be
|
|
24
|
+
* derived statically — it requires running Jest. So it is gated by an env var:
|
|
25
|
+
*
|
|
26
|
+
* BMAD_PLUS_TEST_COUNT unset/empty → test claims are listed as NOT GATED and
|
|
27
|
+
* the run says OK (PARTIAL). Never fails:
|
|
28
|
+
* a local `node tools/build/check-counts.js`
|
|
29
|
+
* must not need a full Jest run first.
|
|
30
|
+
* BMAD_PLUS_TEST_COUNT=<integer> → every "N tests" claim must equal it.
|
|
31
|
+
* CI arms this with the Jest-reported
|
|
32
|
+
* total, which is the only run that
|
|
33
|
+
* proves the README's test number.
|
|
34
|
+
* BMAD_PLUS_TEST_COUNT=<garbage> → exit 1. A misspelled total must not
|
|
35
|
+
* masquerade as coverage.
|
|
36
|
+
*
|
|
37
|
+
* Author: Laurent Rochetta
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
'use strict';
|
|
41
|
+
|
|
42
|
+
const fs = require('node:fs');
|
|
43
|
+
const path = require('node:path');
|
|
44
|
+
const { execFileSync } = require('node:child_process');
|
|
45
|
+
const { loadRegistry, DEFAULT_REGISTRY_PATH } = require('./generate');
|
|
46
|
+
|
|
47
|
+
const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Files whose numeric claims describe the CURRENT product. Anything not listed
|
|
51
|
+
* is out of scope by construction, so the gate can never be "fixed" by quietly
|
|
52
|
+
* moving a lie into an unscanned file — adding a surface is an explicit edit.
|
|
53
|
+
*/
|
|
54
|
+
const TRUTH_SURFACES = [
|
|
55
|
+
'README.md',
|
|
56
|
+
'README-DIST.md',
|
|
57
|
+
'readme-international/README.fr.md',
|
|
58
|
+
'readme-international/README.es.md',
|
|
59
|
+
'readme-international/README.de.md',
|
|
60
|
+
'src/bmad-plus/module.yaml',
|
|
61
|
+
'src/bmad-plus/packs/pack-shield/README.md',
|
|
62
|
+
'src/bmad-plus/packs/pack-shield/SKILL.md',
|
|
63
|
+
'src/bmad-plus/packs/pack-shield/shield-orchestrator.md',
|
|
64
|
+
'tools/cli/lib/ide-config.js',
|
|
65
|
+
'tools/build/adapters.config.js',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* NOT scanned, deliberately:
|
|
70
|
+
* - CHANGELOG.md, audit/**, docs/research/** — historical records. They
|
|
71
|
+
* legitimately quote past and *wrong* numbers ("the advertised 38 agents")
|
|
72
|
+
* verbatim; gating them would force rewriting history to satisfy a linter.
|
|
73
|
+
* - .agents/** — gitignored local install output, not a source.
|
|
74
|
+
* - upstream/** — third-party clone.
|
|
75
|
+
* Release-history table rows inside a scanned README are skipped for the same
|
|
76
|
+
* reason (see isFrozenHistoryRow): v0.6.0 really did ship 30 workflows and
|
|
77
|
+
* v0.12.0 really did ship 333 tests. Those are true statements about the past.
|
|
78
|
+
*/
|
|
79
|
+
const HISTORY_ROW_RE = /^\|\s*\*\*\d+\.\d+\.\d+\*\*\s*\|/;
|
|
80
|
+
|
|
81
|
+
function isFrozenHistoryRow(line) {
|
|
82
|
+
return HISTORY_ROW_RE.test(line);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── Derivation ─────────────────────────────────────────────────────────── */
|
|
86
|
+
|
|
87
|
+
/** Sum of list lengths for a given key across a pack's categories. */
|
|
88
|
+
function sumCategories(pack, key) {
|
|
89
|
+
if (!Array.isArray(pack.categories)) return 0;
|
|
90
|
+
return pack.categories.reduce((n, c) => n + (Array.isArray(c[key]) ? c[key].length : 0), 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Count *.md agent files on disk for a packaged pack's categories/ tree. */
|
|
94
|
+
function countAgentFilesOnDisk(packDir) {
|
|
95
|
+
const root = path.join(REPO_ROOT, 'src', 'bmad-plus', 'packs', packDir, 'categories');
|
|
96
|
+
if (!fs.existsSync(root)) return null;
|
|
97
|
+
let n = 0;
|
|
98
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
99
|
+
// `workflows/` sits under categories/ but is NOT an agent category — it is
|
|
100
|
+
// the directory whose 11 files got summed into the agent total to make 38.
|
|
101
|
+
if (!entry.isDirectory() || entry.name === 'workflows') continue;
|
|
102
|
+
n += fs.readdirSync(path.join(root, entry.name)).filter((f) => f.endsWith('.md')).length;
|
|
103
|
+
}
|
|
104
|
+
return n;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Count *.md workflow files on disk for a packaged pack. */
|
|
108
|
+
function countWorkflowFilesOnDisk(packDir) {
|
|
109
|
+
const dir = path.join(REPO_ROOT, 'src', 'bmad-plus', 'packs', packDir, 'categories', 'workflows');
|
|
110
|
+
if (!fs.existsSync(dir)) return null;
|
|
111
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Installer languages, read from the CLI's own i18n module (its SSOT). */
|
|
115
|
+
function deriveLanguages() {
|
|
116
|
+
const { LANGUAGES } = require(path.join(REPO_ROOT, 'tools', 'cli', 'i18n.js'));
|
|
117
|
+
return Object.keys(LANGUAGES);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Derive every count this gate enforces, from registry.yaml + the filesystem.
|
|
122
|
+
* Cross-checks the registry against the files on disk: if they disagree, the
|
|
123
|
+
* derivation itself is untrustworthy and that is reported as a drift.
|
|
124
|
+
*/
|
|
125
|
+
function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH) {
|
|
126
|
+
const registry = loadRegistry(registryPath);
|
|
127
|
+
const packs = registry.packs;
|
|
128
|
+
const shield = packs.shield;
|
|
129
|
+
const devStudio = packs['dev-studio'];
|
|
130
|
+
|
|
131
|
+
const shieldAgents = sumCategories(shield, 'agents');
|
|
132
|
+
const shieldWorkflows = (shield.workflows || []).length;
|
|
133
|
+
const shieldCategories = (shield.categories || []).length;
|
|
134
|
+
const shieldFrameworks = (shield.compliance_tags || []).length;
|
|
135
|
+
|
|
136
|
+
const installerAgents = Object.values(packs).reduce((n, p) => n + p.agents.length, 0);
|
|
137
|
+
const devStudioSubAgents = (devStudio.sub_agents || []).length;
|
|
138
|
+
const devStudioWorkflows =
|
|
139
|
+
(devStudio.workflows || []).length + sumCategories(devStudio, 'workflows');
|
|
140
|
+
|
|
141
|
+
const mismatches = [];
|
|
142
|
+
const onDiskAgents = countAgentFilesOnDisk(shield.pack_dir);
|
|
143
|
+
const onDiskWorkflows = countWorkflowFilesOnDisk(shield.pack_dir);
|
|
144
|
+
if (onDiskAgents !== null && onDiskAgents !== shieldAgents) {
|
|
145
|
+
mismatches.push(
|
|
146
|
+
`registry.yaml declares ${shieldAgents} shield category agents but ${onDiskAgents} agent files exist on disk`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (onDiskWorkflows !== null && onDiskWorkflows !== shieldWorkflows) {
|
|
150
|
+
mismatches.push(
|
|
151
|
+
`registry.yaml declares ${shieldWorkflows} shield workflows but ${onDiskWorkflows} workflow files exist on disk`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Every distinct agent a user can talk to: the installer entry points plus
|
|
156
|
+
// the specialists the two packaged orchestrators route to.
|
|
157
|
+
const totalAgents = installerAgents + shieldAgents + devStudioSubAgents;
|
|
158
|
+
// Sizes of the declared per-category agent groups ("5 agents", "6 agents"…),
|
|
159
|
+
// which are legitimate claims wherever a category is described.
|
|
160
|
+
const perCategorySizes = new Set((shield.categories || []).map((c) => (c.agents || []).length));
|
|
161
|
+
// Sizes of the per-pack installer rosters ("3 agents (Scout, Chief, Judge)").
|
|
162
|
+
const perPackInstallerSizes = new Set(Object.values(packs).map((p) => p.agents.length));
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
packCount: Object.keys(packs).length,
|
|
166
|
+
installerAgents,
|
|
167
|
+
shieldAgents,
|
|
168
|
+
shieldWorkflows,
|
|
169
|
+
shieldCategories,
|
|
170
|
+
shieldFrameworks,
|
|
171
|
+
devStudioSubAgents,
|
|
172
|
+
devStudioWorkflows,
|
|
173
|
+
totalAgents,
|
|
174
|
+
languages: deriveLanguages(),
|
|
175
|
+
perCategorySizes,
|
|
176
|
+
// The COMPLETE vocabulary of numbers that may legitimately precede "agents"
|
|
177
|
+
// anywhere in the product's own copy. Default-deny: a value outside this set
|
|
178
|
+
// is a number nobody can derive, so it is drift by definition. This is what
|
|
179
|
+
// gates the 47s — "47 agents" stays green only while 47 is still derivable,
|
|
180
|
+
// and every occurrence turns red the day the roster changes.
|
|
181
|
+
agentCounts: new Set([
|
|
182
|
+
installerAgents,
|
|
183
|
+
shieldAgents,
|
|
184
|
+
devStudioSubAgents,
|
|
185
|
+
totalAgents,
|
|
186
|
+
...perCategorySizes,
|
|
187
|
+
...perPackInstallerSizes,
|
|
188
|
+
]),
|
|
189
|
+
// Same idea for the workflow dimension. Anchored lines get graded against
|
|
190
|
+
// the one pack they name; unanchored ones must still be *some* real total.
|
|
191
|
+
workflowCounts: new Set([shieldWorkflows, devStudioWorkflows]),
|
|
192
|
+
derivationMismatches: mismatches,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* ── Rules ──────────────────────────────────────────────────────────────── */
|
|
197
|
+
|
|
198
|
+
const SHIELD_ANCHOR =
|
|
199
|
+
/shield|\bGRC\b|compliance[- ]agent|agents? de conformité|agentes de cumplimiento|Compliance-Agenten/i;
|
|
200
|
+
const DEV_STUDIO_ANCHOR = /dev[- ]studio/i;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Lines whose agent count describes the UPSTREAM project, not this one:
|
|
204
|
+
* "BMAD-METHOD ... 9 specialized agents" is a true statement about a different
|
|
205
|
+
* product, so the roster rule must not grade it. Scoped to the agent dimension
|
|
206
|
+
* only — a line may name BMAD-METHOD and still make a claim of its own
|
|
207
|
+
* ("Fork of BMAD-METHOD — ... CLI installer in 10 languages"), and that claim
|
|
208
|
+
* stays gated.
|
|
209
|
+
*/
|
|
210
|
+
const FOREIGN_SUBJECT_ANCHOR = /BMAD-METHOD/i;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* `| BMAD-METHOD | BMAD+ |` opens the upstream comparison table, whose rows
|
|
214
|
+
* carry a foreign count in the left column and ours in the right ("| 9
|
|
215
|
+
* specialized agents | **47 agents** across 9 packs |") without repeating the
|
|
216
|
+
* marker. The rows are recognised positionally, like isFrozenHistoryRow.
|
|
217
|
+
*/
|
|
218
|
+
const FOREIGN_TABLE_HEADER_RE = /^\s*\|\s*BMAD-METHOD\s*\|/i;
|
|
219
|
+
|
|
220
|
+
/* A claim is `<number> <qualifiers…> <dimension noun>`. The three constants
|
|
221
|
+
* below define what may sit between the number and the noun; every rule is
|
|
222
|
+
* built from them by claimRe(), so a fix to the CLASS lands on every dimension
|
|
223
|
+
* at once instead of on one hand-listed adjective. */
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What may SEPARATE the pieces of a claim: whitespace and markdown
|
|
227
|
+
* emphasis/code markers — "**38** agents", "`38` agents", "**38 agents**".
|
|
228
|
+
* `\s` already covers U+00A0/U+202F, which the French copy uses before
|
|
229
|
+
* punctuation.
|
|
230
|
+
*
|
|
231
|
+
* `-` and `_` are deliberately NOT separators. This repo is full of agent
|
|
232
|
+
* IDENTIFIERS — `iso27001-agent`, `nist-800-53-agent`, `section508-agent`,
|
|
233
|
+
* `iso42001-agent` — and a hyphen-tolerant gap would read every one of them as
|
|
234
|
+
* a claim of "27001 agents". A qualifier may still *contain* a hyphen
|
|
235
|
+
* ("multi-role", "GRC-Compliance-"); it just may not START with one.
|
|
236
|
+
*/
|
|
237
|
+
const GAP = '[\\s*`]*';
|
|
238
|
+
|
|
239
|
+
/** One qualifier word: hyphenated and accented forms included. */
|
|
240
|
+
const WORD = '[A-Za-zÀ-ÖØ-öø-ÿ][A-Za-zÀ-ÖØ-öø-ÿ-]*';
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* How many qualifiers a claim may carry. The bound is load-bearing: it is what
|
|
244
|
+
* stops a match from walking across a whole sentence and inventing a claim
|
|
245
|
+
* ("5 reasons why you should hire agents").
|
|
246
|
+
*/
|
|
247
|
+
const MAX_QUALIFIERS = 4;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Words that TERMINATE a qualifier run instead of decorating it:
|
|
251
|
+
* - dimension nouns — the number belongs to THAT dimension, not to the one
|
|
252
|
+
* further right. "11 workflow agents" is a *workflow* count (graded by the
|
|
253
|
+
* workflows rule), and summing it into the agent dimension is precisely how
|
|
254
|
+
* 27 + 11 became 38.
|
|
255
|
+
* - connectives and articles — without them a single match could span two
|
|
256
|
+
* independent claims ("9 packs and 27 agents").
|
|
257
|
+
*/
|
|
258
|
+
const NOT_A_QUALIFIER = new Set(
|
|
259
|
+
`agent agents agente agentes agenten workflow workflows pack packs test tests
|
|
260
|
+
category categories catégorie catégories categoría categorías kategorie kategorien
|
|
261
|
+
framework frameworks language languages langue langues idioma idiomas sprache sprachen
|
|
262
|
+
skill skills phase phases level levels
|
|
263
|
+
and or plus with across of in on for to the a an
|
|
264
|
+
et ou des les la le du dans sur pour avec y con de en und oder mit für`.split(/\s+/)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Quantity approximators sitting in front of the number. "over 47 agents" is
|
|
269
|
+
* the same marketing hedge as "47+ agents" and must die the same way — the
|
|
270
|
+
* roster is an exact, derivable integer. Deliberately excludes "about" and
|
|
271
|
+
* "around", which read as "on the subject of" far too often to be safe.
|
|
272
|
+
*/
|
|
273
|
+
const APPROX_PREFIX_RE = new RegExp(
|
|
274
|
+
`(?:over|more than|nearly|almost|approximately|approx\\.?|up to|~|≈` +
|
|
275
|
+
`|plus de|environ|más de|mehr als)${GAP}$`,
|
|
276
|
+
'i'
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Build a /g/ claim regex for one dimension's noun alternatives.
|
|
281
|
+
*
|
|
282
|
+
* The leading `\b` is load-bearing: it forbids the number from starting inside
|
|
283
|
+
* a word. Without it a qualifier run happily bridges the stray digit in "E2E
|
|
284
|
+
* tests" into a claim of "2 tests", and reads `section508-agent` as a count.
|
|
285
|
+
* A claim's number is always a whole token.
|
|
286
|
+
*/
|
|
287
|
+
function claimRe(noun) {
|
|
288
|
+
return new RegExp(
|
|
289
|
+
`\\b(\\d+)${GAP}(\\+)?${GAP}((?:${WORD}${GAP}){0,${MAX_QUALIFIERS}}?)(?:${noun})\\b`,
|
|
290
|
+
'gi'
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const AGENTS_RE = claimRe('agents?|agentes|agenten');
|
|
295
|
+
const WORKFLOWS_RE = claimRe('workflows?');
|
|
296
|
+
const CATEGORIES_RE = claimRe('categories|catégories|categorías|Kategorien');
|
|
297
|
+
const PACKS_RE = claimRe('packs?');
|
|
298
|
+
const TESTS_RE = claimRe('tests?');
|
|
299
|
+
const FRAMEWORKS_RE = claimRe('frameworks?');
|
|
300
|
+
const LANGUAGES_RE = claimRe('languages?|langues?|idiomas?|Sprachen?');
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Collect every claim `re` finds on `line`.
|
|
304
|
+
* `re` must be a /g/ regex; it is reset before use so it is safe to reuse.
|
|
305
|
+
* Matches whose qualifier run crosses a NOT_A_QUALIFIER word are dropped: the
|
|
306
|
+
* number belongs to a different dimension (or a different sentence).
|
|
307
|
+
* @returns {{value:number, plus:boolean, approx:boolean, text:string}[]}
|
|
308
|
+
*/
|
|
309
|
+
function matchAll(re, line) {
|
|
310
|
+
re.lastIndex = 0;
|
|
311
|
+
const out = [];
|
|
312
|
+
let m;
|
|
313
|
+
while ((m = re.exec(line)) !== null) {
|
|
314
|
+
const qualifiers = (m[3] || '').split(/[^A-Za-zÀ-ÖØ-öø-ÿ]+/).filter(Boolean);
|
|
315
|
+
if (qualifiers.some((w) => NOT_A_QUALIFIER.has(w.toLowerCase()))) continue;
|
|
316
|
+
out.push({
|
|
317
|
+
value: Number(m[1]),
|
|
318
|
+
plus: m[2] === '+',
|
|
319
|
+
approx: APPROX_PREFIX_RE.test(line.slice(0, m.index)),
|
|
320
|
+
// Markers are dropped from the echoed text so a bolded claim reports as
|
|
321
|
+
// `38 compliance agents`, not `38** compliance agents`. file:line locates it.
|
|
322
|
+
text: m[0].replace(/[*`]+/g, ' ').replace(/\s+/g, ' ').trim(),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Check one line against the derived truth.
|
|
330
|
+
* @param {string} line
|
|
331
|
+
* @param {object} truth output of deriveTruth()
|
|
332
|
+
* @param {{foreignSubject?: boolean}} [opts] foreignSubject suppresses the
|
|
333
|
+
* product-roster rule only (the line counts a DIFFERENT product's agents).
|
|
334
|
+
* @returns {string[]} human-readable drift descriptions (empty when the line is fine)
|
|
335
|
+
*/
|
|
336
|
+
function checkLine(line, truth, { foreignSubject = false } = {}) {
|
|
337
|
+
const problems = [];
|
|
338
|
+
const shieldLine = SHIELD_ANCHOR.test(line);
|
|
339
|
+
const devStudioLine = DEV_STUDIO_ANCHOR.test(line);
|
|
340
|
+
|
|
341
|
+
if (shieldLine) {
|
|
342
|
+
for (const { value, text } of matchAll(AGENTS_RE, line)) {
|
|
343
|
+
// Per-category headings ("5 agents") are legitimate; only reject the
|
|
344
|
+
// values that misstate the pack-wide total. Anything that is not a
|
|
345
|
+
// declared per-category size and not 27 is a conflation.
|
|
346
|
+
if (value !== truth.shieldAgents && !isPerCategorySize(value, truth)) {
|
|
347
|
+
problems.push(
|
|
348
|
+
`"${text.trim()}" — Shield has ${truth.shieldAgents} compliance agents` +
|
|
349
|
+
(value === truth.shieldAgents + truth.shieldWorkflows
|
|
350
|
+
? ` (${truth.shieldAgents} agents + ${truth.shieldWorkflows} workflows conflated into one "agents" figure)`
|
|
351
|
+
: '')
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const { value, text } of matchAll(WORKFLOWS_RE, line)) {
|
|
356
|
+
if (value !== truth.shieldWorkflows) {
|
|
357
|
+
problems.push(`"${text.trim()}" — Shield has ${truth.shieldWorkflows} workflows`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const { value, text } of matchAll(CATEGORIES_RE, line)) {
|
|
361
|
+
if (value !== truth.shieldCategories) {
|
|
362
|
+
problems.push(
|
|
363
|
+
`"${text.trim()}" — Shield has ${truth.shieldCategories} agent categories` +
|
|
364
|
+
(value === truth.shieldCategories + 1
|
|
365
|
+
? ' (the workflows/ directory is not a category)'
|
|
366
|
+
: '')
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (devStudioLine) {
|
|
373
|
+
for (const { value, text } of matchAll(WORKFLOWS_RE, line)) {
|
|
374
|
+
if (value !== truth.devStudioWorkflows) {
|
|
375
|
+
problems.push(`"${text.trim()}" — Dev Studio has ${truth.devStudioWorkflows} workflows`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// A "+" (or an "over"/"more than") on an agent total is always a marketing
|
|
381
|
+
// approximation: the roster is an exact, derivable integer. This is the rule
|
|
382
|
+
// that kills "56+ agents" — and "over 56 agents", which is the same hedge
|
|
383
|
+
// wearing a different hat.
|
|
384
|
+
for (const { plus, approx, text } of matchAll(AGENTS_RE, line)) {
|
|
385
|
+
if (!plus && !approx) continue;
|
|
386
|
+
problems.push(
|
|
387
|
+
`"${text.trim()}" — agent totals are exact and derivable, not approximate` +
|
|
388
|
+
` (the roster is ${truth.totalAgents}; "56+" came from Dev Studio's *skills* count)`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// The roster rule. Shield-anchored lines are graded by the stricter rule
|
|
393
|
+
// above; everywhere else, any number attached to "agents" must be one this
|
|
394
|
+
// repo can actually derive. Default-deny is the point: it is what makes the
|
|
395
|
+
// tracked "47 agents" claims fail the day the roster stops being 47, and what
|
|
396
|
+
// catches a number ("999") that corresponds to nothing at all.
|
|
397
|
+
if (!shieldLine && !foreignSubject) {
|
|
398
|
+
for (const { value, plus, approx, text } of matchAll(AGENTS_RE, line)) {
|
|
399
|
+
if (plus || approx) continue; // already reported as an approximation
|
|
400
|
+
if (!truth.agentCounts.has(value)) {
|
|
401
|
+
problems.push(
|
|
402
|
+
`"${text.trim()}" — no derived agent count equals ${value} (roster: ` +
|
|
403
|
+
`${truth.totalAgents} total = ${truth.installerAgents} installer + ` +
|
|
404
|
+
`${truth.shieldAgents} shield + ${truth.devStudioSubAgents} dev-studio sub-agents)`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Same default-deny for the workflow dimension. Anchored lines are graded
|
|
411
|
+
// against the one pack they name; an unanchored line must still quote a real
|
|
412
|
+
// total (this is what catches a pack description whose own name sits on the
|
|
413
|
+
// line above it).
|
|
414
|
+
if (!shieldLine && !devStudioLine) {
|
|
415
|
+
for (const { value, text } of matchAll(WORKFLOWS_RE, line)) {
|
|
416
|
+
if (!truth.workflowCounts.has(value)) {
|
|
417
|
+
problems.push(
|
|
418
|
+
`"${text.trim()}" — no pack has ${value} workflows (Shield ${truth.shieldWorkflows},` +
|
|
419
|
+
` Dev Studio ${truth.devStudioWorkflows})`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Packs and languages are exact derivable integers, so — like the agent
|
|
426
|
+
// roster — they get no hedge. Modelling each hedge's arithmetic instead would
|
|
427
|
+
// mean conceding that "10+ languages" (>= 10, true) and "more than 10
|
|
428
|
+
// languages" (> 10, false) are different claims about the same 10; the honest
|
|
429
|
+
// rule is that a number you can derive is simply stated.
|
|
430
|
+
for (const { value, plus, approx, text } of matchAll(PACKS_RE, line)) {
|
|
431
|
+
if (plus || approx) {
|
|
432
|
+
problems.push(
|
|
433
|
+
`"${text.trim()}" — the pack count is exact and derivable, not approximate` +
|
|
434
|
+
` (the registry declares ${truth.packCount})`
|
|
435
|
+
);
|
|
436
|
+
} else if (value !== truth.packCount) {
|
|
437
|
+
problems.push(`"${text.trim()}" — the registry declares ${truth.packCount} packs`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const { value, plus, approx, text } of matchAll(LANGUAGES_RE, line)) {
|
|
442
|
+
if (plus || approx) {
|
|
443
|
+
problems.push(
|
|
444
|
+
`"${text.trim()}" — the language count is exact and derivable, not approximate` +
|
|
445
|
+
` (tools/cli/i18n.js declares ${truth.languages.length})`
|
|
446
|
+
);
|
|
447
|
+
} else if (value !== truth.languages.length) {
|
|
448
|
+
problems.push(
|
|
449
|
+
`"${text.trim()}" — tools/cli/i18n.js declares ${truth.languages.length} languages` +
|
|
450
|
+
` (${truth.languages.join(', ')})`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Frameworks are the one dimension gated as a LOWER BOUND. Unlike the rosters
|
|
456
|
+
// above, the exact claims are per-category groupings ("AI Governance (3
|
|
457
|
+
// frameworks)") that are NOT derivable — registry categories carry agents,
|
|
458
|
+
// not compliance_tags — so an equality rule would fire on true statements.
|
|
459
|
+
// The defect that remains checkable is the overclaim: promising more
|
|
460
|
+
// frameworks than the pack actually tags. "25+" stays honest while 26 exist.
|
|
461
|
+
for (const { value, text } of matchAll(FRAMEWORKS_RE, line)) {
|
|
462
|
+
if (value > truth.shieldFrameworks) {
|
|
463
|
+
problems.push(
|
|
464
|
+
`"${text.trim()}" — Shield tags ${truth.shieldFrameworks} compliance frameworks`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return problems;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Per-category agent-group sizes that may legitimately appear near "agents". */
|
|
473
|
+
function isPerCategorySize(value, truth) {
|
|
474
|
+
return truth.perCategorySizes.has(value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* ── Scan ───────────────────────────────────────────────────────────────── */
|
|
478
|
+
|
|
479
|
+
/** Files tracked by git, restricted to the declared truth surfaces. */
|
|
480
|
+
function trackedTruthSurfaces() {
|
|
481
|
+
let tracked;
|
|
482
|
+
try {
|
|
483
|
+
tracked = new Set(
|
|
484
|
+
execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8' })
|
|
485
|
+
.split('\n')
|
|
486
|
+
.filter(Boolean)
|
|
487
|
+
);
|
|
488
|
+
} catch {
|
|
489
|
+
tracked = null; // not a git checkout (e.g. an npm tarball) → scan what exists
|
|
490
|
+
}
|
|
491
|
+
return TRUTH_SURFACES.filter((f) => {
|
|
492
|
+
if (tracked && !tracked.has(f)) return false;
|
|
493
|
+
return fs.existsSync(path.join(REPO_ROOT, f));
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Scan the truth surfaces.
|
|
499
|
+
* @returns {{ ok: boolean, drifts: string[], unverified: string[], truth: object }}
|
|
500
|
+
*/
|
|
501
|
+
function check({ registryPath = DEFAULT_REGISTRY_PATH, testCount = null } = {}) {
|
|
502
|
+
const truth = deriveTruth(registryPath);
|
|
503
|
+
const drifts = [...truth.derivationMismatches];
|
|
504
|
+
const unverified = [];
|
|
505
|
+
|
|
506
|
+
for (const file of trackedTruthSurfaces()) {
|
|
507
|
+
const lines = fs.readFileSync(path.join(REPO_ROOT, file), 'utf8').split(/\r?\n/);
|
|
508
|
+
let inForeignTable = false;
|
|
509
|
+
lines.forEach((line, i) => {
|
|
510
|
+
if (isFrozenHistoryRow(line)) return;
|
|
511
|
+
if (FOREIGN_TABLE_HEADER_RE.test(line)) {
|
|
512
|
+
inForeignTable = true;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (inForeignTable && !line.trimStart().startsWith('|')) inForeignTable = false;
|
|
516
|
+
|
|
517
|
+
const foreignSubject = inForeignTable || FOREIGN_SUBJECT_ANCHOR.test(line);
|
|
518
|
+
for (const problem of checkLine(line, truth, { foreignSubject })) {
|
|
519
|
+
drifts.push(`${file}:${i + 1}: ${problem}`);
|
|
520
|
+
}
|
|
521
|
+
for (const { value, text } of matchAll(TESTS_RE, line)) {
|
|
522
|
+
if (testCount === null) {
|
|
523
|
+
unverified.push(`${file}:${i + 1}: "${text.trim()}"`);
|
|
524
|
+
} else if (value !== testCount) {
|
|
525
|
+
drifts.push(`${file}:${i + 1}: "${text.trim()}" — the suite reports ${testCount} tests`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return { ok: drifts.length === 0, drifts, unverified, truth };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Read the test-count rule's arming variable.
|
|
536
|
+
* Unset/empty means "not armed" (never a failure). Anything else must be a
|
|
537
|
+
* plain non-negative integer: `Number(" ")` is 0 and `Number("-1")` is an
|
|
538
|
+
* integer, so a bare Number()+isInteger check would silently gate against a
|
|
539
|
+
* garbage total.
|
|
540
|
+
* @returns {{armed: boolean, testCount: number|null, error: string|null}}
|
|
541
|
+
*/
|
|
542
|
+
function readTestCountEnv(raw) {
|
|
543
|
+
if (typeof raw !== 'string' || raw.trim() === '') {
|
|
544
|
+
return { armed: false, testCount: null, error: null };
|
|
545
|
+
}
|
|
546
|
+
if (!/^\d+$/.test(raw.trim())) {
|
|
547
|
+
return {
|
|
548
|
+
armed: true,
|
|
549
|
+
testCount: null,
|
|
550
|
+
error: `BMAD_PLUS_TEST_COUNT must be a non-negative integer (got "${raw}")`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { armed: true, testCount: Number(raw.trim()), error: null };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* ── CLI ────────────────────────────────────────────────────────────────── */
|
|
557
|
+
|
|
558
|
+
function main(argv) {
|
|
559
|
+
const args = argv.slice(2);
|
|
560
|
+
const { armed, testCount, error } = readTestCountEnv(process.env.BMAD_PLUS_TEST_COUNT);
|
|
561
|
+
if (error) {
|
|
562
|
+
console.error(error);
|
|
563
|
+
return 1;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const result = check({ testCount });
|
|
567
|
+
|
|
568
|
+
if (args.includes('--print')) {
|
|
569
|
+
const { derivationMismatches, ...facts } = result.truth;
|
|
570
|
+
void derivationMismatches;
|
|
571
|
+
const asArray = (_k, v) => (v instanceof Set ? [...v].sort((a, b) => a - b) : v);
|
|
572
|
+
console.log(JSON.stringify(facts, asArray, 2));
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const partial = result.unverified.length > 0;
|
|
577
|
+
|
|
578
|
+
if (result.ok) {
|
|
579
|
+
console.log(
|
|
580
|
+
`${partial ? 'OK (PARTIAL)' : 'OK'} — no count drift. Derived from registry.yaml + files on disk: ` +
|
|
581
|
+
`${result.truth.shieldAgents} shield agents + ${result.truth.shieldWorkflows} shield workflows, ` +
|
|
582
|
+
`${result.truth.devStudioWorkflows} dev-studio workflows, ${result.truth.totalAgents} agents total, ` +
|
|
583
|
+
`${result.truth.packCount} packs, ${result.truth.languages.length} languages.`
|
|
584
|
+
);
|
|
585
|
+
} else {
|
|
586
|
+
console.error('COUNT DRIFT DETECTED — hand-typed numbers contradict registry.yaml:');
|
|
587
|
+
for (const d of result.drifts) console.error(` - ${d}`);
|
|
588
|
+
console.error(
|
|
589
|
+
'Fix the claim (or registry.yaml if the registry is what is wrong), then re-run:'
|
|
590
|
+
);
|
|
591
|
+
console.error(' node tools/build/check-counts.js');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Reported on BOTH paths, on purpose. A green run with the variable unset
|
|
595
|
+
// must not read as full coverage — and a red run must not bury the fact that
|
|
596
|
+
// the test claims went unchecked on top of whatever else broke.
|
|
597
|
+
if (partial) {
|
|
598
|
+
console.warn('');
|
|
599
|
+
console.warn(
|
|
600
|
+
`!! NOT GATED — ${result.unverified.length} test-count claim(s) were NOT checked ` +
|
|
601
|
+
`(BMAD_PLUS_TEST_COUNT unset):`
|
|
602
|
+
);
|
|
603
|
+
for (const u of result.unverified) console.warn(` ${u}`);
|
|
604
|
+
console.warn(' This run does not cover those numbers. To gate them, pass the');
|
|
605
|
+
console.warn(' Jest-reported total:');
|
|
606
|
+
console.warn(' BMAD_PLUS_TEST_COUNT=<total> node tools/build/check-counts.js');
|
|
607
|
+
} else if (armed) {
|
|
608
|
+
console.log(`Test-count claims gated against BMAD_PLUS_TEST_COUNT=${testCount}.`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return result.ok ? 0 : 1;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (require.main === module) {
|
|
615
|
+
process.exitCode = main(process.argv);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
module.exports = {
|
|
619
|
+
REPO_ROOT,
|
|
620
|
+
TRUTH_SURFACES,
|
|
621
|
+
deriveTruth,
|
|
622
|
+
checkLine,
|
|
623
|
+
isFrozenHistoryRow,
|
|
624
|
+
readTestCountEnv,
|
|
625
|
+
trackedTruthSurfaces,
|
|
626
|
+
check,
|
|
627
|
+
main,
|
|
628
|
+
};
|
|
@@ -100,7 +100,7 @@ Agents MUST follow these behavioral principles:
|
|
|
100
100
|
|
|
101
101
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
102
102
|
|
|
103
|
-
- Product: BMAD+ v0.12.
|
|
103
|
+
- Product: BMAD+ v0.12.2 (derived from BMAD-METHOD v6.6.0)
|
|
104
104
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
105
105
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
106
106
|
- Installer agents (14 across all packs):
|
|
@@ -105,7 +105,7 @@ Agents MUST follow these behavioral principles:
|
|
|
105
105
|
|
|
106
106
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
107
107
|
|
|
108
|
-
- Product: BMAD+ v0.12.
|
|
108
|
+
- Product: BMAD+ v0.12.2 (derived from BMAD-METHOD v6.6.0)
|
|
109
109
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
110
110
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
111
111
|
- Installer agents (14 across all packs):
|
|
@@ -100,7 +100,7 @@ Agents MUST follow these behavioral principles:
|
|
|
100
100
|
|
|
101
101
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
102
102
|
|
|
103
|
-
- Product: BMAD+ v0.12.
|
|
103
|
+
- Product: BMAD+ v0.12.2 (derived from BMAD-METHOD v6.6.0)
|
|
104
104
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
105
105
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
106
106
|
- Installer agents (14 across all packs):
|
|
@@ -98,7 +98,7 @@ Agents MUST follow these behavioral principles:
|
|
|
98
98
|
|
|
99
99
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
100
100
|
|
|
101
|
-
- Product: BMAD+ v0.12.
|
|
101
|
+
- Product: BMAD+ v0.12.2 (derived from BMAD-METHOD v6.6.0)
|
|
102
102
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
103
103
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
104
104
|
- Installer agents (14 across all packs):
|
|
@@ -101,7 +101,7 @@ Agents MUST follow these behavioral principles:
|
|
|
101
101
|
|
|
102
102
|
## Registry facts (computed from registry.yaml — never hand-typed)
|
|
103
103
|
|
|
104
|
-
- Product: BMAD+ v0.12.
|
|
104
|
+
- Product: BMAD+ v0.12.2 (derived from BMAD-METHOD v6.6.0)
|
|
105
105
|
- Models supported: claude, gpt, gemini, local (model-agnostic by contract)
|
|
106
106
|
- Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
|
|
107
107
|
- Installer agents (14 across all packs):
|