@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,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/sensitivity-report.cjs
|
|
4
|
+
// Reads .planning/formal/sensitivity-report.ndjson, ranks parameters by outcome-flip count,
|
|
5
|
+
// and generates .planning/formal/sensitivity-report.md with annotated code paths, test cases,
|
|
6
|
+
// and monitoring metrics.
|
|
7
|
+
// Requirements: SENS-03
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const TAG = '[sensitivity-report]';
|
|
13
|
+
|
|
14
|
+
const REPORT_NDJSON_PATH = process.env.SENSITIVITY_REPORT_PATH ||
|
|
15
|
+
path.join(__dirname, '..', '.planning', 'formal', 'sensitivity-report.ndjson');
|
|
16
|
+
|
|
17
|
+
const REPORT_MD_PATH = process.env.SENSITIVITY_MD_PATH ||
|
|
18
|
+
path.join(__dirname, '..', '.planning', 'formal', 'sensitivity-report.md');
|
|
19
|
+
|
|
20
|
+
// ── Hardcoded annotations (code paths known from codebase analysis) ──────────
|
|
21
|
+
// Maps parameter name → { codePath, testCases[], monitoring[] }
|
|
22
|
+
|
|
23
|
+
const PARAM_ANNOTATIONS = {
|
|
24
|
+
MaxSize: {
|
|
25
|
+
codePath: 'hooks/qgsd-prompt.js FAN_OUT_COUNT; .planning/formal/tla/MCsafety.cfg MaxSize',
|
|
26
|
+
testCases: [
|
|
27
|
+
'Test quorum at N=2 boundary: set FAN_OUT_COUNT=2 in providers.json and run quorum round',
|
|
28
|
+
'Test quorum at N=1 (no quorum): verify workflow rejects insufficient available slots',
|
|
29
|
+
],
|
|
30
|
+
monitoring: [
|
|
31
|
+
'Track FAN_OUT_COUNT per planning session in quorum-scoreboard.json',
|
|
32
|
+
'Alert if available slot count drops below MaxSize threshold for 2+ consecutive sessions',
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
tp_rate: {
|
|
36
|
+
codePath: 'bin/export-prism-constants.cjs TP_PRIOR=0.85; .planning/formal/prism/quorum.pm tp_rate const',
|
|
37
|
+
testCases: [
|
|
38
|
+
'Test quorum with 2/4 slots returning APPROVE (tp_rate≈0.5) — verify inconclusive behavior',
|
|
39
|
+
'Test quorum with all slots UNAVAILABLE — verify graceful degradation exits 0',
|
|
40
|
+
],
|
|
41
|
+
monitoring: [
|
|
42
|
+
'Track per-slot APPROVE rate in quorum-scoreboard.json rounds array',
|
|
43
|
+
'Alert if any slot tp_rate falls below 0.6 for 5+ consecutive quorum rounds',
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
MaxDeliberation: {
|
|
47
|
+
codePath: '.planning/formal/tla/MCsafety.cfg MaxDeliberation=7; src/machines/qgsd-workflow.machine.ts MaxDeliberation guard',
|
|
48
|
+
testCases: [
|
|
49
|
+
'Test quorum workflow with max deliberation rounds reached — verify DECIDED fallback',
|
|
50
|
+
'Test rapid-fire slot responses (all within 1 deliberation round)',
|
|
51
|
+
],
|
|
52
|
+
monitoring: [
|
|
53
|
+
'Track deliberation round count per quorum session in quorum-scoreboard.json',
|
|
54
|
+
'Alert if any session reaches MaxDeliberation-1 rounds (approaching timeout)',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
unavail: {
|
|
58
|
+
codePath: 'bin/export-prism-constants.cjs UNAVAIL_PRIOR=0.15; .planning/formal/prism/quorum.pm unavail const',
|
|
59
|
+
testCases: [
|
|
60
|
+
'Test quorum with 2/4 slots UNAVAILABLE — verify 2-of-2 quorum still reaches DECIDED',
|
|
61
|
+
'Test quorum with 3/4 slots UNAVAILABLE — verify graceful INCONCLUSIVE result',
|
|
62
|
+
],
|
|
63
|
+
monitoring: [
|
|
64
|
+
'Track per-slot UNAVAIL rate in quorum-scoreboard.json',
|
|
65
|
+
'Alert if aggregate unavail rate exceeds 0.3 (less than MIN_QUORUM slots reliably available)',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ── NDJSON parser (fail-open) ─────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function parseNDJSON(filePath) {
|
|
73
|
+
let content;
|
|
74
|
+
try {
|
|
75
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
76
|
+
} catch (_) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const records = [];
|
|
80
|
+
for (const line of content.trim().split('\n')) {
|
|
81
|
+
if (!line.trim()) continue;
|
|
82
|
+
try { records.push(JSON.parse(line)); } catch (_) {}
|
|
83
|
+
}
|
|
84
|
+
return records;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Report generation ─────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function generateReport(records) {
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
const lines = [
|
|
92
|
+
'# Sensitivity Report — QGSD v0.20',
|
|
93
|
+
'',
|
|
94
|
+
'Generated: ' + now,
|
|
95
|
+
'Source: .planning/formal/sensitivity-report.ndjson (' + records.length + ' records)',
|
|
96
|
+
'',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
if (records.length === 0) {
|
|
100
|
+
lines.push('## No Data Available');
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push('Run `node bin/run-sensitivity-sweep.cjs` to generate sweep data.');
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Group by parameter and count outcome flips
|
|
107
|
+
const paramData = {};
|
|
108
|
+
for (const r of records) {
|
|
109
|
+
const p = r.metadata && r.metadata.parameter;
|
|
110
|
+
if (!p) continue;
|
|
111
|
+
if (!paramData[p]) {
|
|
112
|
+
paramData[p] = {
|
|
113
|
+
description: r.property || ('Sweep of ' + p),
|
|
114
|
+
records: [],
|
|
115
|
+
flips: [],
|
|
116
|
+
stable: [],
|
|
117
|
+
inconclusive: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
paramData[p].records.push(r);
|
|
121
|
+
const delta = r.metadata && r.metadata.delta;
|
|
122
|
+
if (delta && delta.startsWith('flip-to-')) {
|
|
123
|
+
paramData[p].flips.push({ value: r.metadata.value, result: r.result });
|
|
124
|
+
} else if (delta === 'stable') {
|
|
125
|
+
paramData[p].stable.push(r.metadata.value);
|
|
126
|
+
} else {
|
|
127
|
+
paramData[p].inconclusive.push(r.metadata.value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Rank parameters by flip count (descending)
|
|
132
|
+
const ranked = Object.entries(paramData)
|
|
133
|
+
.sort((a, b) => b[1].flips.length - a[1].flips.length);
|
|
134
|
+
|
|
135
|
+
const hasAnyFlips = ranked.some(([, d]) => d.flips.length > 0);
|
|
136
|
+
|
|
137
|
+
if (!hasAnyFlips) {
|
|
138
|
+
lines.push('## No Outcome Flips Detected');
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push(
|
|
141
|
+
'All sweep results were inconclusive (tools not installed) or stable. ' +
|
|
142
|
+
'Install TLC and PRISM to run a live sensitivity sweep.'
|
|
143
|
+
);
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push('### Parameter Summary');
|
|
146
|
+
lines.push('');
|
|
147
|
+
for (const [param, data] of ranked) {
|
|
148
|
+
lines.push('- **' + param + '**: ' + data.records.length + ' records — ' +
|
|
149
|
+
(data.inconclusive.length > 0
|
|
150
|
+
? 'all inconclusive (tools not installed)'
|
|
151
|
+
: 'all stable'));
|
|
152
|
+
}
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
lines.push('## High-Sensitivity Parameters (ranked by outcome-flip count)');
|
|
157
|
+
lines.push('');
|
|
158
|
+
|
|
159
|
+
let rank = 1;
|
|
160
|
+
for (const [param, data] of ranked) {
|
|
161
|
+
const ann = PARAM_ANNOTATIONS[param] || {
|
|
162
|
+
codePath: '(unknown — add to PARAM_ANNOTATIONS in sensitivity-report.cjs)',
|
|
163
|
+
testCases: ['Review ' + param + ' at boundary values'],
|
|
164
|
+
monitoring: ['Track ' + param + ' over time'],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const flipSummary = data.flips.length > 0
|
|
168
|
+
? data.flips.map(f => f.value + '→' + f.result).join(', ')
|
|
169
|
+
: 'none';
|
|
170
|
+
|
|
171
|
+
lines.push('### ' + rank + '. ' + param + ' — ' + data.flips.length + ' flip(s)');
|
|
172
|
+
lines.push('**Description:** ' + data.description);
|
|
173
|
+
lines.push('**Flip values:** ' + (data.flips.length > 0 ? flipSummary : 'none detected'));
|
|
174
|
+
if (data.inconclusive.length > 0) {
|
|
175
|
+
lines.push('**Inconclusive at:** ' + data.inconclusive.join(', ') + ' (tools not installed)');
|
|
176
|
+
}
|
|
177
|
+
lines.push('**Code path:** `' + ann.codePath + '`');
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push('**Recommended test cases:**');
|
|
180
|
+
for (const tc of ann.testCases) {
|
|
181
|
+
lines.push('- ' + tc);
|
|
182
|
+
}
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push('**Recommended monitoring:**');
|
|
185
|
+
for (const m of ann.monitoring) {
|
|
186
|
+
lines.push('- ' + m);
|
|
187
|
+
}
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push('---');
|
|
190
|
+
lines.push('');
|
|
191
|
+
rank++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function main() {
|
|
200
|
+
const records = parseNDJSON(REPORT_NDJSON_PATH);
|
|
201
|
+
|
|
202
|
+
if (records.length === 0) {
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
TAG + ' No records found in: ' + REPORT_NDJSON_PATH + '\n' +
|
|
205
|
+
TAG + ' Run `node bin/run-sensitivity-sweep.cjs` first.\n'
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
process.stderr.write(TAG + ' Loaded ' + records.length + ' records from: ' + REPORT_NDJSON_PATH + '\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const markdown = generateReport(records);
|
|
212
|
+
|
|
213
|
+
fs.mkdirSync(path.dirname(REPORT_MD_PATH), { recursive: true });
|
|
214
|
+
fs.writeFileSync(REPORT_MD_PATH, markdown + '\n', 'utf8');
|
|
215
|
+
|
|
216
|
+
process.stderr.write(TAG + ' Report written to: ' + REPORT_MD_PATH + '\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main();
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/sensitivity-sweep-feedback.cjs
|
|
4
|
+
// LOOP-03 (v0.21-03): Read sensitivity sweep report, compare empirical TP rate,
|
|
5
|
+
// re-run PRISM if deviation detected. Exits 0 unless a NEW threshold violation is found.
|
|
6
|
+
// Usage: node bin/sensitivity-sweep-feedback.cjs
|
|
7
|
+
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// ── Named constant ─────────────────────────────────────────────────────────────
|
|
13
|
+
const DEVIATION_THRESHOLD = 0.05;
|
|
14
|
+
|
|
15
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read .planning/formal/sensitivity-report.ndjson from cwd.
|
|
19
|
+
* Returns array of parsed NDJSON records, or null if file absent/malformed.
|
|
20
|
+
*/
|
|
21
|
+
function readSweepRecords(cwd) {
|
|
22
|
+
const reportPath = path.join(cwd, '.planning', 'formal', 'sensitivity-report.ndjson');
|
|
23
|
+
if (!fs.existsSync(reportPath)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(reportPath, 'utf8');
|
|
28
|
+
return raw.trim().split('\n').filter(l => l.length > 0).map(l => JSON.parse(l));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
process.stderr.write('[sensitivity-sweep-feedback] Warning: failed to parse sensitivity-report.ndjson: ' + e.message + '\n');
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute empirical TP rate from quorum-scoreboard.md in cwd.
|
|
37
|
+
* Format: | Slot | Wins | Losses |
|
|
38
|
+
* Returns a number (0..1) or null if absent/empty.
|
|
39
|
+
*/
|
|
40
|
+
function readEmpiricalRate(cwd) {
|
|
41
|
+
const sbPath = path.join(cwd, 'quorum-scoreboard.md');
|
|
42
|
+
if (!fs.existsSync(sbPath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const raw = fs.readFileSync(sbPath, 'utf8');
|
|
47
|
+
const rows = raw.split('\n');
|
|
48
|
+
let totalWins = 0;
|
|
49
|
+
let totalLosses = 0;
|
|
50
|
+
const rowRe = /\|\s*\S+\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|/g;
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
let m;
|
|
53
|
+
rowRe.lastIndex = 0;
|
|
54
|
+
while ((m = rowRe.exec(row)) !== null) {
|
|
55
|
+
totalWins += parseInt(m[1], 10);
|
|
56
|
+
totalLosses += parseInt(m[2], 10);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const total = totalWins + totalLosses;
|
|
60
|
+
if (total === 0) return null;
|
|
61
|
+
return totalWins / total;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
process.stderr.write('[sensitivity-sweep-feedback] Warning: failed to parse quorum-scoreboard.md: ' + e.message + '\n');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const cwd = process.cwd();
|
|
71
|
+
|
|
72
|
+
// 1. Read sensitivity report
|
|
73
|
+
const allRecords = readSweepRecords(cwd);
|
|
74
|
+
if (allRecords === null) {
|
|
75
|
+
process.stdout.write('[sensitivity-sweep-feedback] Warning: .planning/formal/sensitivity-report.ndjson not found — no feedback to process.\n');
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Filter to PRISM tp_rate records
|
|
80
|
+
const prismTpRecords = allRecords.filter(r =>
|
|
81
|
+
r.formalism === 'prism' &&
|
|
82
|
+
r.metadata &&
|
|
83
|
+
r.metadata.parameter === 'tp_rate'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 3. If all inconclusive — no actionable data
|
|
87
|
+
const actionableRecords = prismTpRecords.filter(r => r.result !== 'inconclusive');
|
|
88
|
+
if (actionableRecords.length === 0) {
|
|
89
|
+
process.stdout.write('[sensitivity-sweep-feedback] Warning: all PRISM tp_rate sweep records are inconclusive — PRISM not available, skipping feedback.\n');
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 4. Compute empirical TP rate
|
|
94
|
+
const empiricalRate = readEmpiricalRate(cwd);
|
|
95
|
+
if (empiricalRate === null) {
|
|
96
|
+
process.stdout.write('[sensitivity-sweep-feedback] Warning: quorum-scoreboard.md not found or empty — cannot compute empirical rate.\n');
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 5. Find nearest tested value (from actionable records)
|
|
101
|
+
const testedValues = actionableRecords.map(r => r.metadata.value);
|
|
102
|
+
const nearestTestedValue = testedValues.reduce((nearest, v) =>
|
|
103
|
+
Math.abs(empiricalRate - v) < Math.abs(empiricalRate - nearest) ? v : nearest
|
|
104
|
+
, testedValues[0]);
|
|
105
|
+
|
|
106
|
+
const deviation = Math.abs(empiricalRate - nearestTestedValue);
|
|
107
|
+
|
|
108
|
+
// 6. No deviation — exit 0
|
|
109
|
+
if (deviation <= DEVIATION_THRESHOLD) {
|
|
110
|
+
process.stdout.write(
|
|
111
|
+
'[sensitivity-sweep-feedback] No deviation detected: empirical rate=' + empiricalRate.toFixed(4) +
|
|
112
|
+
' nearest tested=' + nearestTestedValue + ' deviation=' + deviation.toFixed(4) +
|
|
113
|
+
' (threshold=' + DEVIATION_THRESHOLD + ').\n'
|
|
114
|
+
);
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 7. Deviation detected — check for flip-to-fail records near the empirical rate
|
|
119
|
+
process.stdout.write(
|
|
120
|
+
'[sensitivity-sweep-feedback] Deviation detected: empirical rate=' + empiricalRate.toFixed(4) +
|
|
121
|
+
' nearest tested=' + nearestTestedValue + ' deviation=' + deviation.toFixed(4) +
|
|
122
|
+
' (threshold=' + DEVIATION_THRESHOLD + ').\n'
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 7a. Check if any flip-to-fail record is near the empirical rate (within 2x threshold)
|
|
126
|
+
// If so, this is a NEW threshold violation — exit 1 immediately (conservative, no PRISM run required)
|
|
127
|
+
const flipToFailRecords = actionableRecords.filter(r =>
|
|
128
|
+
r.result === 'fail' &&
|
|
129
|
+
r.metadata.delta === 'flip-to-fail' &&
|
|
130
|
+
Math.abs(empiricalRate - r.metadata.value) <= DEVIATION_THRESHOLD * 2
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (flipToFailRecords.length > 0) {
|
|
134
|
+
process.stdout.write(
|
|
135
|
+
'[sensitivity-sweep-feedback] NEW THRESHOLD VIOLATION DETECTED: empirical rate=' + empiricalRate.toFixed(4) +
|
|
136
|
+
' is near flip-to-fail boundary (value=' + flipToFailRecords[0].metadata.value + ').\n'
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// 7b. Update rates.const and re-run PRISM to confirm (fail-open: proceed to exit 1 regardless)
|
|
140
|
+
const exportScript = path.join(__dirname, 'export-prism-constants.cjs');
|
|
141
|
+
const exportResult = spawnSync(process.execPath, [exportScript], {
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
cwd: cwd,
|
|
144
|
+
timeout: 10000,
|
|
145
|
+
});
|
|
146
|
+
if (exportResult.status !== 0 || exportResult.error) {
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
'[sensitivity-sweep-feedback] Warning: export-prism-constants pre-step failed — rates.const may be stale.\n' +
|
|
149
|
+
(exportResult.stderr || '') + '\n'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const runPrismScript = path.join(__dirname, 'run-prism.cjs');
|
|
154
|
+
const prismResult = spawnSync(process.execPath, [runPrismScript], {
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
cwd: cwd,
|
|
157
|
+
timeout: 60000,
|
|
158
|
+
});
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
'[sensitivity-sweep-feedback] PRISM re-run exit=' + prismResult.status + '. Exiting 1 (threshold violation).\n'
|
|
161
|
+
);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 7c. Deviation but no flip-to-fail records nearby — update rates.const and re-run PRISM
|
|
166
|
+
process.stdout.write('[sensitivity-sweep-feedback] No flip-to-fail boundary near empirical rate. Re-running PRISM with updated rates...\n');
|
|
167
|
+
|
|
168
|
+
const exportScript = path.join(__dirname, 'export-prism-constants.cjs');
|
|
169
|
+
const exportResult = spawnSync(process.execPath, [exportScript], {
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
cwd: cwd,
|
|
172
|
+
timeout: 10000,
|
|
173
|
+
});
|
|
174
|
+
if (exportResult.status !== 0 || exportResult.error) {
|
|
175
|
+
process.stderr.write(
|
|
176
|
+
'[sensitivity-sweep-feedback] Warning: export-prism-constants pre-step failed — rates.const may be stale.\n' +
|
|
177
|
+
(exportResult.stderr || '') + '\n'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const runPrismScript = path.join(__dirname, 'run-prism.cjs');
|
|
182
|
+
const prismResult = spawnSync(process.execPath, [runPrismScript], {
|
|
183
|
+
encoding: 'utf8',
|
|
184
|
+
cwd: cwd,
|
|
185
|
+
timeout: 60000,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (prismResult.status !== 0) {
|
|
189
|
+
process.stdout.write('[sensitivity-sweep-feedback] PRISM re-run failed after rate update (non-flip-to-fail deviation).\n');
|
|
190
|
+
process.exit(0); // Not a new flip-to-fail violation
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
process.stdout.write('[sensitivity-sweep-feedback] PRISM re-run passed after rate update.\n');
|
|
194
|
+
process.exit(0);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* set-secret.cjs
|
|
5
|
+
* Usage: node bin/set-secret.cjs <KEY_NAME> <value>
|
|
6
|
+
*
|
|
7
|
+
* Stores KEY_NAME=value in the OS keychain under service "qgsd",
|
|
8
|
+
* then syncs all qgsd secrets into ~/.claude.json mcpServers env blocks.
|
|
9
|
+
*/
|
|
10
|
+
const { set, syncToClaudeJson, SERVICE } = require('./secrets.cjs');
|
|
11
|
+
|
|
12
|
+
const [,, keyName, ...valueParts] = process.argv;
|
|
13
|
+
if (!keyName || valueParts.length === 0) {
|
|
14
|
+
console.error('Usage: node bin/set-secret.cjs <KEY_NAME> <value>');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const value = valueParts.join(' ');
|
|
18
|
+
|
|
19
|
+
(async () => {
|
|
20
|
+
try {
|
|
21
|
+
await set(SERVICE, keyName, value);
|
|
22
|
+
console.log(`[qgsd] Stored ${keyName} in keychain (service: ${SERVICE})`);
|
|
23
|
+
await syncToClaudeJson(SERVICE);
|
|
24
|
+
console.log('[qgsd] Synced keychain secrets to ~/.claude.json');
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error('[qgsd] Error:', e.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# setup-telemetry-cron.sh
|
|
3
|
+
#
|
|
4
|
+
# Installs an hourly cron entry that runs telemetry-collector.cjs + issue-classifier.cjs.
|
|
5
|
+
# Safe to run multiple times — idempotent.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash bin/setup-telemetry-cron.sh
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
NODE_BIN="$(command -v node)"
|
|
14
|
+
|
|
15
|
+
if [ -z "$NODE_BIN" ]; then
|
|
16
|
+
echo "ERROR: node not found in PATH. Install Node.js first."
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
CRON_CMD="$NODE_BIN $SCRIPT_DIR/telemetry-collector.cjs && $NODE_BIN $SCRIPT_DIR/issue-classifier.cjs"
|
|
21
|
+
|
|
22
|
+
# Idempotency check
|
|
23
|
+
if crontab -l 2>/dev/null | grep -q "telemetry-collector"; then
|
|
24
|
+
echo "Telemetry cron already installed."
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Install cron entry: top of every hour
|
|
29
|
+
(crontab -l 2>/dev/null; echo "0 * * * * $CRON_CMD >> /tmp/qgsd-telemetry.log 2>&1") | crontab -
|
|
30
|
+
|
|
31
|
+
echo "Telemetry cron installed."
|
|
32
|
+
|
|
33
|
+
# Windows: use Task Scheduler. Create a Basic Task that runs:
|
|
34
|
+
# node C:\path\to\qgsd\bin\telemetry-collector.cjs
|
|
35
|
+
# followed by: node C:\path\to\qgsd\bin\issue-classifier.cjs
|
|
36
|
+
# Trigger: Daily, repeat every 1 hour indefinitely.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P->F residual sweep function
|
|
3
|
+
* Reads acknowledged debt entries, compares production measurements against
|
|
4
|
+
* formal model thresholds, and returns a residual count with detail.
|
|
5
|
+
*
|
|
6
|
+
* Requirements: PF-01, PF-02, PF-03
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { readDebtLedger } = require('./debt-ledger.cjs');
|
|
13
|
+
const { compareDrift } = require('./compareDrift.cjs');
|
|
14
|
+
const { extractFormalExpected } = require('./extractFormalExpected.cjs');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sweep Production-to-Formal layer for divergent debt entries
|
|
18
|
+
* @param {object} [options]
|
|
19
|
+
* @param {string} [options.ledgerPath] - Override path to debt.json
|
|
20
|
+
* @param {string} [options.specDir] - Override path to spec directory
|
|
21
|
+
* @returns {{ residual: number, detail: object }}
|
|
22
|
+
*/
|
|
23
|
+
function sweepPtoF(options = {}) {
|
|
24
|
+
const ROOT = options.root || process.cwd();
|
|
25
|
+
const ledgerPath = options.ledgerPath || path.join(ROOT, '.planning/formal/debt.json');
|
|
26
|
+
const specDir = options.specDir || path.join(ROOT, '.planning/formal/spec');
|
|
27
|
+
|
|
28
|
+
// 1. Read ledger (fail-open: empty on error)
|
|
29
|
+
const ledger = readDebtLedger(ledgerPath);
|
|
30
|
+
|
|
31
|
+
// 2. Filter for acknowledged entries only (PF-03)
|
|
32
|
+
const acknowledged = (ledger.debt_entries || []).filter(e => e.status === 'acknowledged');
|
|
33
|
+
|
|
34
|
+
// 3. Separate linked vs unlinked
|
|
35
|
+
const withRef = acknowledged.filter(e => e.formal_ref != null);
|
|
36
|
+
const unlinked = acknowledged.filter(e => e.formal_ref == null);
|
|
37
|
+
|
|
38
|
+
// 4. Detect divergence for linked entries
|
|
39
|
+
const divergent = [];
|
|
40
|
+
for (const entry of withRef) {
|
|
41
|
+
const expected = extractFormalExpected(entry.formal_ref, { specDir });
|
|
42
|
+
if (compareDrift(entry, expected)) {
|
|
43
|
+
divergent.push({
|
|
44
|
+
id: entry.id,
|
|
45
|
+
formal_ref: entry.formal_ref,
|
|
46
|
+
measured: entry.meta?.measured_value,
|
|
47
|
+
expected,
|
|
48
|
+
issue_type: entry.issue_type,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
residual: divergent.length,
|
|
55
|
+
detail: {
|
|
56
|
+
divergent_entries: divergent,
|
|
57
|
+
skipped_unlinked: unlinked.length,
|
|
58
|
+
skipped_unlinked_ids: unlinked.map(e => e.id),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { sweepPtoF };
|