@nforma.ai/nforma 0.2.1
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/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/generate-traceability-matrix.cjs
|
|
4
|
+
// Generates .planning/formal/traceability-matrix.json — a bidirectional index linking
|
|
5
|
+
// requirements to formal properties and vice versa, with coverage statistics.
|
|
6
|
+
//
|
|
7
|
+
// Data sources:
|
|
8
|
+
// 1. extract-annotations.cjs output (primary — property-level)
|
|
9
|
+
// 2. .planning/formal/model-registry.json (fallback — model-level)
|
|
10
|
+
// 3. .planning/formal/check-results.ndjson (verification status)
|
|
11
|
+
// 4. .planning/formal/requirements.json (total requirement inventory)
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// node bin/generate-traceability-matrix.cjs # write to .planning/formal/traceability-matrix.json
|
|
15
|
+
// node bin/generate-traceability-matrix.cjs --json # print JSON to stdout
|
|
16
|
+
// node bin/generate-traceability-matrix.cjs --quiet # suppress summary output
|
|
17
|
+
//
|
|
18
|
+
// Requirements: TRACE-01, TRACE-02, TRACE-04, ANNOT-05
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const TAG = '[generate-traceability-matrix]';
|
|
25
|
+
let ROOT = process.cwd();
|
|
26
|
+
|
|
27
|
+
// Parse --project-root (overrides CWD-based ROOT for cross-repo usage)
|
|
28
|
+
for (const arg of process.argv.slice(2)) {
|
|
29
|
+
if (arg.startsWith('--project-root=')) {
|
|
30
|
+
ROOT = path.resolve(arg.slice('--project-root='.length));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ANNOTATIONS_SCRIPT = path.join(__dirname, 'extract-annotations.cjs');
|
|
35
|
+
const REGISTRY_PATH = path.join(ROOT, '.planning', 'formal', 'model-registry.json');
|
|
36
|
+
const NDJSON_PATH = path.join(ROOT, '.planning', 'formal', 'check-results.ndjson');
|
|
37
|
+
const REQUIREMENTS_PATH = path.join(ROOT, '.planning', 'formal', 'requirements.json');
|
|
38
|
+
const OUTPUT_PATH = path.join(ROOT, '.planning', 'formal', 'traceability-matrix.json');
|
|
39
|
+
|
|
40
|
+
// ── CLI flags ───────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
const jsonMode = args.includes('--json');
|
|
44
|
+
const quietMode = args.includes('--quiet');
|
|
45
|
+
|
|
46
|
+
// ── Data Loading ────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load annotations from extract-annotations.cjs (primary source).
|
|
50
|
+
* Returns { model_file: [{ property, requirement_ids }] } or {} on failure.
|
|
51
|
+
*/
|
|
52
|
+
function loadAnnotations() {
|
|
53
|
+
try {
|
|
54
|
+
const result = spawnSync(process.execPath, [ANNOTATIONS_SCRIPT, '--project-root=' + ROOT], {
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
cwd: ROOT,
|
|
57
|
+
timeout: 30000,
|
|
58
|
+
});
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
process.stderr.write(TAG + ' warn: extract-annotations.cjs exited ' + result.status + '\n');
|
|
61
|
+
if (result.stderr) process.stderr.write(TAG + ' stderr: ' + result.stderr.trim() + '\n');
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(result.stdout);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
process.stderr.write(TAG + ' warn: extract-annotations.cjs failed: ' + err.message + '\n');
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load model-registry.json (fallback source).
|
|
73
|
+
* Returns { models: { file: { requirements: [...] } } } or throws on missing.
|
|
74
|
+
*/
|
|
75
|
+
function loadRegistry() {
|
|
76
|
+
if (!fs.existsSync(REGISTRY_PATH)) {
|
|
77
|
+
process.stderr.write(TAG + ' FATAL: model-registry.json not found at ' + REGISTRY_PATH + '\n');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load check-results.ndjson (verification status).
|
|
85
|
+
* Returns array of parsed check result objects, or [] if missing/empty.
|
|
86
|
+
*/
|
|
87
|
+
function loadCheckResults() {
|
|
88
|
+
if (!fs.existsSync(NDJSON_PATH)) return [];
|
|
89
|
+
const content = fs.readFileSync(NDJSON_PATH, 'utf8').trim();
|
|
90
|
+
if (!content) return [];
|
|
91
|
+
const entries = [];
|
|
92
|
+
for (const line of content.split('\n')) {
|
|
93
|
+
if (!line.trim()) continue;
|
|
94
|
+
try {
|
|
95
|
+
entries.push(JSON.parse(line));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
process.stderr.write(TAG + ' warn: skipping invalid NDJSON line\n');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Load requirements.json (reference for coverage stats).
|
|
105
|
+
* Returns array of requirement objects, or [] if missing.
|
|
106
|
+
*/
|
|
107
|
+
function loadRequirements() {
|
|
108
|
+
if (!fs.existsSync(REQUIREMENTS_PATH)) {
|
|
109
|
+
process.stderr.write(TAG + ' warn: requirements.json not found — coverage stats will show 0 total\n');
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const data = JSON.parse(fs.readFileSync(REQUIREMENTS_PATH, 'utf8'));
|
|
114
|
+
return data.requirements || [];
|
|
115
|
+
} catch (err) {
|
|
116
|
+
process.stderr.write(TAG + ' warn: requirements.json parse error: ' + err.message + '\n');
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Load unit-test-coverage.json sidecar (generated by formal-test-sync.cjs).
|
|
123
|
+
* Returns the sidecar object or null if missing/unparseable.
|
|
124
|
+
*/
|
|
125
|
+
function loadUnitTestCoverage() {
|
|
126
|
+
const UNIT_TEST_COVERAGE_PATH = path.join(ROOT, '.planning', 'formal', 'unit-test-coverage.json');
|
|
127
|
+
if (!fs.existsSync(UNIT_TEST_COVERAGE_PATH)) return null;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(fs.readFileSync(UNIT_TEST_COVERAGE_PATH, 'utf8'));
|
|
130
|
+
} catch (err) {
|
|
131
|
+
process.stderr.write(TAG + ' warn: unit-test-coverage.json parse error: ' + err.message + '\n');
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Check Result Matching ───────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build a map of requirement_id -> latest check result from NDJSON entries.
|
|
140
|
+
* For each check result, distribute its result to all its requirement_ids.
|
|
141
|
+
* Last entry wins for a given check_id.
|
|
142
|
+
*/
|
|
143
|
+
function buildCheckResultMap(checkResults) {
|
|
144
|
+
// Deduplicate by check_id (last entry wins)
|
|
145
|
+
const byCheckId = new Map();
|
|
146
|
+
for (const entry of checkResults) {
|
|
147
|
+
if (entry.check_id) {
|
|
148
|
+
byCheckId.set(entry.check_id, entry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Map: requirement_id -> { result, check_id }
|
|
153
|
+
const reqMap = new Map();
|
|
154
|
+
for (const entry of byCheckId.values()) {
|
|
155
|
+
const reqIds = entry.requirement_ids || [];
|
|
156
|
+
for (const reqId of reqIds) {
|
|
157
|
+
reqMap.set(reqId, {
|
|
158
|
+
result: entry.result,
|
|
159
|
+
check_id: entry.check_id,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return reqMap;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Bidirectional Validation (TRACE-04) ─────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Cross-validate model-registry requirements arrays against requirements.json
|
|
170
|
+
* formal_models arrays. Detects asymmetric links and stale references.
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} registry — parsed model-registry.json
|
|
173
|
+
* @param {Array} requirements — array of requirement objects from requirements.json
|
|
174
|
+
* @returns {{ asymmetric_links: Array, stale_links: Array, summary: Object }}
|
|
175
|
+
*/
|
|
176
|
+
function validateBidirectionalLinks(registry, requirements) {
|
|
177
|
+
const asymmetricLinks = [];
|
|
178
|
+
const staleLinks = [];
|
|
179
|
+
const checkedPairs = new Set(); // "modelFile|reqId" to deduplicate
|
|
180
|
+
|
|
181
|
+
// Build maps
|
|
182
|
+
const registryReqs = {}; // modelFile -> Set<reqId>
|
|
183
|
+
for (const [modelFile, entry] of Object.entries(registry.models || {})) {
|
|
184
|
+
const reqs = entry.requirements || [];
|
|
185
|
+
if (reqs.length > 0) {
|
|
186
|
+
registryReqs[modelFile] = new Set(reqs);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const reqModels = {}; // reqId -> Set<modelFile>
|
|
191
|
+
const reqIdSet = new Set();
|
|
192
|
+
for (const req of requirements) {
|
|
193
|
+
reqIdSet.add(req.id);
|
|
194
|
+
const models = req.formal_models || [];
|
|
195
|
+
if (models.length > 0) {
|
|
196
|
+
reqModels[req.id] = new Set(models);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Direction 1: model claims requirement — check if requirement claims model back
|
|
201
|
+
for (const [modelFile, reqIds] of Object.entries(registryReqs)) {
|
|
202
|
+
for (const reqId of reqIds) {
|
|
203
|
+
const pairKey = modelFile + '|' + reqId;
|
|
204
|
+
if (checkedPairs.has(pairKey)) continue;
|
|
205
|
+
checkedPairs.add(pairKey);
|
|
206
|
+
|
|
207
|
+
// Check if requirement ID exists at all
|
|
208
|
+
if (!reqIdSet.has(reqId)) {
|
|
209
|
+
staleLinks.push({
|
|
210
|
+
type: 'unknown_requirement_id',
|
|
211
|
+
reference: reqId,
|
|
212
|
+
referenced_by: 'model-registry ' + modelFile + ' requirements',
|
|
213
|
+
});
|
|
214
|
+
process.stderr.write(TAG + ' warn: stale link — model-registry ' + modelFile + ' references requirement ' + reqId + ' which does not exist in requirements.json\n');
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check if requirement's formal_models lists this model
|
|
219
|
+
const modelsForReq = reqModels[reqId];
|
|
220
|
+
if (!modelsForReq || !modelsForReq.has(modelFile)) {
|
|
221
|
+
asymmetricLinks.push({
|
|
222
|
+
model_file: modelFile,
|
|
223
|
+
requirement_id: reqId,
|
|
224
|
+
direction: 'model_claims_requirement',
|
|
225
|
+
detail: 'model-registry lists ' + reqId + ' for ' + modelFile + ' but requirements.json ' + reqId + ' does not list ' + modelFile + ' in formal_models',
|
|
226
|
+
});
|
|
227
|
+
process.stderr.write(TAG + ' warn: asymmetric link — model-registry lists ' + reqId + ' for ' + modelFile + ' but requirements.json ' + reqId + ' does not list ' + modelFile + ' in formal_models\n');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Direction 2: requirement claims model — check if model claims requirement back
|
|
233
|
+
for (const [reqId, modelFiles] of Object.entries(reqModels)) {
|
|
234
|
+
for (const modelFile of modelFiles) {
|
|
235
|
+
const pairKey = modelFile + '|' + reqId;
|
|
236
|
+
if (checkedPairs.has(pairKey)) {
|
|
237
|
+
// Already checked from direction 1 — skip
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
checkedPairs.add(pairKey);
|
|
241
|
+
|
|
242
|
+
// Check if model file exists in registry
|
|
243
|
+
if (!registry.models || !registry.models[modelFile]) {
|
|
244
|
+
// Check if file exists on disk
|
|
245
|
+
const diskPath = path.join(ROOT, modelFile);
|
|
246
|
+
if (!fs.existsSync(diskPath)) {
|
|
247
|
+
staleLinks.push({
|
|
248
|
+
type: 'missing_model_file',
|
|
249
|
+
reference: modelFile,
|
|
250
|
+
referenced_by: 'requirements.json ' + reqId + ' formal_models',
|
|
251
|
+
});
|
|
252
|
+
process.stderr.write(TAG + ' warn: stale link — requirements.json ' + reqId + ' references ' + modelFile + ' but file does not exist\n');
|
|
253
|
+
} else {
|
|
254
|
+
// File exists on disk but not in registry
|
|
255
|
+
asymmetricLinks.push({
|
|
256
|
+
model_file: modelFile,
|
|
257
|
+
requirement_id: reqId,
|
|
258
|
+
direction: 'requirement_claims_model',
|
|
259
|
+
detail: 'requirements.json ' + reqId + ' lists ' + modelFile + ' in formal_models but model file is not in model-registry.json',
|
|
260
|
+
});
|
|
261
|
+
process.stderr.write(TAG + ' warn: asymmetric link — requirements.json ' + reqId + ' lists ' + modelFile + ' in formal_models but model file is not in model-registry.json\n');
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Model is in registry — check if it claims this requirement back
|
|
267
|
+
const regsForModel = registryReqs[modelFile];
|
|
268
|
+
if (!regsForModel || !regsForModel.has(reqId)) {
|
|
269
|
+
asymmetricLinks.push({
|
|
270
|
+
model_file: modelFile,
|
|
271
|
+
requirement_id: reqId,
|
|
272
|
+
direction: 'requirement_claims_model',
|
|
273
|
+
detail: 'requirements.json ' + reqId + ' lists ' + modelFile + ' in formal_models but model-registry ' + modelFile + ' does not list ' + reqId + ' in requirements',
|
|
274
|
+
});
|
|
275
|
+
process.stderr.write(TAG + ' warn: asymmetric link — requirements.json ' + reqId + ' lists ' + modelFile + ' in formal_models but model-registry ' + modelFile + ' does not list ' + reqId + ' in requirements\n');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const totalChecked = checkedPairs.size;
|
|
281
|
+
const asymmetricCount = asymmetricLinks.length;
|
|
282
|
+
const staleCount = staleLinks.length;
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
asymmetric_links: asymmetricLinks,
|
|
286
|
+
stale_links: staleLinks,
|
|
287
|
+
summary: {
|
|
288
|
+
total_checked: totalChecked,
|
|
289
|
+
asymmetric_count: asymmetricCount,
|
|
290
|
+
stale_count: staleCount,
|
|
291
|
+
clean: asymmetricCount === 0 && staleCount === 0,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── State-Space Analysis (DECOMP-04) ────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Load state-space analysis by spawning bin/analyze-state-space.cjs.
|
|
300
|
+
* Returns the full report object, or {} on failure (fail-open).
|
|
301
|
+
*/
|
|
302
|
+
function loadStateSpaceAnalysis() {
|
|
303
|
+
const analyzerPath = path.join(__dirname, 'analyze-state-space.cjs');
|
|
304
|
+
if (!fs.existsSync(analyzerPath)) {
|
|
305
|
+
process.stderr.write(TAG + ' warn: analyze-state-space.cjs not found — state_space section will be empty\n');
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const result = spawnSync(process.execPath, [analyzerPath, '--json', '--project-root=' + ROOT], {
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
cwd: ROOT,
|
|
312
|
+
timeout: 30000,
|
|
313
|
+
});
|
|
314
|
+
if (result.status !== 0) {
|
|
315
|
+
process.stderr.write(TAG + ' warn: analyze-state-space.cjs exited ' + result.status + '\n');
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
return JSON.parse(result.stdout);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
process.stderr.write(TAG + ' warn: analyze-state-space.cjs failed: ' + err.message + '\n');
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Matrix Construction ─────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
function buildMatrix() {
|
|
328
|
+
// Load all data sources
|
|
329
|
+
const annotations = loadAnnotations();
|
|
330
|
+
const registry = loadRegistry();
|
|
331
|
+
const checkResults = loadCheckResults();
|
|
332
|
+
const requirements = loadRequirements();
|
|
333
|
+
const unitTestCoverage = loadUnitTestCoverage();
|
|
334
|
+
|
|
335
|
+
const checkMap = buildCheckResultMap(checkResults);
|
|
336
|
+
|
|
337
|
+
// Track which files had annotations vs fallback
|
|
338
|
+
const annotatedFiles = new Set(Object.keys(annotations));
|
|
339
|
+
let fallbackCount = 0;
|
|
340
|
+
|
|
341
|
+
// Bidirectional indexes
|
|
342
|
+
const requirementsIndex = {}; // reqId -> { id, properties: [...] }
|
|
343
|
+
const propertiesIndex = {}; // "file::property" -> { ... }
|
|
344
|
+
|
|
345
|
+
// Track all covered requirement IDs
|
|
346
|
+
const coveredReqIds = new Set();
|
|
347
|
+
|
|
348
|
+
// ── Process annotation-sourced properties ──
|
|
349
|
+
|
|
350
|
+
let totalAnnotationProps = 0;
|
|
351
|
+
|
|
352
|
+
for (const [modelFile, props] of Object.entries(annotations)) {
|
|
353
|
+
for (const { property, requirement_ids } of props) {
|
|
354
|
+
totalAnnotationProps++;
|
|
355
|
+
const key = modelFile + '::' + property;
|
|
356
|
+
|
|
357
|
+
// Find check result for this property (match by any shared requirement_id)
|
|
358
|
+
let latestResult = null;
|
|
359
|
+
let checkId = null;
|
|
360
|
+
for (const reqId of requirement_ids) {
|
|
361
|
+
const cr = checkMap.get(reqId);
|
|
362
|
+
if (cr) {
|
|
363
|
+
latestResult = cr.result;
|
|
364
|
+
checkId = cr.check_id;
|
|
365
|
+
break; // use first match
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Build property entry
|
|
370
|
+
const propEntry = {
|
|
371
|
+
model_file: modelFile,
|
|
372
|
+
property_name: property,
|
|
373
|
+
requirement_ids: requirement_ids,
|
|
374
|
+
source: 'annotation',
|
|
375
|
+
latest_result: latestResult,
|
|
376
|
+
check_id: checkId,
|
|
377
|
+
};
|
|
378
|
+
propertiesIndex[key] = propEntry;
|
|
379
|
+
|
|
380
|
+
// Add to requirements index
|
|
381
|
+
for (const reqId of requirement_ids) {
|
|
382
|
+
coveredReqIds.add(reqId);
|
|
383
|
+
if (!requirementsIndex[reqId]) {
|
|
384
|
+
requirementsIndex[reqId] = { id: reqId, properties: [] };
|
|
385
|
+
}
|
|
386
|
+
requirementsIndex[reqId].properties.push({
|
|
387
|
+
model_file: modelFile,
|
|
388
|
+
property_name: property,
|
|
389
|
+
source: 'annotation',
|
|
390
|
+
latest_result: latestResult,
|
|
391
|
+
check_id: checkId,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Detect orphan
|
|
396
|
+
if (requirement_ids.length === 0) {
|
|
397
|
+
// Property with no requirements — will be counted as orphan
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Process model-registry fallback ──
|
|
403
|
+
|
|
404
|
+
const registryFiles = Object.keys(registry.models || {});
|
|
405
|
+
|
|
406
|
+
for (const modelFile of registryFiles) {
|
|
407
|
+
if (annotatedFiles.has(modelFile)) continue; // annotations exist — skip fallback
|
|
408
|
+
|
|
409
|
+
const entry = registry.models[modelFile];
|
|
410
|
+
const reqs = entry.requirements || [];
|
|
411
|
+
if (reqs.length === 0) continue; // no requirements in registry either
|
|
412
|
+
|
|
413
|
+
fallbackCount++;
|
|
414
|
+
process.stderr.write(TAG + ' warn: No annotations for ' + modelFile + ', using model-registry fallback\n');
|
|
415
|
+
|
|
416
|
+
const key = modelFile + '::(model-level)';
|
|
417
|
+
|
|
418
|
+
// Find check result by requirement overlap
|
|
419
|
+
let latestResult = null;
|
|
420
|
+
let checkId = null;
|
|
421
|
+
for (const reqId of reqs) {
|
|
422
|
+
const cr = checkMap.get(reqId);
|
|
423
|
+
if (cr) {
|
|
424
|
+
latestResult = cr.result;
|
|
425
|
+
checkId = cr.check_id;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const propEntry = {
|
|
431
|
+
model_file: modelFile,
|
|
432
|
+
property_name: '(model-level)',
|
|
433
|
+
requirement_ids: reqs,
|
|
434
|
+
source: 'model-registry',
|
|
435
|
+
latest_result: latestResult,
|
|
436
|
+
check_id: checkId,
|
|
437
|
+
};
|
|
438
|
+
propertiesIndex[key] = propEntry;
|
|
439
|
+
|
|
440
|
+
for (const reqId of reqs) {
|
|
441
|
+
coveredReqIds.add(reqId);
|
|
442
|
+
if (!requirementsIndex[reqId]) {
|
|
443
|
+
requirementsIndex[reqId] = { id: reqId, properties: [] };
|
|
444
|
+
}
|
|
445
|
+
requirementsIndex[reqId].properties.push({
|
|
446
|
+
model_file: modelFile,
|
|
447
|
+
property_name: '(model-level)',
|
|
448
|
+
source: 'model-registry',
|
|
449
|
+
latest_result: latestResult,
|
|
450
|
+
check_id: checkId,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ── Coverage Summary ──
|
|
456
|
+
|
|
457
|
+
const allReqIds = requirements.map(r => r.id);
|
|
458
|
+
const totalRequirements = allReqIds.length;
|
|
459
|
+
const coveredCount = allReqIds.filter(id => coveredReqIds.has(id)).length;
|
|
460
|
+
const coveragePercentage = totalRequirements > 0
|
|
461
|
+
? Math.round((coveredCount / totalRequirements) * 1000) / 10
|
|
462
|
+
: 0;
|
|
463
|
+
|
|
464
|
+
const uncoveredRequirements = allReqIds
|
|
465
|
+
.filter(id => !coveredReqIds.has(id))
|
|
466
|
+
.sort();
|
|
467
|
+
|
|
468
|
+
// Orphan detection: properties with empty requirement_ids
|
|
469
|
+
const orphanProperties = [];
|
|
470
|
+
for (const [key, prop] of Object.entries(propertiesIndex)) {
|
|
471
|
+
if (!prop.requirement_ids || prop.requirement_ids.length === 0) {
|
|
472
|
+
orphanProperties.push(key);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Bidirectional validation (TRACE-04) ──
|
|
477
|
+
|
|
478
|
+
const bidirectionalValidation = validateBidirectionalLinks(registry, requirements);
|
|
479
|
+
|
|
480
|
+
// ── State-space analysis (DECOMP-04) ──
|
|
481
|
+
|
|
482
|
+
const stateSpaceReport = loadStateSpaceAnalysis();
|
|
483
|
+
const stateSpaceSection = {};
|
|
484
|
+
|
|
485
|
+
if (stateSpaceReport.models) {
|
|
486
|
+
for (const [modelFile, analysis] of Object.entries(stateSpaceReport.models)) {
|
|
487
|
+
stateSpaceSection[modelFile] = {
|
|
488
|
+
risk_level: analysis.risk_level,
|
|
489
|
+
estimated_states: analysis.estimated_states,
|
|
490
|
+
has_unbounded: analysis.has_unbounded,
|
|
491
|
+
unbounded_domains: analysis.unbounded_domains || [],
|
|
492
|
+
risk_reason: analysis.risk_reason || null,
|
|
493
|
+
variable_count: (analysis.variables || []).length,
|
|
494
|
+
constant_count: (analysis.constants || []).length,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ── Merge unit test coverage ──
|
|
500
|
+
|
|
501
|
+
let unitTestCoverageMerged = 0;
|
|
502
|
+
if (unitTestCoverage && unitTestCoverage.requirements) {
|
|
503
|
+
for (const [reqId, utcData] of Object.entries(unitTestCoverage.requirements)) {
|
|
504
|
+
if (requirementsIndex[reqId]) {
|
|
505
|
+
requirementsIndex[reqId].unit_test_coverage = {
|
|
506
|
+
covered: utcData.covered,
|
|
507
|
+
test_cases: utcData.test_cases || [],
|
|
508
|
+
};
|
|
509
|
+
unitTestCoverageMerged++;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── Build matrix ──
|
|
515
|
+
|
|
516
|
+
const matrix = {
|
|
517
|
+
metadata: {
|
|
518
|
+
generated_at: new Date().toISOString(),
|
|
519
|
+
generator_version: '1.1',
|
|
520
|
+
data_sources: {
|
|
521
|
+
annotations: {
|
|
522
|
+
file_count: annotatedFiles.size,
|
|
523
|
+
property_count: totalAnnotationProps,
|
|
524
|
+
},
|
|
525
|
+
model_registry: {
|
|
526
|
+
file_count: registryFiles.length,
|
|
527
|
+
used_as_fallback: fallbackCount,
|
|
528
|
+
},
|
|
529
|
+
check_results: {
|
|
530
|
+
entry_count: checkResults.length,
|
|
531
|
+
},
|
|
532
|
+
unit_test_coverage: {
|
|
533
|
+
available: unitTestCoverage !== null,
|
|
534
|
+
requirements_matched: unitTestCoverageMerged,
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
requirements: requirementsIndex,
|
|
539
|
+
properties: propertiesIndex,
|
|
540
|
+
coverage_summary: {
|
|
541
|
+
total_requirements: totalRequirements,
|
|
542
|
+
covered_count: coveredCount,
|
|
543
|
+
coverage_percentage: coveragePercentage,
|
|
544
|
+
uncovered_requirements: uncoveredRequirements,
|
|
545
|
+
orphan_properties: orphanProperties,
|
|
546
|
+
},
|
|
547
|
+
bidirectional_validation: bidirectionalValidation,
|
|
548
|
+
state_space: stateSpaceSection,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return matrix;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ── Coverage Preservation (DECOMP-03) ───────────────────────────────────────
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Compare the newly generated matrix against the previous traceability-matrix.json
|
|
558
|
+
* on disk to detect per-requirement coverage regressions.
|
|
559
|
+
*
|
|
560
|
+
* Must be called BEFORE the new matrix is written to disk so the baseline file
|
|
561
|
+
* is still the old version.
|
|
562
|
+
*
|
|
563
|
+
* @param {Object} newMatrix — the newly built matrix object
|
|
564
|
+
* @returns {{ baseline_found, baseline_date, requirements_checked, regressions, summary }}
|
|
565
|
+
*/
|
|
566
|
+
function validateCoveragePreservation(newMatrix) {
|
|
567
|
+
// Attempt to load baseline (current file on disk)
|
|
568
|
+
let baseline = null;
|
|
569
|
+
try {
|
|
570
|
+
if (fs.existsSync(OUTPUT_PATH)) {
|
|
571
|
+
baseline = JSON.parse(fs.readFileSync(OUTPUT_PATH, 'utf8'));
|
|
572
|
+
}
|
|
573
|
+
} catch (err) {
|
|
574
|
+
process.stderr.write(TAG + ' warn: could not parse previous traceability-matrix.json — treating as no baseline: ' + err.message + '\n');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!baseline || !baseline.requirements) {
|
|
578
|
+
process.stderr.write(TAG + ' info: no previous traceability-matrix.json found — coverage preservation check skipped\n');
|
|
579
|
+
return {
|
|
580
|
+
baseline_found: false,
|
|
581
|
+
baseline_date: null,
|
|
582
|
+
requirements_checked: 0,
|
|
583
|
+
regressions: [],
|
|
584
|
+
summary: {
|
|
585
|
+
total_regressions: 0,
|
|
586
|
+
affected_requirements: 0,
|
|
587
|
+
clean: true,
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const baselineDate = (baseline.metadata && baseline.metadata.generated_at) || null;
|
|
593
|
+
const regressions = [];
|
|
594
|
+
let requirementsChecked = 0;
|
|
595
|
+
|
|
596
|
+
// Compare per-requirement property counts
|
|
597
|
+
for (const reqId of Object.keys(baseline.requirements)) {
|
|
598
|
+
// Only check requirements that exist in both baseline and new matrix
|
|
599
|
+
if (!newMatrix.requirements || !newMatrix.requirements[reqId]) continue;
|
|
600
|
+
|
|
601
|
+
requirementsChecked++;
|
|
602
|
+
|
|
603
|
+
const baselineProps = baseline.requirements[reqId].properties || [];
|
|
604
|
+
const currentProps = newMatrix.requirements[reqId].properties || [];
|
|
605
|
+
const baselineCount = baselineProps.length;
|
|
606
|
+
const currentCount = currentProps.length;
|
|
607
|
+
|
|
608
|
+
if (currentCount < baselineCount) {
|
|
609
|
+
const lostCount = baselineCount - currentCount;
|
|
610
|
+
const baselineModels = [...new Set(baselineProps.map(function(p) { return p.model_file; }))];
|
|
611
|
+
const currentModels = [...new Set(currentProps.map(function(p) { return p.model_file; }))];
|
|
612
|
+
|
|
613
|
+
const regression = {
|
|
614
|
+
requirement_id: reqId,
|
|
615
|
+
baseline_property_count: baselineCount,
|
|
616
|
+
current_property_count: currentCount,
|
|
617
|
+
lost_count: lostCount,
|
|
618
|
+
baseline_models: baselineModels,
|
|
619
|
+
current_models: currentModels,
|
|
620
|
+
detail: reqId + ' coverage decreased from ' + baselineCount + ' properties to ' + currentCount + ' (lost ' + lostCount + ') — possible model split dropped coverage',
|
|
621
|
+
};
|
|
622
|
+
regressions.push(regression);
|
|
623
|
+
process.stderr.write(TAG + ' warn: coverage regression — ' + reqId + ' had ' + baselineCount + ' properties, now has ' + currentCount + ' (lost ' + lostCount + ') — check if model split dropped coverage\n');
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
baseline_found: true,
|
|
629
|
+
baseline_date: baselineDate,
|
|
630
|
+
requirements_checked: requirementsChecked,
|
|
631
|
+
regressions: regressions,
|
|
632
|
+
summary: {
|
|
633
|
+
total_regressions: regressions.length,
|
|
634
|
+
affected_requirements: regressions.length,
|
|
635
|
+
clean: regressions.length === 0,
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ── Output ──────────────────────────────────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
function main() {
|
|
643
|
+
const matrix = buildMatrix();
|
|
644
|
+
|
|
645
|
+
// Coverage preservation — compare against baseline BEFORE overwriting file
|
|
646
|
+
const coveragePreservation = validateCoveragePreservation(matrix);
|
|
647
|
+
matrix.coverage_preservation = coveragePreservation;
|
|
648
|
+
|
|
649
|
+
const jsonStr = JSON.stringify(matrix, null, 2);
|
|
650
|
+
|
|
651
|
+
if (jsonMode) {
|
|
652
|
+
process.stdout.write(jsonStr + '\n');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Write to file (overwrites previous baseline)
|
|
657
|
+
fs.writeFileSync(OUTPUT_PATH, jsonStr + '\n', 'utf8');
|
|
658
|
+
|
|
659
|
+
if (!quietMode) {
|
|
660
|
+
const cs = matrix.coverage_summary;
|
|
661
|
+
const ds = matrix.metadata.data_sources;
|
|
662
|
+
const matchedChecks = new Set();
|
|
663
|
+
for (const prop of Object.values(matrix.properties)) {
|
|
664
|
+
if (prop.check_id) matchedChecks.add(prop.check_id);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const bv = matrix.bidirectional_validation.summary;
|
|
668
|
+
const cp = matrix.coverage_preservation;
|
|
669
|
+
const utc = ds.unit_test_coverage;
|
|
670
|
+
|
|
671
|
+
process.stdout.write(TAG + ' Generated .planning/formal/traceability-matrix.json\n');
|
|
672
|
+
process.stdout.write(TAG + ' Requirements: ' + cs.covered_count + ' covered / ' + cs.total_requirements + ' total (' + cs.coverage_percentage + '%)\n');
|
|
673
|
+
process.stdout.write(TAG + ' Properties: ' + ds.annotations.property_count + ' (' + ds.annotations.file_count + ' files)\n');
|
|
674
|
+
process.stdout.write(TAG + ' Orphan properties: ' + cs.orphan_properties.length + '\n');
|
|
675
|
+
process.stdout.write(TAG + ' Check results matched: ' + matchedChecks.size + '\n');
|
|
676
|
+
if (utc.available) {
|
|
677
|
+
process.stdout.write(TAG + ' Unit test coverage: ' + utc.requirements_matched + ' requirements matched\n');
|
|
678
|
+
}
|
|
679
|
+
process.stdout.write(TAG + ' Bidirectional validation: ' + bv.asymmetric_count + ' asymmetric, ' + bv.stale_count + ' stale (' + bv.total_checked + ' pairs checked)\n');
|
|
680
|
+
if (cp.baseline_found) {
|
|
681
|
+
process.stdout.write(TAG + ' Coverage preservation: ' + cp.summary.total_regressions + ' regressions (' + cp.requirements_checked + ' requirements checked vs baseline)\n');
|
|
682
|
+
} else {
|
|
683
|
+
process.stdout.write(TAG + ' Coverage preservation: no baseline (first run)\n');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// State-space summary
|
|
687
|
+
const ssKeys = Object.keys(matrix.state_space || {});
|
|
688
|
+
if (ssKeys.length > 0) {
|
|
689
|
+
const counts = { HIGH: 0, MODERATE: 0, LOW: 0, MINIMAL: 0 };
|
|
690
|
+
for (const k of ssKeys) {
|
|
691
|
+
const level = matrix.state_space[k].risk_level;
|
|
692
|
+
if (counts[level] !== undefined) counts[level]++;
|
|
693
|
+
}
|
|
694
|
+
process.stdout.write(TAG + ' State-space: ' + ssKeys.length + ' models analyzed (' + counts.HIGH + ' HIGH, ' + counts.MODERATE + ' MODERATE, ' + counts.LOW + ' LOW, ' + counts.MINIMAL + ' MINIMAL)\n');
|
|
695
|
+
} else {
|
|
696
|
+
process.stdout.write(TAG + ' State-space: not available (analyze-state-space.cjs missing or failed)\n');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
main();
|