@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,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/read-policy.cjs
|
|
4
|
+
// Reads and validates .planning/formal/policy.yaml using lightweight regex extraction.
|
|
5
|
+
// No external YAML parser required — policy.yaml uses flat key: value structure.
|
|
6
|
+
// Requirements: CALIB-01, CALIB-04
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read and validate .planning/formal/policy.yaml.
|
|
13
|
+
* Throws Error with descriptive message on missing file or missing required fields.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} policyPath Absolute path to policy.yaml
|
|
16
|
+
* @returns {{ cold_start: { min_ci_runs: number, min_quorum_rounds: number, min_days: number },
|
|
17
|
+
* steady_state: { mode: string },
|
|
18
|
+
* conservative_priors: { tp_rate: number, unavail: number } }}
|
|
19
|
+
*/
|
|
20
|
+
function readPolicy(policyPath) {
|
|
21
|
+
if (!fs.existsSync(policyPath)) {
|
|
22
|
+
throw new Error('[read-policy] Policy file not found: ' + policyPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const yaml = fs.readFileSync(policyPath, 'utf8');
|
|
26
|
+
|
|
27
|
+
// Extract flat key: value using regex (ignores YAML comments)
|
|
28
|
+
const extract = (key, asFloat) => {
|
|
29
|
+
const match = yaml.match(new RegExp('^\\s*' + key + ':\\s*([\\d.]+)', 'm'));
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw new Error('[read-policy] Policy missing required key: ' + key);
|
|
32
|
+
}
|
|
33
|
+
return asFloat ? parseFloat(match[1]) : parseInt(match[1], 10);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const extractStr = (key) => {
|
|
37
|
+
const match = yaml.match(new RegExp('^\\s*' + key + ':\\s*["\']?([\\w]+)["\']?', 'm'));
|
|
38
|
+
if (!match) {
|
|
39
|
+
throw new Error('[read-policy] Policy missing required key: ' + key);
|
|
40
|
+
}
|
|
41
|
+
return match[1];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
cold_start: {
|
|
46
|
+
min_ci_runs: extract('min_ci_runs', false),
|
|
47
|
+
min_quorum_rounds: extract('min_quorum_rounds', false),
|
|
48
|
+
min_days: extract('min_days', true),
|
|
49
|
+
},
|
|
50
|
+
steady_state: {
|
|
51
|
+
mode: extractStr('mode'),
|
|
52
|
+
},
|
|
53
|
+
conservative_priors: {
|
|
54
|
+
tp_rate: extract('tp_rate', true),
|
|
55
|
+
unavail: extract('unavail', true),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { readPolicy };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// bin/requirement-map.cjs
|
|
3
|
+
// Centralized mapping from check_id (used by run-formal-verify.cjs steps) to requirement IDs.
|
|
4
|
+
// Single source of truth for SCHEMA-03: runners import this to populate requirement_ids in NDJSON output.
|
|
5
|
+
//
|
|
6
|
+
// Maintenance: when adding a new formal check or requirement, update this map.
|
|
7
|
+
// The check_id values match those in run-formal-verify.cjs STEPS array and individual runner CHECK_ID_MAP objects.
|
|
8
|
+
|
|
9
|
+
const CHECK_ID_TO_REQUIREMENTS = {
|
|
10
|
+
// ── TLA+ checks ──────────────────────────────────────
|
|
11
|
+
'tla:quorum-safety': ['QUORUM-01', 'QUORUM-02', 'QUORUM-03', 'SAFE-01', 'SAFE-02', 'SAFE-03', 'LOOP-01'],
|
|
12
|
+
'tla:quorum-liveness': ['QUORUM-04', 'RECV-01'],
|
|
13
|
+
'tla:mcp-environment': ['MCPENV-01', 'MCPENV-02', 'MCPENV-03'],
|
|
14
|
+
'tla:breaker': ['DETECT-01', 'DETECT-02', 'DETECT-03'],
|
|
15
|
+
'tla:oscillation': ['DETECT-04', 'DETECT-05', 'DETECT-06'],
|
|
16
|
+
'tla:convergence': ['ORES-01', 'ORES-02', 'ORES-03', 'ORES-04', 'ORES-05'],
|
|
17
|
+
'tla:deliberation': ['PLAN-01', 'PLAN-02', 'SAFE-03', 'IMPR-01', 'LOOP-02'],
|
|
18
|
+
'tla:prefilter': ['PLAN-03', 'PLAN-04', 'PLAN-05', 'PLAN-06', 'LOOP-03'],
|
|
19
|
+
'tla:account-manager': ['CRED-01', 'CRED-02', 'CRED-03', 'CRED-04', 'CRED-05', 'CRED-06'],
|
|
20
|
+
'tla:stop-hook': ['STOP-01', 'STOP-02', 'STOP-03', 'STOP-04', 'STOP-05', 'STOP-06', 'STOP-07', 'SPEC-01'],
|
|
21
|
+
'tla:recruiting-safety': ['SLOT-02', 'SLOT-03', 'SLOT-04'],
|
|
22
|
+
'tla:recruiting-liveness': ['SLOT-05'],
|
|
23
|
+
'tla:tui-navigation': [],
|
|
24
|
+
|
|
25
|
+
// ── Alloy checks ─────────────────────────────────────
|
|
26
|
+
// check_id values match those emitted by individual runner scripts:
|
|
27
|
+
'alloy:quorum-votes': ['QUORUM-02', 'SAFE-01', 'SAFE-04'],
|
|
28
|
+
'alloy:quorum-composition': ['SPEC-03', 'COMP-01'],
|
|
29
|
+
'alloy:scoreboard': ['SCBD-01', 'SCBD-02', 'SCBD-03', 'SCBD-04'],
|
|
30
|
+
'alloy:availability': ['CALIB-01', 'CALIB-02', 'CALIB-03'],
|
|
31
|
+
'alloy:transcript': ['STOP-08', 'STOP-09', 'STOP-10', 'STOP-11'],
|
|
32
|
+
'alloy:install-scope': ['INST-01', 'INST-02', 'INST-03', 'INST-04', 'INST-05'],
|
|
33
|
+
'alloy:taxonomy-safety': ['SCBD-05', 'SCBD-06', 'SCBD-07'],
|
|
34
|
+
'alloy:account-pool': ['CRED-07', 'CRED-08', 'CRED-09', 'CRED-10', 'CRED-11'],
|
|
35
|
+
|
|
36
|
+
// ── PRISM checks ─────────────────────────────────────
|
|
37
|
+
'prism:quorum': ['PRM-01', 'QUORUM-04', 'LOOP-01'],
|
|
38
|
+
'prism:oauth-rotation': ['PRM-AM-01', 'CRED-12'],
|
|
39
|
+
'prism:mcp-availability': ['MCPENV-04', 'FAIL-01'],
|
|
40
|
+
|
|
41
|
+
// ── UPPAAL checks ────────────────────────────────────
|
|
42
|
+
'uppaal:quorum-races': ['UPPAAL-01', 'UPPAAL-02', 'UPPAAL-03'],
|
|
43
|
+
|
|
44
|
+
// ── CI enforcement checks ────────────────────────────
|
|
45
|
+
'ci:trace-redaction': ['REDACT-01'],
|
|
46
|
+
'ci:trace-schema-drift': ['DRIFT-01'],
|
|
47
|
+
'ci:liveness-fairness-lint': ['LIVE-01', 'LIVE-02'],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get requirement IDs for a given check_id.
|
|
52
|
+
* Returns empty array for unknown check_ids (fail-open).
|
|
53
|
+
*
|
|
54
|
+
* @param {string} checkId - The check_id (e.g., 'tla:quorum-safety')
|
|
55
|
+
* @returns {string[]} Array of requirement IDs
|
|
56
|
+
*/
|
|
57
|
+
function getRequirementIds(checkId) {
|
|
58
|
+
// Return a copy to prevent callers from mutating the shared source-of-truth array.
|
|
59
|
+
const found = CHECK_ID_TO_REQUIREMENTS[checkId];
|
|
60
|
+
return found ? found.slice() : [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { CHECK_ID_TO_REQUIREMENTS, getRequirementIds };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure data functions for requirements management.
|
|
5
|
+
* No blessed dependency — all functions are testable in isolation.
|
|
6
|
+
* Consumers: bin/qgsd.cjs (blessed TUI)
|
|
7
|
+
*
|
|
8
|
+
* Data sources (project-relative paths via process.cwd()):
|
|
9
|
+
* .planning/formal/requirements.json — 210 requirements in frozen envelope
|
|
10
|
+
* .planning/formal/model-registry.json — formal models with requirement links
|
|
11
|
+
* .planning/formal/check-results.ndjson — check results (NDJSON)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { getRequirementIds } = require('./requirement-map.cjs');
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Data loaders
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function readRequirementsJson(basePath) {
|
|
23
|
+
const p = path.join(basePath || process.cwd(), '.planning', 'formal', 'requirements.json');
|
|
24
|
+
if (!fs.existsSync(p)) return { envelope: null, requirements: [] };
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
27
|
+
const reqs = raw.requirements || [];
|
|
28
|
+
const envelope = {
|
|
29
|
+
aggregated_at: raw.aggregated_at || null,
|
|
30
|
+
content_hash: raw.content_hash || null,
|
|
31
|
+
frozen_at: raw.frozen_at || null,
|
|
32
|
+
};
|
|
33
|
+
return { envelope, requirements: reqs };
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return { envelope: null, requirements: [] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readModelRegistry(basePath) {
|
|
40
|
+
const p = path.join(basePath || process.cwd(), '.planning', 'formal', 'model-registry.json');
|
|
41
|
+
if (!fs.existsSync(p)) return { version: null, last_sync: null, models: {} };
|
|
42
|
+
try {
|
|
43
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
44
|
+
return {
|
|
45
|
+
version: raw.version || null,
|
|
46
|
+
last_sync: raw.last_sync || null,
|
|
47
|
+
models: raw.models || {},
|
|
48
|
+
};
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return { version: null, last_sync: null, models: {} };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readCheckResults(basePath) {
|
|
55
|
+
const p = path.join(basePath || process.cwd(), '.planning', 'formal', 'check-results.ndjson');
|
|
56
|
+
if (!fs.existsSync(p)) return [];
|
|
57
|
+
try {
|
|
58
|
+
return fs.readFileSync(p, 'utf8')
|
|
59
|
+
.split('\n')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map(line => { try { return JSON.parse(line); } catch (_) { return null; } })
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Computation
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function computeCoverage(requirements, registry, checkResults) {
|
|
73
|
+
const total = requirements.length;
|
|
74
|
+
|
|
75
|
+
// By status
|
|
76
|
+
const byStatus = {};
|
|
77
|
+
for (const r of requirements) {
|
|
78
|
+
const s = r.status || 'Unknown';
|
|
79
|
+
byStatus[s] = (byStatus[s] || 0) + 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// By category
|
|
83
|
+
const byCategory = {};
|
|
84
|
+
for (const r of requirements) {
|
|
85
|
+
const c = r.category || 'Uncategorized';
|
|
86
|
+
if (!byCategory[c]) byCategory[c] = { total: 0, complete: 0 };
|
|
87
|
+
byCategory[c].total++;
|
|
88
|
+
if (r.status === 'Complete') byCategory[c].complete++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Formal model coverage: which requirements have at least one formal model?
|
|
92
|
+
const reqsWithModels = new Set();
|
|
93
|
+
const models = (registry && registry.models) || {};
|
|
94
|
+
for (const entry of Object.values(models)) {
|
|
95
|
+
for (const reqId of (entry.requirements || [])) {
|
|
96
|
+
reqsWithModels.add(reqId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Also include requirements with direct formal_models field (SCHEMA-04)
|
|
100
|
+
for (const r of requirements) {
|
|
101
|
+
if (Array.isArray(r.formal_models) && r.formal_models.length > 0) {
|
|
102
|
+
reqsWithModels.add(r.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const withFormalModels = requirements.filter(r => reqsWithModels.has(r.id)).length;
|
|
106
|
+
const totalModels = Object.keys(models).length;
|
|
107
|
+
|
|
108
|
+
// Check results summary
|
|
109
|
+
const checksByResult = {};
|
|
110
|
+
for (const cr of (checkResults || [])) {
|
|
111
|
+
const res = cr.result || 'unknown';
|
|
112
|
+
checksByResult[res] = (checksByResult[res] || 0) + 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Requirements with at least one check result (via requirement_ids or requirement-map)
|
|
116
|
+
const reqsWithChecks = new Set();
|
|
117
|
+
for (const cr of (checkResults || [])) {
|
|
118
|
+
// Direct requirement_ids on the check result
|
|
119
|
+
if (Array.isArray(cr.requirement_ids)) {
|
|
120
|
+
for (const rid of cr.requirement_ids) reqsWithChecks.add(rid);
|
|
121
|
+
}
|
|
122
|
+
// Reverse lookup via requirement-map
|
|
123
|
+
if (cr.check_id) {
|
|
124
|
+
for (const rid of getRequirementIds(cr.check_id)) reqsWithChecks.add(rid);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const withCheckResults = requirements.filter(r => reqsWithChecks.has(r.id)).length;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
total,
|
|
131
|
+
byStatus,
|
|
132
|
+
byCategory,
|
|
133
|
+
withFormalModels,
|
|
134
|
+
withCheckResults,
|
|
135
|
+
totalModels,
|
|
136
|
+
checksByResult,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildTraceability(reqId, requirements, registry, checkResults) {
|
|
141
|
+
const requirement = requirements.find(r => r.id === reqId);
|
|
142
|
+
if (!requirement) return null;
|
|
143
|
+
|
|
144
|
+
// Forward: find formal models that list this requirement
|
|
145
|
+
const formalModels = [];
|
|
146
|
+
const models = (registry && registry.models) || {};
|
|
147
|
+
for (const [modelPath, entry] of Object.entries(models)) {
|
|
148
|
+
if ((entry.requirements || []).includes(reqId)) {
|
|
149
|
+
formalModels.push({
|
|
150
|
+
path: modelPath,
|
|
151
|
+
description: entry.description || '',
|
|
152
|
+
version: entry.version || null,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Also include models listed in requirement's own formal_models field (SCHEMA-04)
|
|
157
|
+
if (Array.isArray(requirement.formal_models)) {
|
|
158
|
+
for (const modelPath of requirement.formal_models) {
|
|
159
|
+
// Deduplicate: skip if already found via registry
|
|
160
|
+
if (!formalModels.some(fm => fm.path === modelPath)) {
|
|
161
|
+
// Try to get description from registry if available
|
|
162
|
+
const registryEntry = models[modelPath];
|
|
163
|
+
formalModels.push({
|
|
164
|
+
path: modelPath,
|
|
165
|
+
description: (registryEntry && registryEntry.description) || '',
|
|
166
|
+
version: (registryEntry && registryEntry.version) || null,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Reverse: find check results linked to this requirement
|
|
173
|
+
const matchingChecks = [];
|
|
174
|
+
const unmappedCheckIds = [];
|
|
175
|
+
for (const cr of (checkResults || [])) {
|
|
176
|
+
// Direct match via requirement_ids field
|
|
177
|
+
const directIds = Array.isArray(cr.requirement_ids) ? cr.requirement_ids : [];
|
|
178
|
+
// Reverse lookup via requirement-map
|
|
179
|
+
const mapIds = cr.check_id ? getRequirementIds(cr.check_id) : [];
|
|
180
|
+
const allIds = new Set([...directIds, ...mapIds]);
|
|
181
|
+
|
|
182
|
+
if (allIds.has(reqId)) {
|
|
183
|
+
matchingChecks.push(cr);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Find check_ids from requirement-map that link to this req but have no results
|
|
188
|
+
const { CHECK_ID_TO_REQUIREMENTS } = require('./requirement-map.cjs');
|
|
189
|
+
for (const [checkId, reqIds] of Object.entries(CHECK_ID_TO_REQUIREMENTS)) {
|
|
190
|
+
if (reqIds.includes(reqId)) {
|
|
191
|
+
const hasResult = (checkResults || []).some(cr => cr.check_id === checkId);
|
|
192
|
+
if (!hasResult) unmappedCheckIds.push(checkId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
requirement,
|
|
198
|
+
formalModels,
|
|
199
|
+
checkResults: matchingChecks,
|
|
200
|
+
unmappedCheckIds,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function filterRequirements(requirements, filters) {
|
|
205
|
+
filters = filters || {};
|
|
206
|
+
let result = requirements;
|
|
207
|
+
|
|
208
|
+
if (filters.category) {
|
|
209
|
+
result = result.filter(r => (r.category || 'Uncategorized') === filters.category);
|
|
210
|
+
}
|
|
211
|
+
if (filters.status) {
|
|
212
|
+
result = result.filter(r => (r.status || 'Unknown') === filters.status);
|
|
213
|
+
}
|
|
214
|
+
if (filters.search) {
|
|
215
|
+
const s = filters.search.toLowerCase();
|
|
216
|
+
result = result.filter(r =>
|
|
217
|
+
(r.id || '').toLowerCase().includes(s) ||
|
|
218
|
+
(r.text || '').toLowerCase().includes(s)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getUniqueCategories(requirements) {
|
|
226
|
+
const cats = new Set();
|
|
227
|
+
for (const r of requirements) {
|
|
228
|
+
cats.add(r.category || 'Uncategorized');
|
|
229
|
+
}
|
|
230
|
+
return [...cats].sort();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Exports
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
readRequirementsJson,
|
|
239
|
+
readModelRegistry,
|
|
240
|
+
readCheckResults,
|
|
241
|
+
computeCoverage,
|
|
242
|
+
buildTraceability,
|
|
243
|
+
filterRequirements,
|
|
244
|
+
getUniqueCategories,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
module.exports._pure = module.exports;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* resolve-cli.cjs — CLI path resolution utility
|
|
5
|
+
*
|
|
6
|
+
* Resolves a bare CLI name to its full executable path using a priority-ordered search:
|
|
7
|
+
* 1. which <name> — system PATH lookup
|
|
8
|
+
* 2. Homebrew prefixes — /opt/homebrew/bin, /usr/local/bin
|
|
9
|
+
* 3. npm global bin — derived from `npm root -g`
|
|
10
|
+
* 4. Common system paths — /usr/bin, /usr/local/bin
|
|
11
|
+
* 5. Bare fallback — return name unchanged (let OS resolve at spawn time)
|
|
12
|
+
*
|
|
13
|
+
* Never throws. Always returns a non-empty string.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a CLI name to its full executable path.
|
|
22
|
+
* @param {string} name - bare name like "codex", "gemini", "opencode"
|
|
23
|
+
* @returns {string} full path like "/opt/homebrew/bin/codex" or bare name if not found
|
|
24
|
+
*/
|
|
25
|
+
function resolveCli(name) {
|
|
26
|
+
if (!name || typeof name !== 'string') return name || '';
|
|
27
|
+
|
|
28
|
+
// 1. which <name>
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnSync('which', [name], { encoding: 'utf8', timeout: 3000 });
|
|
31
|
+
if (result.status === 0 && result.stdout) {
|
|
32
|
+
const found = result.stdout.trim();
|
|
33
|
+
if (found && found.length > 0) {
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (_) {
|
|
38
|
+
// which failed — continue to next strategy
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Known Homebrew prefixes
|
|
42
|
+
const homebrewPrefixes = ['/opt/homebrew/bin', '/usr/local/bin'];
|
|
43
|
+
for (const prefix of homebrewPrefixes) {
|
|
44
|
+
const candidate = path.join(prefix, name);
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(candidate)) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {
|
|
50
|
+
// fs error — continue
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. npm global bin: npm root -g -> ../bin/<name>
|
|
55
|
+
try {
|
|
56
|
+
const npmResult = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000 });
|
|
57
|
+
if (npmResult.status === 0 && npmResult.stdout) {
|
|
58
|
+
const npmRoot = npmResult.stdout.trim();
|
|
59
|
+
if (npmRoot) {
|
|
60
|
+
const candidate = path.join(npmRoot, '..', 'bin', name);
|
|
61
|
+
if (fs.existsSync(candidate)) {
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (_) {
|
|
67
|
+
// npm failed — continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Common system paths (deduplicated — /usr/local/bin was checked in step 2)
|
|
71
|
+
const systemPaths = ['/usr/bin'];
|
|
72
|
+
for (const dir of systemPaths) {
|
|
73
|
+
const candidate = path.join(dir, name);
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(candidate)) {
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
} catch (_) {
|
|
79
|
+
// continue
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. Fallback: return bare name (let OS resolve at spawn time)
|
|
84
|
+
return name;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { resolveCli };
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Standalone CLI interface
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
if (require.main === module) {
|
|
94
|
+
const name = process.argv[2];
|
|
95
|
+
if (!name) {
|
|
96
|
+
console.error('Usage: node bin/resolve-cli.cjs <name>');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const resolved = resolveCli(name);
|
|
100
|
+
console.log(resolved);
|
|
101
|
+
}
|