@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
package/bin/polyrepo.cjs
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const TAG = '[qgsd-polyrepo]';
|
|
9
|
+
const POLYREPOS_DIR = path.join(os.homedir(), '.claude', 'polyrepos');
|
|
10
|
+
const MARKER_FILE = 'polyrepo.json';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensure ~/.claude/polyrepos/ directory exists
|
|
14
|
+
*/
|
|
15
|
+
function ensurePolyreposDir() {
|
|
16
|
+
try {
|
|
17
|
+
fs.mkdirSync(POLYREPOS_DIR, { recursive: true });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error(`${TAG} Failed to create polyrepos directory:`, err.message);
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load a group config from ~/.claude/polyrepos/<name>.json
|
|
26
|
+
* Returns parsed object or null if not found.
|
|
27
|
+
* Fail-open: if malformed JSON, log warning and return null.
|
|
28
|
+
*/
|
|
29
|
+
function loadGroup(name) {
|
|
30
|
+
const filePath = path.join(POLYREPOS_DIR, `${name}.json`);
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(filePath)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
36
|
+
return JSON.parse(content);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err instanceof SyntaxError) {
|
|
39
|
+
console.error(`${TAG} Warning: malformed JSON in ${filePath}, returning null`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Save a group config to ~/.claude/polyrepos/<group.name>.json
|
|
48
|
+
*/
|
|
49
|
+
function saveGroup(group) {
|
|
50
|
+
ensurePolyreposDir();
|
|
51
|
+
const filePath = path.join(POLYREPOS_DIR, `${group.name}.json`);
|
|
52
|
+
try {
|
|
53
|
+
fs.writeFileSync(filePath, JSON.stringify(group, null, 2), 'utf8');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`${TAG} Failed to save group:`, err.message);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write per-repo marker at <repoPath>/.planning/polyrepo.json
|
|
62
|
+
* Optional docs: { user?, developer?, examples? } — relative paths within the repo
|
|
63
|
+
*/
|
|
64
|
+
function writeMarker(repoPath, name, role, docs) {
|
|
65
|
+
try {
|
|
66
|
+
const markerDir = path.join(repoPath, '.planning');
|
|
67
|
+
fs.mkdirSync(markerDir, { recursive: true });
|
|
68
|
+
const markerPath = path.join(markerDir, MARKER_FILE);
|
|
69
|
+
const marker = { name, role };
|
|
70
|
+
if (docs && Object.keys(docs).length > 0) {
|
|
71
|
+
marker.docs = docs;
|
|
72
|
+
}
|
|
73
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2), 'utf8');
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`${TAG} Failed to write marker:`, err.message);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read per-repo marker from <repoPath>/.planning/polyrepo.json
|
|
82
|
+
* Returns parsed object or null if not found/malformed.
|
|
83
|
+
*/
|
|
84
|
+
function readMarker(repoPath) {
|
|
85
|
+
try {
|
|
86
|
+
const markerPath = path.join(repoPath, '.planning', MARKER_FILE);
|
|
87
|
+
if (!fs.existsSync(markerPath)) return null;
|
|
88
|
+
return JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err instanceof SyntaxError) {
|
|
91
|
+
console.error(`${TAG} Warning: malformed marker at ${repoPath}`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set docs paths on an existing per-repo marker.
|
|
100
|
+
* docs: { user?, developer?, examples? } — relative paths within the repo.
|
|
101
|
+
* Merges with existing docs (pass null value to remove a key).
|
|
102
|
+
*/
|
|
103
|
+
function setDocs(repoPath, docs) {
|
|
104
|
+
const marker = readMarker(repoPath);
|
|
105
|
+
if (!marker) {
|
|
106
|
+
return { ok: false, error: `No polyrepo marker found at ${repoPath}/.planning/polyrepo.json` };
|
|
107
|
+
}
|
|
108
|
+
const merged = { ...(marker.docs || {}) };
|
|
109
|
+
for (const [key, val] of Object.entries(docs)) {
|
|
110
|
+
if (val === null) {
|
|
111
|
+
delete merged[key];
|
|
112
|
+
} else {
|
|
113
|
+
merged[key] = val;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
marker.docs = Object.keys(merged).length > 0 ? merged : undefined;
|
|
117
|
+
try {
|
|
118
|
+
const markerPath = path.join(repoPath, '.planning', MARKER_FILE);
|
|
119
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2), 'utf8');
|
|
120
|
+
return { ok: true, docs: merged };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return { ok: false, error: err.message };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Remove per-repo marker at <repoPath>/.planning/polyrepo.json
|
|
128
|
+
* Fail-open: no error if file doesn't exist
|
|
129
|
+
*/
|
|
130
|
+
function removeMarker(repoPath) {
|
|
131
|
+
try {
|
|
132
|
+
const markerPath = path.join(repoPath, '.planning', MARKER_FILE);
|
|
133
|
+
if (fs.existsSync(markerPath)) {
|
|
134
|
+
fs.unlinkSync(markerPath);
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`${TAG} Warning: failed to remove marker:`, err.message);
|
|
138
|
+
// Fail-open: don't throw
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate group name: alphanumeric + hyphens, 1-50 chars, lowercase, start with alphanumeric
|
|
144
|
+
*/
|
|
145
|
+
function validateGroupName(name) {
|
|
146
|
+
const nameRegex = /^[a-z0-9][a-z0-9-]*$/;
|
|
147
|
+
if (!name || typeof name !== 'string') {
|
|
148
|
+
return { ok: false, error: 'Name must be a non-empty string' };
|
|
149
|
+
}
|
|
150
|
+
if (name.length > 50) {
|
|
151
|
+
return { ok: false, error: 'Name must be 1-50 characters' };
|
|
152
|
+
}
|
|
153
|
+
if (!nameRegex.test(name)) {
|
|
154
|
+
return { ok: false, error: 'Name must start with alphanumeric and contain only lowercase alphanumeric and hyphens' };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a new polyrepo group with optional initial repos
|
|
161
|
+
* repos: array of { role, path, planning? }
|
|
162
|
+
* Empty repos array is valid (used by interactive create flow).
|
|
163
|
+
*/
|
|
164
|
+
function createGroup(name, repos = []) {
|
|
165
|
+
// Validate name
|
|
166
|
+
const nameVal = validateGroupName(name);
|
|
167
|
+
if (!nameVal.ok) {
|
|
168
|
+
return nameVal;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if group already exists
|
|
172
|
+
if (loadGroup(name) !== null) {
|
|
173
|
+
return { ok: false, error: `Group '${name}' already exists` };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Validate repos
|
|
177
|
+
if (!Array.isArray(repos)) {
|
|
178
|
+
return { ok: false, error: 'Repos must be an array' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const seenPaths = new Set();
|
|
182
|
+
for (const repo of repos) {
|
|
183
|
+
if (typeof repo.role !== 'string' || repo.role.length === 0) {
|
|
184
|
+
return { ok: false, error: 'Each repo must have a non-empty role string' };
|
|
185
|
+
}
|
|
186
|
+
if (typeof repo.path !== 'string' || !path.isAbsolute(repo.path)) {
|
|
187
|
+
return { ok: false, error: `Repo path must be absolute: ${repo.path}` };
|
|
188
|
+
}
|
|
189
|
+
if (!fs.existsSync(repo.path)) {
|
|
190
|
+
return { ok: false, error: `Repo path does not exist: ${repo.path}` };
|
|
191
|
+
}
|
|
192
|
+
if (!fs.statSync(repo.path).isDirectory()) {
|
|
193
|
+
return { ok: false, error: `Repo path is not a directory: ${repo.path}` };
|
|
194
|
+
}
|
|
195
|
+
if (seenPaths.has(repo.path)) {
|
|
196
|
+
return { ok: false, error: `Duplicate path in group: ${repo.path}` };
|
|
197
|
+
}
|
|
198
|
+
seenPaths.add(repo.path);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create group
|
|
202
|
+
const group = {
|
|
203
|
+
name,
|
|
204
|
+
repos: repos.map(r => ({
|
|
205
|
+
role: r.role,
|
|
206
|
+
path: r.path,
|
|
207
|
+
planning: r.planning !== false ? true : false
|
|
208
|
+
}))
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
saveGroup(group);
|
|
213
|
+
// Write markers for repos with planning: true
|
|
214
|
+
for (const repo of group.repos) {
|
|
215
|
+
if (repo.planning) {
|
|
216
|
+
writeMarker(repo.path, name, repo.role);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { ok: true, group };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return { ok: false, error: `Failed to create group: ${err.message}` };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Add a repo to an existing group
|
|
227
|
+
*/
|
|
228
|
+
function addRepo(groupName, repoPath, role, planning = true) {
|
|
229
|
+
// Load group
|
|
230
|
+
const group = loadGroup(groupName);
|
|
231
|
+
if (!group) {
|
|
232
|
+
return { ok: false, error: `Group '${groupName}' does not exist` };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Validate repo path
|
|
236
|
+
if (typeof repoPath !== 'string' || !path.isAbsolute(repoPath)) {
|
|
237
|
+
return { ok: false, error: `Repo path must be absolute: ${repoPath}` };
|
|
238
|
+
}
|
|
239
|
+
if (!fs.existsSync(repoPath)) {
|
|
240
|
+
return { ok: false, error: `Repo path does not exist: ${repoPath}` };
|
|
241
|
+
}
|
|
242
|
+
if (!fs.statSync(repoPath).isDirectory()) {
|
|
243
|
+
return { ok: false, error: `Repo path is not a directory: ${repoPath}` };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check for duplicates
|
|
247
|
+
if (group.repos.some(r => r.path === repoPath)) {
|
|
248
|
+
return { ok: false, error: `Repo path already in group: ${repoPath}` };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Validate role
|
|
252
|
+
if (typeof role !== 'string' || role.length === 0) {
|
|
253
|
+
return { ok: false, error: 'Role must be a non-empty string' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Add repo
|
|
257
|
+
try {
|
|
258
|
+
group.repos.push({
|
|
259
|
+
role,
|
|
260
|
+
path: repoPath,
|
|
261
|
+
planning: planning !== false ? true : false
|
|
262
|
+
});
|
|
263
|
+
saveGroup(group);
|
|
264
|
+
if (planning !== false) {
|
|
265
|
+
writeMarker(repoPath, groupName, role);
|
|
266
|
+
}
|
|
267
|
+
return { ok: true };
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return { ok: false, error: `Failed to add repo: ${err.message}` };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Remove a repo from a group
|
|
275
|
+
* If group becomes empty, delete the group config file entirely.
|
|
276
|
+
*/
|
|
277
|
+
function removeRepo(groupName, repoPath) {
|
|
278
|
+
// Load group
|
|
279
|
+
const group = loadGroup(groupName);
|
|
280
|
+
if (!group) {
|
|
281
|
+
return { ok: false, error: `Group '${groupName}' does not exist` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Find and remove repo
|
|
285
|
+
const initialLength = group.repos.length;
|
|
286
|
+
group.repos = group.repos.filter(r => r.path !== repoPath);
|
|
287
|
+
|
|
288
|
+
if (group.repos.length === initialLength) {
|
|
289
|
+
return { ok: false, error: `Repo not found in group: ${repoPath}` };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
removeMarker(repoPath);
|
|
294
|
+
|
|
295
|
+
// If group is now empty, delete the group config file
|
|
296
|
+
if (group.repos.length === 0) {
|
|
297
|
+
const filePath = path.join(POLYREPOS_DIR, `${groupName}.json`);
|
|
298
|
+
fs.unlinkSync(filePath);
|
|
299
|
+
return { ok: true, deleted_group: true };
|
|
300
|
+
} else {
|
|
301
|
+
saveGroup(group);
|
|
302
|
+
return { ok: true };
|
|
303
|
+
}
|
|
304
|
+
} catch (err) {
|
|
305
|
+
return { ok: false, error: `Failed to remove repo: ${err.message}` };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* List all polyrepo groups
|
|
311
|
+
*/
|
|
312
|
+
function listGroups() {
|
|
313
|
+
try {
|
|
314
|
+
ensurePolyreposDir();
|
|
315
|
+
if (!fs.existsSync(POLYREPOS_DIR)) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
const files = fs.readdirSync(POLYREPOS_DIR);
|
|
319
|
+
const groups = [];
|
|
320
|
+
for (const file of files) {
|
|
321
|
+
if (!file.endsWith('.json')) continue;
|
|
322
|
+
const group = loadGroup(file.replace(/\.json$/, ''));
|
|
323
|
+
if (group) {
|
|
324
|
+
groups.push(group);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return groups;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.error(`${TAG} Failed to list groups:`, err.message);
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Load a single group by name
|
|
336
|
+
*/
|
|
337
|
+
function listGroup(name) {
|
|
338
|
+
return loadGroup(name);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* CLI Subcommand Handler
|
|
343
|
+
*/
|
|
344
|
+
function handleCLI() {
|
|
345
|
+
const args = process.argv.slice(2);
|
|
346
|
+
const cmd = args[0];
|
|
347
|
+
|
|
348
|
+
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
349
|
+
process.stdout.write(`${TAG} Polyrepo Config Management
|
|
350
|
+
|
|
351
|
+
Usage:
|
|
352
|
+
node polyrepo.cjs create <name>
|
|
353
|
+
node polyrepo.cjs add <group> <path> [role] [--no-planning]
|
|
354
|
+
node polyrepo.cjs remove <group> <path>
|
|
355
|
+
node polyrepo.cjs list [group]
|
|
356
|
+
node polyrepo.cjs info
|
|
357
|
+
node polyrepo.cjs docs [show|set|remove]
|
|
358
|
+
node polyrepo.cjs --help
|
|
359
|
+
|
|
360
|
+
Commands:
|
|
361
|
+
create <name> Create an empty polyrepo group
|
|
362
|
+
add <group> <path> [role] [--no-planning]
|
|
363
|
+
Add a repo to a group (role defaults to basename)
|
|
364
|
+
remove <group> <path> Remove a repo from a group
|
|
365
|
+
list [group] List all groups or repos in a specific group
|
|
366
|
+
info Show this repo's polyrepo group membership
|
|
367
|
+
docs show Show doc paths for current repo
|
|
368
|
+
docs set <key> <path> Set a doc path (user, developer, examples, or custom)
|
|
369
|
+
docs remove <key> Remove a doc path
|
|
370
|
+
--help Show this help message
|
|
371
|
+
`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (cmd === 'create') {
|
|
376
|
+
const name = args[1];
|
|
377
|
+
if (!name) {
|
|
378
|
+
console.error(`${TAG} create: missing name argument`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
const result = createGroup(name, []);
|
|
382
|
+
if (result.ok) {
|
|
383
|
+
process.stdout.write(`${TAG} Created group '${name}' at ${POLYREPOS_DIR}/${name}.json\n`);
|
|
384
|
+
} else {
|
|
385
|
+
console.error(`${TAG} Error: ${result.error}`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
} else if (cmd === 'add') {
|
|
389
|
+
const group = args[1];
|
|
390
|
+
const repoPath = args[2];
|
|
391
|
+
let role = args[3];
|
|
392
|
+
const noPlanning = args.includes('--no-planning');
|
|
393
|
+
|
|
394
|
+
if (!group || !repoPath) {
|
|
395
|
+
console.error(`${TAG} add: missing group or path argument`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Default role to basename if not provided
|
|
400
|
+
if (!role || role.startsWith('--')) {
|
|
401
|
+
role = path.basename(repoPath);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const result = addRepo(group, repoPath, role, !noPlanning);
|
|
405
|
+
if (result.ok) {
|
|
406
|
+
process.stdout.write(`${TAG} Added '${role}' (${repoPath}) to group '${group}'\n`);
|
|
407
|
+
} else {
|
|
408
|
+
console.error(`${TAG} Error: ${result.error}`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
} else if (cmd === 'remove') {
|
|
412
|
+
const group = args[1];
|
|
413
|
+
const repoPath = args[2];
|
|
414
|
+
if (!group || !repoPath) {
|
|
415
|
+
console.error(`${TAG} remove: missing group or path argument`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
const result = removeRepo(group, repoPath);
|
|
419
|
+
if (result.ok) {
|
|
420
|
+
if (result.deleted_group) {
|
|
421
|
+
process.stdout.write(`${TAG} Removed repo from group '${group}'. Group was empty, so config deleted.\n`);
|
|
422
|
+
} else {
|
|
423
|
+
process.stdout.write(`${TAG} Removed repo from group '${group}'\n`);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
console.error(`${TAG} Error: ${result.error}`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
} else if (cmd === 'list') {
|
|
430
|
+
const groupName = args[1];
|
|
431
|
+
if (groupName) {
|
|
432
|
+
const group = listGroup(groupName);
|
|
433
|
+
if (!group) {
|
|
434
|
+
console.error(`${TAG} Group '${groupName}' not found`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
process.stdout.write(`\nPolyrepo Group: ${group.name}\n`);
|
|
438
|
+
for (const repo of group.repos) {
|
|
439
|
+
const planning = repo.planning ? '[planning]' : '[no planning]';
|
|
440
|
+
process.stdout.write(` ${repo.role.padEnd(15)} ${repo.path.padEnd(30)} ${planning}\n`);
|
|
441
|
+
}
|
|
442
|
+
process.stdout.write('\n');
|
|
443
|
+
} else {
|
|
444
|
+
const groups = listGroups();
|
|
445
|
+
if (groups.length === 0) {
|
|
446
|
+
process.stdout.write(`${TAG} No polyrepo groups defined\n`);
|
|
447
|
+
} else {
|
|
448
|
+
process.stdout.write('\nPolyrepo Groups:\n');
|
|
449
|
+
for (const group of groups) {
|
|
450
|
+
process.stdout.write(` ${group.name} (${group.repos.length} repos)\n`);
|
|
451
|
+
for (const repo of group.repos) {
|
|
452
|
+
const planning = repo.planning ? '[planning]' : '[no planning]';
|
|
453
|
+
process.stdout.write(` ${repo.role.padEnd(15)} ${repo.path.padEnd(30)} ${planning}\n`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
process.stdout.write('\n');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} else if (cmd === 'info') {
|
|
460
|
+
const cwd = process.cwd();
|
|
461
|
+
const marker = readMarker(cwd);
|
|
462
|
+
if (!marker) {
|
|
463
|
+
process.stdout.write(`${TAG} This repo is not part of any polyrepo group\n`);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
process.stdout.write(`\nThis repo belongs to polyrepo group: ${marker.name}\n`);
|
|
467
|
+
process.stdout.write(`Role: ${marker.role}\n`);
|
|
468
|
+
const group = listGroup(marker.name);
|
|
469
|
+
if (group) {
|
|
470
|
+
const repo = group.repos.find(r => r.path === cwd);
|
|
471
|
+
process.stdout.write(`Planning: ${(repo ? repo.planning : true) ? 'yes' : 'no'}\n`);
|
|
472
|
+
} else {
|
|
473
|
+
process.stdout.write(`Planning: yes\n`);
|
|
474
|
+
}
|
|
475
|
+
if (marker.docs) {
|
|
476
|
+
process.stdout.write(`Docs:\n`);
|
|
477
|
+
for (const [key, val] of Object.entries(marker.docs)) {
|
|
478
|
+
process.stdout.write(` ${key.padEnd(12)} ${val}\n`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
process.stdout.write('\n');
|
|
482
|
+
} else if (cmd === 'docs') {
|
|
483
|
+
const subcmd = args[1];
|
|
484
|
+
const cwd = process.cwd();
|
|
485
|
+
if (!subcmd || subcmd === 'show') {
|
|
486
|
+
const marker = readMarker(cwd);
|
|
487
|
+
if (!marker) {
|
|
488
|
+
console.error(`${TAG} No polyrepo marker found in current directory`);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
if (!marker.docs || Object.keys(marker.docs).length === 0) {
|
|
492
|
+
process.stdout.write(`${TAG} No docs paths configured for this repo\n`);
|
|
493
|
+
} else {
|
|
494
|
+
process.stdout.write(`\nDocs paths for ${marker.name} (${marker.role}):\n`);
|
|
495
|
+
for (const [key, val] of Object.entries(marker.docs)) {
|
|
496
|
+
process.stdout.write(` ${key.padEnd(12)} ${val}\n`);
|
|
497
|
+
}
|
|
498
|
+
process.stdout.write('\n');
|
|
499
|
+
}
|
|
500
|
+
} else if (subcmd === 'set') {
|
|
501
|
+
// docs set <key> <path>
|
|
502
|
+
const key = args[2];
|
|
503
|
+
const docPath = args[3];
|
|
504
|
+
if (!key || !docPath) {
|
|
505
|
+
console.error(`${TAG} docs set: usage: docs set <key> <path>`);
|
|
506
|
+
console.error(` Keys: user, developer, examples (or any custom key)`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
const result = setDocs(cwd, { [key]: docPath });
|
|
510
|
+
if (result.ok) {
|
|
511
|
+
process.stdout.write(`${TAG} Set docs.${key} = ${docPath}\n`);
|
|
512
|
+
} else {
|
|
513
|
+
console.error(`${TAG} Error: ${result.error}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
} else if (subcmd === 'remove') {
|
|
517
|
+
const key = args[2];
|
|
518
|
+
if (!key) {
|
|
519
|
+
console.error(`${TAG} docs remove: usage: docs remove <key>`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
const result = setDocs(cwd, { [key]: null });
|
|
523
|
+
if (result.ok) {
|
|
524
|
+
process.stdout.write(`${TAG} Removed docs.${key}\n`);
|
|
525
|
+
} else {
|
|
526
|
+
console.error(`${TAG} Error: ${result.error}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
console.error(`${TAG} docs: unknown subcommand '${subcmd}'. Use: show, set, remove`);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
console.error(`${TAG} Unknown command: ${cmd}`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Export for testability
|
|
540
|
+
module.exports = {
|
|
541
|
+
createGroup,
|
|
542
|
+
addRepo,
|
|
543
|
+
removeRepo,
|
|
544
|
+
listGroups,
|
|
545
|
+
listGroup,
|
|
546
|
+
loadGroup,
|
|
547
|
+
saveGroup,
|
|
548
|
+
writeMarker,
|
|
549
|
+
readMarker,
|
|
550
|
+
removeMarker,
|
|
551
|
+
setDocs,
|
|
552
|
+
ensurePolyreposDir,
|
|
553
|
+
POLYREPOS_DIR,
|
|
554
|
+
MARKER_FILE
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// Run CLI if executed directly
|
|
558
|
+
if (require.main === module) {
|
|
559
|
+
handleCLI();
|
|
560
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/prism-priority.cjs
|
|
4
|
+
// PRISM failure probability priority ranker for roadmap signal injection.
|
|
5
|
+
// Requirements: SIG-03
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/prism-priority.cjs [--path=check-results.ndjson]
|
|
9
|
+
//
|
|
10
|
+
// Reads check-results.ndjson, extracts PRISM failure entries, ranks by
|
|
11
|
+
// P(failure) x impact, and outputs a formatted priority signal block
|
|
12
|
+
// for injection into plan-phase.md quorum context.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// ── Impact score map ─────────────────────────────────────────────────────────
|
|
18
|
+
const IMPACT_SCORES = {
|
|
19
|
+
'prism:quorum': 10,
|
|
20
|
+
'prism:mcp-availability': 7,
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_IMPACT = 5;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* readPrismResults(ndjsonPath) — reads check-results.ndjson and filters PRISM entries.
|
|
26
|
+
* @param {string} [ndjsonPath] - path to check-results.ndjson
|
|
27
|
+
* @returns {Array<{ check_id: string, result: string, summary: string, timestamp: string, metadata: object }>}
|
|
28
|
+
*/
|
|
29
|
+
function readPrismResults(ndjsonPath) {
|
|
30
|
+
const p = ndjsonPath || path.join(process.cwd(), '.planning', 'formal', 'check-results.ndjson');
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(p)) return [];
|
|
33
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
34
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
35
|
+
|
|
36
|
+
// Parse and filter for PRISM entries
|
|
37
|
+
const prismEntries = [];
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
try {
|
|
40
|
+
const entry = JSON.parse(line);
|
|
41
|
+
if (entry.formalism === 'prism') {
|
|
42
|
+
prismEntries.push({
|
|
43
|
+
check_id: entry.check_id || 'unknown',
|
|
44
|
+
result: entry.result || 'unknown',
|
|
45
|
+
summary: entry.summary || '',
|
|
46
|
+
timestamp: entry.timestamp || '',
|
|
47
|
+
metadata: entry.metadata || {},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
} catch (_) { /* skip malformed lines */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Group by check_id, keep only most recent entry per check_id
|
|
54
|
+
const byCheckId = new Map();
|
|
55
|
+
for (const entry of prismEntries) {
|
|
56
|
+
const existing = byCheckId.get(entry.check_id);
|
|
57
|
+
if (!existing || entry.timestamp > existing.timestamp) {
|
|
58
|
+
byCheckId.set(entry.check_id, entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Array.from(byCheckId.values());
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* rankFailureModes(prismResults) — ranks failure modes by P(failure) x impact.
|
|
70
|
+
* @param {Array<{ check_id: string, result: string, summary: string, timestamp: string, metadata: object }>} prismResults
|
|
71
|
+
* @returns {Array<{ check_id: string, priority: number, p_failure: number, impact: number, summary: string }>}
|
|
72
|
+
*/
|
|
73
|
+
function rankFailureModes(prismResults) {
|
|
74
|
+
const failures = prismResults.filter(e => e.result === 'fail' || e.result === 'warn');
|
|
75
|
+
if (failures.length === 0) return [];
|
|
76
|
+
|
|
77
|
+
const ranked = failures.map(entry => {
|
|
78
|
+
// Determine P(failure)
|
|
79
|
+
let p_failure;
|
|
80
|
+
if (entry.result === 'fail') {
|
|
81
|
+
p_failure = 1.0;
|
|
82
|
+
} else {
|
|
83
|
+
// warn results — uncertain, use 0.5
|
|
84
|
+
p_failure = 0.5;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Determine impact
|
|
88
|
+
const impact = IMPACT_SCORES[entry.check_id] || DEFAULT_IMPACT;
|
|
89
|
+
|
|
90
|
+
// Compute priority
|
|
91
|
+
const priority = Math.round(p_failure * impact * 100) / 100;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
check_id: entry.check_id,
|
|
95
|
+
priority,
|
|
96
|
+
p_failure,
|
|
97
|
+
impact,
|
|
98
|
+
summary: entry.summary,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Sort descending by priority
|
|
103
|
+
ranked.sort((a, b) => b.priority - a.priority);
|
|
104
|
+
|
|
105
|
+
return ranked;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* formatPrioritySignal(rankedModes) — produces formatted text block for quorum injection.
|
|
110
|
+
* @param {Array<{ check_id: string, priority: number, p_failure: number, impact: number, summary: string }>} rankedModes
|
|
111
|
+
* @returns {string|null} - formatted text block, or null if no failures
|
|
112
|
+
*/
|
|
113
|
+
function formatPrioritySignal(rankedModes) {
|
|
114
|
+
if (!rankedModes || rankedModes.length === 0) return null;
|
|
115
|
+
|
|
116
|
+
const lines = [
|
|
117
|
+
'=== PRISM Priority Signal ===',
|
|
118
|
+
'Failure modes ranked by P(failure) x impact:',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < rankedModes.length; i++) {
|
|
122
|
+
const mode = rankedModes[i];
|
|
123
|
+
lines.push(
|
|
124
|
+
(i + 1) + '. [' + mode.check_id + '] priority=' + mode.priority.toFixed(1) +
|
|
125
|
+
' -- P(fail)=' + mode.p_failure.toFixed(1) +
|
|
126
|
+
', impact=' + mode.impact +
|
|
127
|
+
' -- "' + mode.summary + '"'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lines.push('=============================');
|
|
132
|
+
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── CLI entrypoint ───────────────────────────────────────────────────────────
|
|
137
|
+
if (require.main === module) {
|
|
138
|
+
const args = process.argv.slice(2);
|
|
139
|
+
const pathArg = args.find(a => a.startsWith('--path='));
|
|
140
|
+
const ndjsonPath = pathArg ? pathArg.split('=')[1] : undefined;
|
|
141
|
+
|
|
142
|
+
const results = readPrismResults(ndjsonPath);
|
|
143
|
+
const ranked = rankFailureModes(results);
|
|
144
|
+
const signal = formatPrioritySignal(ranked);
|
|
145
|
+
|
|
146
|
+
if (signal) {
|
|
147
|
+
process.stdout.write(signal + '\n');
|
|
148
|
+
} else {
|
|
149
|
+
process.stderr.write('No PRISM failures found\n');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { readPrismResults, rankFailureModes, formatPrioritySignal };
|