@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,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* validate-requirements-haiku.cjs
|
|
6
|
+
*
|
|
7
|
+
* Semantic requirements validator using Claude Haiku
|
|
8
|
+
*
|
|
9
|
+
* Detects:
|
|
10
|
+
* - DUPLICATES: Different requirement IDs with same intent
|
|
11
|
+
* - CONTRADICTIONS: Requirements that cannot both be satisfied
|
|
12
|
+
* - AMBIGUITY: Requirements with multiple incompatible interpretations
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node bin/validate-requirements-haiku.cjs [--envelope=path] [--passes=3] [--freeze]
|
|
16
|
+
*
|
|
17
|
+
* Exits with code 0 for validation complete (regardless of findings).
|
|
18
|
+
* Exits with code 1 for operational errors (missing file, API failure, etc).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const https = require('https');
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Raw HTTPS Haiku API helper (replaces SDK dependency)
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function callHaikuAPI(apiKey, prompt, maxTokens) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const body = JSON.stringify({
|
|
32
|
+
model: 'claude-haiku-4-5-20251001',
|
|
33
|
+
max_tokens: maxTokens,
|
|
34
|
+
messages: [{ role: 'user', content: prompt }],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const req = https.request({
|
|
38
|
+
hostname: 'api.anthropic.com',
|
|
39
|
+
path: '/v1/messages',
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'x-api-key': apiKey,
|
|
44
|
+
'anthropic-version': '2023-06-01',
|
|
45
|
+
'Content-Length': Buffer.byteLength(body),
|
|
46
|
+
},
|
|
47
|
+
timeout: 30000,
|
|
48
|
+
}, (res) => {
|
|
49
|
+
let data = '';
|
|
50
|
+
res.on('data', chunk => { data += chunk; });
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(data);
|
|
54
|
+
const text = ((parsed.content || [])[0] || {}).text || '';
|
|
55
|
+
resolve(text);
|
|
56
|
+
} catch (e) { reject(e); }
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
req.on('error', reject);
|
|
60
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
61
|
+
req.write(body);
|
|
62
|
+
req.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// buildValidationPrompt
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Construct the Haiku validation prompt with explicit rubrics
|
|
72
|
+
* @param {Array} requirements - Array of requirement objects { id, text, category, phase }
|
|
73
|
+
* @returns {string} The prompt string
|
|
74
|
+
*/
|
|
75
|
+
function buildValidationPrompt(requirements) {
|
|
76
|
+
const reqList = requirements
|
|
77
|
+
.map(r => `- ${r.id}: ${r.text}`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
|
|
80
|
+
const prompt = `You are reviewing a requirements envelope for quality issues.
|
|
81
|
+
|
|
82
|
+
Requirements:
|
|
83
|
+
${reqList}
|
|
84
|
+
|
|
85
|
+
Analyze for exactly these three categories:
|
|
86
|
+
|
|
87
|
+
1. DUPLICATES: Different IDs where the INTENT is the same (not just similar wording).
|
|
88
|
+
Example: "Users can log in" and "Authentication allows user access" are duplicates.
|
|
89
|
+
NOT duplicates: "Users can log in" and "Admins can log in" (different scope).
|
|
90
|
+
|
|
91
|
+
2. CONTRADICTIONS: Two requirements that CANNOT BOTH be satisfied.
|
|
92
|
+
Example: "Data is immutable" and "Data auto-updates daily" are contradictory.
|
|
93
|
+
NOT contradictions: "Data is immutable" and "Amendments require approval" (compatible).
|
|
94
|
+
|
|
95
|
+
3. AMBIGUITY: A single requirement that admits two or more INCOMPATIBLE interpretations.
|
|
96
|
+
Example: "The system should be fast" (fast for whom? what metric?).
|
|
97
|
+
NOT ambiguous: "Response time under 200ms for 95th percentile" (specific).
|
|
98
|
+
|
|
99
|
+
Return ONLY valid JSON (no markdown, no explanation):
|
|
100
|
+
{
|
|
101
|
+
"findings": [
|
|
102
|
+
{
|
|
103
|
+
"type": "duplicate|contradiction|ambiguity",
|
|
104
|
+
"requirement_ids": ["ENV-XX", "ENV-YY"],
|
|
105
|
+
"description": "...",
|
|
106
|
+
"severity": "high|medium|low",
|
|
107
|
+
"suggested_resolution": "..."
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"summary": "N duplicates, N contradictions, N ambiguities found"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
If no issues found, return: {"findings": [], "summary": "No issues found"}`;
|
|
114
|
+
|
|
115
|
+
return prompt;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
// parseHaikuResponse
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse Haiku JSON response, handling markdown wrapping
|
|
124
|
+
* @param {string} responseText - Raw Haiku response
|
|
125
|
+
* @returns {Object} Parsed findings or error object
|
|
126
|
+
*/
|
|
127
|
+
function parseHaikuResponse(responseText) {
|
|
128
|
+
try {
|
|
129
|
+
// Try to extract JSON from markdown code fence
|
|
130
|
+
let jsonStr = responseText;
|
|
131
|
+
const codeBlockMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
132
|
+
if (codeBlockMatch) {
|
|
133
|
+
jsonStr = codeBlockMatch[1];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parsed = JSON.parse(jsonStr);
|
|
137
|
+
|
|
138
|
+
// Validate structure
|
|
139
|
+
if (!Array.isArray(parsed.findings)) {
|
|
140
|
+
return { findings: [], summary: 'Invalid structure', error: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Ensure each finding has required fields
|
|
144
|
+
for (const finding of parsed.findings) {
|
|
145
|
+
if (!finding.type || !Array.isArray(finding.requirement_ids) || !finding.description) {
|
|
146
|
+
return { findings: [], summary: 'Invalid finding structure', error: true };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return parsed;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return { findings: [], summary: 'Parse error: ' + e.message, error: true };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
// aggregateFindings
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Aggregate multi-pass results — only report findings in 2+ of N passes
|
|
162
|
+
* @param {Array} passResults - Array of parsed responses from Haiku passes
|
|
163
|
+
* @returns {Object} { confirmed, total_passes, agreement_threshold }
|
|
164
|
+
*/
|
|
165
|
+
function aggregateFindings(passResults) {
|
|
166
|
+
const total_passes = passResults.length;
|
|
167
|
+
const agreement_threshold = Math.ceil(total_passes / 2);
|
|
168
|
+
|
|
169
|
+
// Collect all unique findings with pass counts
|
|
170
|
+
const findingMap = new Map();
|
|
171
|
+
|
|
172
|
+
for (let passIdx = 0; passIdx < passResults.length; passIdx++) {
|
|
173
|
+
const result = passResults[passIdx];
|
|
174
|
+
if (!result.findings) continue;
|
|
175
|
+
|
|
176
|
+
for (const finding of result.findings) {
|
|
177
|
+
// Create a key for matching: type + sorted IDs
|
|
178
|
+
const key = JSON.stringify({
|
|
179
|
+
type: finding.type,
|
|
180
|
+
ids: Array.from(new Set(finding.requirement_ids)).sort(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!findingMap.has(key)) {
|
|
184
|
+
findingMap.set(key, {
|
|
185
|
+
finding: finding,
|
|
186
|
+
passes: new Set(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
findingMap.get(key).passes.add(passIdx);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Filter to confirmed findings (2+ passes)
|
|
195
|
+
const confirmed = Array.from(findingMap.values())
|
|
196
|
+
.filter(item => item.passes.size >= agreement_threshold)
|
|
197
|
+
.map(item => item.finding);
|
|
198
|
+
|
|
199
|
+
return { confirmed, total_passes, agreement_threshold };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
// validateRequirements
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Main validation pipeline
|
|
208
|
+
* @param {Object} options - { envelopePath, passes, apiKey, mockCall }
|
|
209
|
+
* @returns {Promise<Object>} Validation result
|
|
210
|
+
*/
|
|
211
|
+
async function validateRequirements(options = {}) {
|
|
212
|
+
const {
|
|
213
|
+
envelopePath = '.planning/formal/requirements.json',
|
|
214
|
+
passes = 3,
|
|
215
|
+
apiKey = process.env.ANTHROPIC_API_KEY,
|
|
216
|
+
mockCall = null, // For testing: mock Haiku response
|
|
217
|
+
} = options;
|
|
218
|
+
|
|
219
|
+
// Read envelope first (needed to check frozen_at before SDK availability)
|
|
220
|
+
if (!fs.existsSync(envelopePath)) {
|
|
221
|
+
return { status: 'error', reason: `Envelope file not found: ${envelopePath}` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let envelope;
|
|
225
|
+
try {
|
|
226
|
+
const content = fs.readFileSync(envelopePath, 'utf8');
|
|
227
|
+
envelope = JSON.parse(content);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return { status: 'error', reason: `Failed to read envelope: ${e.message}` };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if already frozen (before SDK check)
|
|
233
|
+
if (envelope.frozen_at) {
|
|
234
|
+
return { status: 'already-frozen', frozen_at: envelope.frozen_at };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if API key is available (skip check if using mockCall)
|
|
238
|
+
if (!apiKey && !mockCall) {
|
|
239
|
+
return { status: 'skipped', reason: 'ANTHROPIC_API_KEY not set' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Extract requirements array
|
|
243
|
+
const requirements = Array.isArray(envelope.requirements) ? envelope.requirements : [];
|
|
244
|
+
if (requirements.length === 0) {
|
|
245
|
+
return { status: 'validated', confirmed: [], total_passes: 0, message: 'No requirements to validate' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Build prompt
|
|
249
|
+
const prompt = buildValidationPrompt(requirements);
|
|
250
|
+
|
|
251
|
+
// Run Haiku passes
|
|
252
|
+
const rawPasses = [];
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
for (let i = 0; i < passes; i++) {
|
|
256
|
+
let responseText;
|
|
257
|
+
|
|
258
|
+
if (mockCall) {
|
|
259
|
+
// For testing
|
|
260
|
+
responseText = mockCall();
|
|
261
|
+
} else {
|
|
262
|
+
// Real Haiku call via raw HTTPS
|
|
263
|
+
responseText = await callHaikuAPI(apiKey, prompt, 4096);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const parsed = parseHaikuResponse(responseText);
|
|
267
|
+
rawPasses.push(parsed);
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return { status: 'error', reason: `Haiku call failed: ${e.message}` };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Aggregate findings
|
|
274
|
+
const { confirmed, total_passes, agreement_threshold } = aggregateFindings(rawPasses);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
status: 'validated',
|
|
278
|
+
confirmed,
|
|
279
|
+
total_passes,
|
|
280
|
+
agreement_threshold,
|
|
281
|
+
raw_passes: rawPasses,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
286
|
+
// freezeEnvelope
|
|
287
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Freeze the envelope by setting frozen_at timestamp
|
|
291
|
+
* @param {string} envelopePath - Path to envelope file
|
|
292
|
+
* @returns {Object} { frozen: true, frozen_at }
|
|
293
|
+
*/
|
|
294
|
+
function freezeEnvelope(envelopePath) {
|
|
295
|
+
if (!fs.existsSync(envelopePath)) {
|
|
296
|
+
throw new Error(`Envelope file not found: ${envelopePath}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let envelope;
|
|
300
|
+
try {
|
|
301
|
+
const content = fs.readFileSync(envelopePath, 'utf8');
|
|
302
|
+
envelope = JSON.parse(content);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
throw new Error(`Failed to read envelope: ${e.message}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Set frozen_at timestamp
|
|
308
|
+
const frozen_at = new Date().toISOString();
|
|
309
|
+
envelope.frozen_at = frozen_at;
|
|
310
|
+
|
|
311
|
+
// Write atomically: temp file + rename
|
|
312
|
+
const dir = path.dirname(envelopePath);
|
|
313
|
+
const basename = path.basename(envelopePath);
|
|
314
|
+
const tempPath = path.join(dir, '.' + basename + '.tmp');
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
fs.writeFileSync(tempPath, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
|
|
318
|
+
fs.renameSync(tempPath, envelopePath);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// Clean up temp file if rename failed
|
|
321
|
+
try {
|
|
322
|
+
fs.unlinkSync(tempPath);
|
|
323
|
+
} catch (e2) {
|
|
324
|
+
// Ignore cleanup error
|
|
325
|
+
}
|
|
326
|
+
throw new Error(`Failed to write envelope: ${e.message}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { frozen: true, frozen_at };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
333
|
+
// CLI entrypoint
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
async function main() {
|
|
337
|
+
// Parse CLI arguments
|
|
338
|
+
const args = {};
|
|
339
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
340
|
+
const arg = process.argv[i];
|
|
341
|
+
if (arg.startsWith('--')) {
|
|
342
|
+
const [key, value] = arg.slice(2).split('=');
|
|
343
|
+
args[key] = value || true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const envelopePath = args.envelope || '.planning/formal/requirements.json';
|
|
348
|
+
const passes = parseInt(args.passes || '3', 10);
|
|
349
|
+
const shouldFreeze = args.freeze === true || args.freeze === '';
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
// Run validation
|
|
353
|
+
const result = await validateRequirements({ envelopePath, passes });
|
|
354
|
+
|
|
355
|
+
if (result.status === 'skipped') {
|
|
356
|
+
console.log(`Validation skipped: ${result.reason}`);
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (result.status === 'error') {
|
|
361
|
+
console.error(`Validation error: ${result.reason}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (result.status === 'already-frozen') {
|
|
366
|
+
console.log(`Envelope already frozen at ${result.frozen_at}`);
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Display results
|
|
371
|
+
console.log(`\nValidation Results (${result.total_passes} passes, ${result.agreement_threshold}+ agreement):\n`);
|
|
372
|
+
|
|
373
|
+
if (result.confirmed.length === 0) {
|
|
374
|
+
console.log('CONFIRMED FINDINGS: None\n');
|
|
375
|
+
} else {
|
|
376
|
+
console.log('CONFIRMED FINDINGS:');
|
|
377
|
+
for (const finding of result.confirmed) {
|
|
378
|
+
const severity = finding.severity?.toUpperCase() || 'MEDIUM';
|
|
379
|
+
const type = finding.type.toUpperCase();
|
|
380
|
+
const ids = finding.requirement_ids.join(', ');
|
|
381
|
+
console.log(` [${severity}] ${type}: ${ids} -- ${finding.description}`);
|
|
382
|
+
}
|
|
383
|
+
console.log('');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Summary
|
|
387
|
+
const counts = {
|
|
388
|
+
duplicates: result.confirmed.filter(f => f.type === 'duplicate').length,
|
|
389
|
+
contradictions: result.confirmed.filter(f => f.type === 'contradiction').length,
|
|
390
|
+
ambiguities: result.confirmed.filter(f => f.type === 'ambiguity').length,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const summary = `${counts.duplicates} duplicates, ${counts.contradictions} contradictions, ${counts.ambiguities} ambiguities`;
|
|
394
|
+
console.log(`Summary: ${summary}\n`);
|
|
395
|
+
|
|
396
|
+
// Freeze if requested and no high-severity findings
|
|
397
|
+
if (shouldFreeze) {
|
|
398
|
+
const hasHighSeverity = result.confirmed.some(f => f.severity === 'high');
|
|
399
|
+
if (hasHighSeverity) {
|
|
400
|
+
console.log('Cannot freeze: high-severity findings must be resolved first\n');
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const frozen = freezeEnvelope(envelopePath);
|
|
405
|
+
console.log(`Envelope frozen at: ${frozen.frozen_at}`);
|
|
406
|
+
} else {
|
|
407
|
+
console.log(`To freeze: node bin/validate-requirements-haiku.cjs --freeze`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
process.exit(0);
|
|
411
|
+
} catch (e) {
|
|
412
|
+
console.error(`Fatal error: ${e.message}`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
+
// Exports
|
|
419
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
module.exports = {
|
|
422
|
+
buildValidationPrompt,
|
|
423
|
+
parseHaikuResponse,
|
|
424
|
+
validateRequirements,
|
|
425
|
+
aggregateFindings,
|
|
426
|
+
freezeEnvelope,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Run CLI if this is the main module
|
|
430
|
+
if (require.main === module) {
|
|
431
|
+
main().catch(err => {
|
|
432
|
+
console.error(`Unhandled error: ${err.message}`);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
});
|
|
435
|
+
}
|