@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,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/generate-petri-net.cjs
|
|
4
|
+
// Generates a Graphviz DOT + SVG Petri Net for the QGSD quorum token-passing model.
|
|
5
|
+
// Requirements: PET-01, PET-02, PET-03
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node bin/generate-petri-net.cjs
|
|
9
|
+
//
|
|
10
|
+
// Output:
|
|
11
|
+
// .planning/formal/petri/quorum-petri-net.dot — Graphviz DOT source
|
|
12
|
+
// .planning/formal/petri/quorum-petri-net.svg — Rendered SVG (via @hpcc-js/wasm-graphviz)
|
|
13
|
+
//
|
|
14
|
+
// No system Graphviz install required — uses @hpcc-js/wasm-graphviz WASM build.
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// Quorum configuration
|
|
20
|
+
const QUORUM_SLOTS = ['gemini', 'opencode', 'copilot', 'codex', 'claude'];
|
|
21
|
+
const MIN_QUORUM_SIZE = Math.ceil(QUORUM_SLOTS.length / 2); // = 3
|
|
22
|
+
|
|
23
|
+
// Optional --min-quorum=N override (makes PET-03 deadlock check exercisable at runtime)
|
|
24
|
+
const minQuorumArg = process.argv.slice(2).find(a => a.startsWith('--min-quorum='));
|
|
25
|
+
const effectiveMinQuorum = minQuorumArg
|
|
26
|
+
? parseInt(minQuorumArg.split('=')[1], 10)
|
|
27
|
+
: MIN_QUORUM_SIZE;
|
|
28
|
+
|
|
29
|
+
// PET-03: structural deadlock check (pure logic — before any rendering)
|
|
30
|
+
// A structural deadlock occurs when the quorum transition can NEVER fire because
|
|
31
|
+
// min_quorum_size > available_slots (more approvals needed than slots available)
|
|
32
|
+
if (effectiveMinQuorum > QUORUM_SLOTS.length) {
|
|
33
|
+
process.stderr.write(
|
|
34
|
+
'[generate-petri-net] WARNING: Structural deadlock detected.\n' +
|
|
35
|
+
'[generate-petri-net] min_quorum_size (' + effectiveMinQuorum + ') > ' +
|
|
36
|
+
'available_slots (' + QUORUM_SLOTS.length + ').\n' +
|
|
37
|
+
'[generate-petri-net] Quorum transition can never fire.\n'
|
|
38
|
+
);
|
|
39
|
+
// Do NOT exit 1 — still emit the net for documentation purposes (per PET-03)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// buildDot: pure function — exported via _pure for unit testing
|
|
43
|
+
function buildDot(slots, minQuorum) {
|
|
44
|
+
return [
|
|
45
|
+
'digraph quorum_petri_net {',
|
|
46
|
+
' rankdir=LR;',
|
|
47
|
+
' label="QGSD Quorum Petri Net (min_quorum=' + minQuorum + '/' + slots.length + ')";',
|
|
48
|
+
' node [fontname="Helvetica"];',
|
|
49
|
+
'',
|
|
50
|
+
' // Places (circles)',
|
|
51
|
+
' node [shape=circle, fixedsize=true, width=1.2];',
|
|
52
|
+
' idle [label="idle"];',
|
|
53
|
+
' collecting [label="collecting\\nvotes"];',
|
|
54
|
+
' deliberating [label="deliberating"];',
|
|
55
|
+
' decided [label="decided"];',
|
|
56
|
+
'',
|
|
57
|
+
' // Transitions (filled rectangles)',
|
|
58
|
+
' node [shape=rect, height=0.3, width=1.5, style=filled, fillcolor=black, fontcolor=white];',
|
|
59
|
+
' t_start [label="start quorum"];',
|
|
60
|
+
' t_approve [label="approve\\n(>=' + minQuorum + '/' + slots.length + ')"];',
|
|
61
|
+
' t_deliberate [label="deliberate"];',
|
|
62
|
+
' t_force [label="force decide\\n(max rounds)"];',
|
|
63
|
+
'',
|
|
64
|
+
' // Arcs (bipartite: place->transition or transition->place only)',
|
|
65
|
+
' idle -> t_start;',
|
|
66
|
+
' t_start -> collecting;',
|
|
67
|
+
' collecting -> t_approve;',
|
|
68
|
+
' collecting -> t_deliberate;',
|
|
69
|
+
' t_approve -> decided;',
|
|
70
|
+
' t_deliberate -> deliberating;',
|
|
71
|
+
' deliberating -> t_approve;',
|
|
72
|
+
' deliberating -> t_force;',
|
|
73
|
+
' t_force -> decided;',
|
|
74
|
+
'}',
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Roadmap Petri Net (SIG-02) ───────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* parseRoadmapPhases(roadmapContent) — parses ROADMAP.md to extract phases and dependencies.
|
|
82
|
+
* @param {string} roadmapContent - Raw ROADMAP.md content
|
|
83
|
+
* @returns {Array<{ number: string, name: string, dependsOn: string[], completed: boolean }>}
|
|
84
|
+
*/
|
|
85
|
+
function parseRoadmapPhases(roadmapContent) {
|
|
86
|
+
const lines = roadmapContent.split('\n');
|
|
87
|
+
const phases = [];
|
|
88
|
+
let currentPhase = null;
|
|
89
|
+
|
|
90
|
+
const phaseHeaderRe = /^### Phase (v[\d.]+-\d+):\s*(.+)/;
|
|
91
|
+
const dependsOnRe = /^\*\*Depends on\*\*:\s*(.+)/;
|
|
92
|
+
const checkboxRe = /^- \[(x| )\].*(?:Phase )?(v[\d.]+-\d+)/;
|
|
93
|
+
|
|
94
|
+
const completedFromCheckboxes = new Set();
|
|
95
|
+
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const headerMatch = phaseHeaderRe.exec(line);
|
|
98
|
+
if (headerMatch) {
|
|
99
|
+
if (currentPhase) phases.push(currentPhase);
|
|
100
|
+
currentPhase = {
|
|
101
|
+
number: headerMatch[1],
|
|
102
|
+
name: headerMatch[2].trim(),
|
|
103
|
+
dependsOn: [],
|
|
104
|
+
completed: false,
|
|
105
|
+
};
|
|
106
|
+
if (line.includes('completed')) currentPhase.completed = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (currentPhase) {
|
|
111
|
+
const depsMatch = dependsOnRe.exec(line);
|
|
112
|
+
if (depsMatch) {
|
|
113
|
+
const depsStr = depsMatch[1];
|
|
114
|
+
const phaseRefs = depsStr.match(/v[\d.]+-\d+/g);
|
|
115
|
+
if (phaseRefs) {
|
|
116
|
+
currentPhase.dependsOn = phaseRefs;
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cbMatch = checkboxRe.exec(line);
|
|
123
|
+
if (cbMatch && cbMatch[1] === 'x') {
|
|
124
|
+
completedFromCheckboxes.add(cbMatch[2]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (currentPhase) phases.push(currentPhase);
|
|
128
|
+
|
|
129
|
+
// Mark completed from checkboxes and content patterns
|
|
130
|
+
for (const phase of phases) {
|
|
131
|
+
if (completedFromCheckboxes.has(phase.number)) {
|
|
132
|
+
phase.completed = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Second pass: detect "(completed YYYY-MM-DD)" near phase headers
|
|
137
|
+
let currentPhaseIdx = -1;
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
const headerMatch = phaseHeaderRe.exec(line);
|
|
140
|
+
if (headerMatch) {
|
|
141
|
+
currentPhaseIdx = phases.findIndex(function(p) { return p.number === headerMatch[1]; });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (currentPhaseIdx >= 0 && /completed\s+\d{4}-\d{2}-\d{2}/.test(line)) {
|
|
145
|
+
phases[currentPhaseIdx].completed = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return phases;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* buildRoadmapDot(phases) — generates a Petri net DOT from parsed phases.
|
|
154
|
+
* @param {Array<{ number: string, name: string, dependsOn: string[], completed: boolean }>} phases
|
|
155
|
+
* @returns {string} DOT source
|
|
156
|
+
*/
|
|
157
|
+
function buildRoadmapDot(phases) {
|
|
158
|
+
const lines = [
|
|
159
|
+
'digraph roadmap_petri_net {',
|
|
160
|
+
' rankdir=LR;',
|
|
161
|
+
' label="QGSD Roadmap Petri Net (' + phases.length + ' phases)";',
|
|
162
|
+
' node [fontname="Helvetica"];',
|
|
163
|
+
'',
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
if (phases.length === 0) {
|
|
167
|
+
lines.push('}');
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const phaseNumbers = new Set(phases.map(function(p) { return p.number; }));
|
|
172
|
+
const hasDependents = new Set();
|
|
173
|
+
for (const phase of phases) {
|
|
174
|
+
for (const dep of phase.dependsOn) {
|
|
175
|
+
if (phaseNumbers.has(dep)) hasDependents.add(dep);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
lines.push(' // Places (circles)');
|
|
180
|
+
lines.push(' node [shape=circle, fixedsize=true, width=0.8];');
|
|
181
|
+
|
|
182
|
+
const sourcesExist = phases.some(function(p) { return p.dependsOn.length === 0; });
|
|
183
|
+
if (sourcesExist) {
|
|
184
|
+
lines.push(' p_start [label="start"];');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const phase of phases) {
|
|
188
|
+
for (const dep of phase.dependsOn) {
|
|
189
|
+
if (!phaseNumbers.has(dep)) continue;
|
|
190
|
+
const placeId = 'p_' + dep.replace(/[.-]/g, '_') + '__' + phase.number.replace(/[.-]/g, '_');
|
|
191
|
+
lines.push(' ' + placeId + ' [label=""];');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const sinksExist = phases.some(function(p) { return !hasDependents.has(p.number); });
|
|
196
|
+
if (sinksExist) {
|
|
197
|
+
lines.push(' p_done [label="done"];');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lines.push('');
|
|
201
|
+
lines.push(' // Transitions (rectangles) -- phases');
|
|
202
|
+
for (const phase of phases) {
|
|
203
|
+
const nodeId = 't_' + phase.number.replace(/[.-]/g, '_');
|
|
204
|
+
const label = phase.number + '\\n' + phase.name.substring(0, 30);
|
|
205
|
+
if (phase.completed) {
|
|
206
|
+
lines.push(' ' + nodeId + ' [shape=rect, height=0.5, width=2.0, style=filled, fillcolor="#4CAF50", fontcolor=white, label="' + label + '"];');
|
|
207
|
+
} else {
|
|
208
|
+
lines.push(' ' + nodeId + ' [shape=rect, height=0.5, width=2.0, style=filled, fillcolor=black, fontcolor=white, label="' + label + '"];');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push(' // Arcs');
|
|
214
|
+
for (const phase of phases) {
|
|
215
|
+
const nodeId = 't_' + phase.number.replace(/[.-]/g, '_');
|
|
216
|
+
|
|
217
|
+
if (phase.dependsOn.length === 0 && sourcesExist) {
|
|
218
|
+
lines.push(' p_start -> ' + nodeId + ';');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const dep of phase.dependsOn) {
|
|
222
|
+
if (!phaseNumbers.has(dep)) continue;
|
|
223
|
+
const placeId = 'p_' + dep.replace(/[.-]/g, '_') + '__' + phase.number.replace(/[.-]/g, '_');
|
|
224
|
+
lines.push(' ' + placeId + ' -> ' + nodeId + ';');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const downstream of phases) {
|
|
228
|
+
if (downstream.dependsOn.includes(phase.number)) {
|
|
229
|
+
const placeId = 'p_' + phase.number.replace(/[.-]/g, '_') + '__' + downstream.number.replace(/[.-]/g, '_');
|
|
230
|
+
lines.push(' ' + nodeId + ' -> ' + placeId + ';');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!hasDependents.has(phase.number) && sinksExist) {
|
|
235
|
+
lines.push(' ' + nodeId + ' -> p_done;');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
lines.push('}');
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* computeCriticalPath(phases) — finds the longest path through the phase DAG.
|
|
245
|
+
* @param {Array<{ number: string, name: string, dependsOn: string[], completed: boolean }>} phases
|
|
246
|
+
* @returns {{ path: string[], length: number }}
|
|
247
|
+
*/
|
|
248
|
+
function computeCriticalPath(phases) {
|
|
249
|
+
if (phases.length === 0) return { path: [], length: 0 };
|
|
250
|
+
|
|
251
|
+
const phaseMap = new Map();
|
|
252
|
+
for (const p of phases) phaseMap.set(p.number, p);
|
|
253
|
+
|
|
254
|
+
// Kahn's algorithm for topological sort + longest path DP
|
|
255
|
+
const inDegree = new Map();
|
|
256
|
+
const adj = new Map();
|
|
257
|
+
|
|
258
|
+
for (const p of phases) {
|
|
259
|
+
if (!inDegree.has(p.number)) inDegree.set(p.number, 0);
|
|
260
|
+
if (!adj.has(p.number)) adj.set(p.number, []);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const p of phases) {
|
|
264
|
+
for (const dep of p.dependsOn) {
|
|
265
|
+
if (!phaseMap.has(dep)) continue;
|
|
266
|
+
if (!adj.has(dep)) adj.set(dep, []);
|
|
267
|
+
adj.get(dep).push(p.number);
|
|
268
|
+
inDegree.set(p.number, (inDegree.get(p.number) || 0) + 1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const queue = [];
|
|
273
|
+
for (const [node, deg] of inDegree) {
|
|
274
|
+
if (deg === 0) queue.push(node);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const dist = new Map();
|
|
278
|
+
const pred = new Map();
|
|
279
|
+
for (const p of phases) {
|
|
280
|
+
dist.set(p.number, 1);
|
|
281
|
+
pred.set(p.number, null);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
while (queue.length > 0) {
|
|
285
|
+
const node = queue.shift();
|
|
286
|
+
for (const next of (adj.get(node) || [])) {
|
|
287
|
+
if (dist.get(node) + 1 > dist.get(next)) {
|
|
288
|
+
dist.set(next, dist.get(node) + 1);
|
|
289
|
+
pred.set(next, node);
|
|
290
|
+
}
|
|
291
|
+
inDegree.set(next, inDegree.get(next) - 1);
|
|
292
|
+
if (inDegree.get(next) === 0) queue.push(next);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let maxDist = 0;
|
|
297
|
+
let endNode = null;
|
|
298
|
+
for (const [node, d] of dist) {
|
|
299
|
+
if (d > maxDist) {
|
|
300
|
+
maxDist = d;
|
|
301
|
+
endNode = node;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const criticalPath = [];
|
|
306
|
+
let current = endNode;
|
|
307
|
+
while (current !== null) {
|
|
308
|
+
criticalPath.unshift(current);
|
|
309
|
+
current = pred.get(current);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { path: criticalPath, length: criticalPath.length };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Export pure functions for unit testing
|
|
316
|
+
module.exports._pure = { buildDot, buildRoadmapDot, computeCriticalPath, parseRoadmapPhases };
|
|
317
|
+
|
|
318
|
+
// Guard against running main logic when required as a module (test imports)
|
|
319
|
+
if (require.main === module) {
|
|
320
|
+
const isRoadmap = process.argv.includes('--roadmap');
|
|
321
|
+
|
|
322
|
+
if (isRoadmap) {
|
|
323
|
+
// ── Roadmap Petri Net mode ──────────────────────────────────────────────
|
|
324
|
+
const roadmapPath = path.join(process.cwd(), '.planning', 'ROADMAP.md');
|
|
325
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
326
|
+
process.stderr.write('[generate-petri-net] ROADMAP.md not found at: ' + roadmapPath + '\n');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const content = fs.readFileSync(roadmapPath, 'utf8');
|
|
331
|
+
const phases = parseRoadmapPhases(content);
|
|
332
|
+
const dotContent = buildRoadmapDot(phases);
|
|
333
|
+
const criticalPath = computeCriticalPath(phases);
|
|
334
|
+
|
|
335
|
+
const outDir = path.join(process.cwd(), '.planning', 'formal', 'petri');
|
|
336
|
+
const dotPath = path.join(outDir, 'roadmap-petri-net.dot');
|
|
337
|
+
const svgPath = path.join(outDir, 'roadmap-petri-net.svg');
|
|
338
|
+
|
|
339
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
340
|
+
fs.writeFileSync(dotPath, dotContent);
|
|
341
|
+
process.stdout.write('[generate-petri-net] Roadmap DOT written to: ' + dotPath + '\n');
|
|
342
|
+
process.stdout.write('[generate-petri-net] Phases: ' + phases.length + '\n');
|
|
343
|
+
process.stdout.write('[generate-petri-net] Critical path (' + criticalPath.length + '): ' + criticalPath.path.join(' -> ') + '\n');
|
|
344
|
+
|
|
345
|
+
// Render SVG
|
|
346
|
+
(async () => {
|
|
347
|
+
let Graphviz;
|
|
348
|
+
try {
|
|
349
|
+
({ Graphviz } = await import('@hpcc-js/wasm-graphviz'));
|
|
350
|
+
} catch (_e) {
|
|
351
|
+
process.stderr.write('[generate-petri-net] @hpcc-js/wasm-graphviz not installed -- SVG skipped.\n');
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const graphviz = await Graphviz.load();
|
|
356
|
+
const svg = graphviz.dot(dotContent);
|
|
357
|
+
fs.writeFileSync(svgPath, svg);
|
|
358
|
+
process.stdout.write('[generate-petri-net] Roadmap SVG written to: ' + svgPath + '\n');
|
|
359
|
+
} catch (renderErr) {
|
|
360
|
+
process.stderr.write('[generate-petri-net] SVG render failed: ' + renderErr.message + '\n');
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
})();
|
|
364
|
+
} else {
|
|
365
|
+
// ── Quorum Petri Net mode (existing) ────────────────────────────────────
|
|
366
|
+
const dotContent = buildDot(QUORUM_SLOTS, effectiveMinQuorum);
|
|
367
|
+
const outDir = path.join(process.cwd(), '.planning', 'formal', 'petri');
|
|
368
|
+
const dotPath = path.join(outDir, 'quorum-petri-net.dot');
|
|
369
|
+
const svgPath = path.join(outDir, 'quorum-petri-net.svg');
|
|
370
|
+
|
|
371
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
372
|
+
fs.writeFileSync(dotPath, dotContent);
|
|
373
|
+
process.stdout.write('[generate-petri-net] DOT written to: ' + dotPath + '\n');
|
|
374
|
+
|
|
375
|
+
(async () => {
|
|
376
|
+
let Graphviz;
|
|
377
|
+
try {
|
|
378
|
+
({ Graphviz } = await import('@hpcc-js/wasm-graphviz'));
|
|
379
|
+
} catch (importErr) {
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
'[generate-petri-net] @hpcc-js/wasm-graphviz not installed.\n' +
|
|
382
|
+
'[generate-petri-net] Run: npm install --save-dev @hpcc-js/wasm-graphviz\n'
|
|
383
|
+
);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const graphviz = await Graphviz.load();
|
|
388
|
+
const svg = graphviz.dot(dotContent);
|
|
389
|
+
fs.writeFileSync(svgPath, svg);
|
|
390
|
+
process.stdout.write('[generate-petri-net] SVG written to: ' + svgPath + '\n');
|
|
391
|
+
} catch (renderErr) {
|
|
392
|
+
process.stderr.write('[generate-petri-net] SVG render failed: ' + renderErr.message + '\n');
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/generate-phase-spec.cjs
|
|
4
|
+
// SPEC-04: Reads must_haves: truths: from *-PLAN.md YAML frontmatter and translates
|
|
5
|
+
// them into per-phase TLA+ scratch spec with INVARIANT/PROPERTY stubs.
|
|
6
|
+
//
|
|
7
|
+
// Data source: YAML frontmatter in *-PLAN.md files (NOT task-envelope.json — truths
|
|
8
|
+
// are empty in task-envelope.json at planning time; plan frontmatter is the correct source).
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node bin/generate-phase-spec.cjs .planning/phases/v0.21-04-spec-completeness/
|
|
12
|
+
// node bin/generate-phase-spec.cjs .planning/phases/v0.21-04-spec-completeness/v0.21-04-01-PLAN.md
|
|
13
|
+
// node bin/generate-phase-spec.cjs <phase-dir-or-plan-file> --dry-run
|
|
14
|
+
//
|
|
15
|
+
// Output: .planning/formal/tla/scratch/<phase>-<timestamp>.tla
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const ROOT = path.join(__dirname, '..');
|
|
21
|
+
const SCRATCH_DIR = path.join(ROOT, '.planning', 'formal', 'tla', 'scratch');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse YAML frontmatter from a PLAN.md file.
|
|
25
|
+
* Returns the frontmatter object (parsed manually — no yaml dependency).
|
|
26
|
+
* Frontmatter is between the first two '---' lines.
|
|
27
|
+
*
|
|
28
|
+
* Extracts:
|
|
29
|
+
* - phase: string
|
|
30
|
+
* - truths: string[] from must_haves.truths
|
|
31
|
+
*/
|
|
32
|
+
function parsePlanFrontmatter(content) {
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
if (!lines[0] || lines[0].trim() !== '---') return { truths: [] };
|
|
35
|
+
const endIdx = lines.slice(1).findIndex(l => l.trim() === '---');
|
|
36
|
+
if (endIdx === -1) return { truths: [] };
|
|
37
|
+
const fmLines = lines.slice(1, endIdx + 1);
|
|
38
|
+
|
|
39
|
+
// Minimal YAML parser: extracts must_haves.truths array and phase field only.
|
|
40
|
+
// Looks for 'must_haves:' block, then 'truths:' key, then collects '- "..."' or "- '...'" entries.
|
|
41
|
+
const result = {};
|
|
42
|
+
let inMustHaves = false;
|
|
43
|
+
let inTruths = false;
|
|
44
|
+
const truths = [];
|
|
45
|
+
|
|
46
|
+
for (const line of fmLines) {
|
|
47
|
+
// Detect phase field at top-level
|
|
48
|
+
const phaseMatch = line.match(/^phase:\s*(.+)/);
|
|
49
|
+
if (phaseMatch) {
|
|
50
|
+
result.phase = phaseMatch[1].replace(/['"]/g, '').trim();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (/^must_haves:/.test(line)) {
|
|
55
|
+
inMustHaves = true;
|
|
56
|
+
inTruths = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (inMustHaves && /^\s{2}truths:/.test(line)) {
|
|
61
|
+
inTruths = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (inMustHaves && inTruths) {
|
|
66
|
+
// Match list items: " - "text"" or " - 'text'" or " - text"
|
|
67
|
+
const itemMatch = line.match(/^\s{4}-\s+"?'?(.+?)'?"?\s*$/);
|
|
68
|
+
if (itemMatch) {
|
|
69
|
+
// Clean up: strip surrounding quotes if present
|
|
70
|
+
let truth = itemMatch[1].trim();
|
|
71
|
+
// Remove leading/trailing quotes that may remain
|
|
72
|
+
truth = truth.replace(/^["']|["']$/g, '');
|
|
73
|
+
if (truth.length > 0) truths.push(truth);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Stop collecting truths when indentation drops below 4 spaces (non-list, non-empty)
|
|
77
|
+
if (!/^\s{4}/.test(line) && line.trim() !== '') {
|
|
78
|
+
inTruths = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Detect top-level key change inside must_haves (drops indentation)
|
|
83
|
+
if (inMustHaves && !inTruths && !/^\s{2,}/.test(line) && line.trim() !== '') {
|
|
84
|
+
inMustHaves = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
result.truths = truths;
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Collect all *-PLAN.md files from a directory or single file path.
|
|
94
|
+
* Returns sorted array of absolute paths.
|
|
95
|
+
*/
|
|
96
|
+
function collectPlanFiles(inputPath) {
|
|
97
|
+
const resolved = path.resolve(inputPath);
|
|
98
|
+
const stat = fs.statSync(resolved);
|
|
99
|
+
if (stat.isFile()) return [resolved];
|
|
100
|
+
// Directory: find *-PLAN.md files
|
|
101
|
+
return fs.readdirSync(resolved)
|
|
102
|
+
.filter(f => f.endsWith('-PLAN.md'))
|
|
103
|
+
.sort()
|
|
104
|
+
.map(f => path.join(resolved, f));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Classify a truth string as safety (INVARIANT) or liveness (PROPERTY).
|
|
109
|
+
*
|
|
110
|
+
* Liveness keywords: eventually, deadline, async, progress, future, after, until, leads-to
|
|
111
|
+
* Safety keywords: everything else (threshold, depth, count, max, min, never, always, etc.)
|
|
112
|
+
*
|
|
113
|
+
* @param {string} truth
|
|
114
|
+
* @returns {'PROPERTY' | 'INVARIANT'}
|
|
115
|
+
*/
|
|
116
|
+
function classifyTruth(truth) {
|
|
117
|
+
const lower = truth.toLowerCase();
|
|
118
|
+
const livenessKeywords = [
|
|
119
|
+
'eventually',
|
|
120
|
+
'deadline',
|
|
121
|
+
'async',
|
|
122
|
+
'progress',
|
|
123
|
+
'future',
|
|
124
|
+
'leads-to',
|
|
125
|
+
'after',
|
|
126
|
+
'until',
|
|
127
|
+
];
|
|
128
|
+
const isLiveness = livenessKeywords.some(kw => lower.includes(kw));
|
|
129
|
+
return isLiveness ? 'PROPERTY' : 'INVARIANT';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate TLA+ module content from collected truths.
|
|
134
|
+
*
|
|
135
|
+
* @param {{ phase: string, truths: string[] }} options
|
|
136
|
+
* @returns {{ moduleName: string, spec: string, truthCount: number }}
|
|
137
|
+
*/
|
|
138
|
+
function generatePhaseSpec({ phase, truths }) {
|
|
139
|
+
const allTruths = truths || [];
|
|
140
|
+
|
|
141
|
+
// TLA+ module name: sanitize phase string (e.g. v0.21-04 → Phasev0_21_04Spec)
|
|
142
|
+
const moduleName = 'Phase' + (phase || 'unknown').replace(/[^a-zA-Z0-9]/g, '_') + 'Spec';
|
|
143
|
+
const timestamp = new Date().toISOString();
|
|
144
|
+
|
|
145
|
+
let spec = `---- MODULE ${moduleName} ----
|
|
146
|
+
(* Source: must_haves: truths: from *-PLAN.md YAML frontmatter *)
|
|
147
|
+
(* Generated by bin/generate-phase-spec.cjs — SPEC-04 *)
|
|
148
|
+
(* Phase: ${phase || 'unknown'} *)
|
|
149
|
+
(* Generated: ${timestamp} *)
|
|
150
|
+
(* Purpose: Verify proposed state machine changes before quorum approval *)
|
|
151
|
+
(* PLACEHOLDER properties — developer fills in formal logic before TLC run *)
|
|
152
|
+
|
|
153
|
+
EXTENDS Naturals, FiniteSets, TLC
|
|
154
|
+
|
|
155
|
+
VARIABLES state, counter
|
|
156
|
+
|
|
157
|
+
TypeOK == state \\in {"INIT", "RUNNING", "DONE"}
|
|
158
|
+
|
|
159
|
+
Init == state = "INIT" /\\ counter = 0
|
|
160
|
+
|
|
161
|
+
Next ==
|
|
162
|
+
\\/ /\\ state = "INIT" /\\ state' = "RUNNING" /\\ counter' = counter + 1
|
|
163
|
+
\\/ /\\ state = "RUNNING" /\\ state' = "DONE" /\\ counter' = counter
|
|
164
|
+
\\/ /\\ state = "DONE" /\\ UNCHANGED <<state, counter>>
|
|
165
|
+
|
|
166
|
+
Spec == Init /\\ [][Next]_<<state, counter>>
|
|
167
|
+
|
|
168
|
+
\\* ── Requirements as Properties ──────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
if (allTruths.length === 0) {
|
|
173
|
+
spec += `\\* No must_haves: truths: found in *-PLAN.md frontmatter
|
|
174
|
+
\\* Add truths to see them translated to INVARIANT/PROPERTY stubs
|
|
175
|
+
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
allTruths.forEach((truth, idx) => {
|
|
180
|
+
const kind = classifyTruth(truth);
|
|
181
|
+
const propName = `Req${String(idx + 1).padStart(2, '0')}`;
|
|
182
|
+
// Escape backslashes and truncate label for TLA+ comment
|
|
183
|
+
const label = truth.substring(0, 100).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
184
|
+
spec += `${kind} ${propName} ==
|
|
185
|
+
\\* "${label}${truth.length > 100 ? '...' : ''}"
|
|
186
|
+
TRUE \\* PLACEHOLDER: replace TRUE with formal TLA+ expression
|
|
187
|
+
|
|
188
|
+
`;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
spec += `====`;
|
|
192
|
+
return { moduleName, spec, truthCount: allTruths.length };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── CLI entrypoint ────────────────────────────────────────────────────────────
|
|
196
|
+
if (require.main === module) {
|
|
197
|
+
const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
|
|
198
|
+
const flags = process.argv.slice(2).filter(a => a.startsWith('--'));
|
|
199
|
+
const isDryRun = flags.includes('--dry-run');
|
|
200
|
+
|
|
201
|
+
if (args.length === 0) {
|
|
202
|
+
process.stderr.write('[generate-phase-spec] Usage: node bin/generate-phase-spec.cjs <phase-dir-or-PLAN.md> [--dry-run]\n');
|
|
203
|
+
process.stderr.write('[generate-phase-spec] Reads must_haves: truths: from *-PLAN.md YAML frontmatter (NOT task-envelope.json)\n');
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const inputPath = path.resolve(args[0]);
|
|
208
|
+
if (!fs.existsSync(inputPath)) {
|
|
209
|
+
process.stderr.write('[generate-phase-spec] Error: path not found: ' + inputPath + '\n');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const planFiles = collectPlanFiles(inputPath);
|
|
214
|
+
if (planFiles.length === 0) {
|
|
215
|
+
process.stderr.write('[generate-phase-spec] Error: no *-PLAN.md files found in ' + inputPath + '\n');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Collect truths from all plan files
|
|
220
|
+
let phase = 'unknown';
|
|
221
|
+
const allTruths = [];
|
|
222
|
+
for (const planFile of planFiles) {
|
|
223
|
+
const content = fs.readFileSync(planFile, 'utf8');
|
|
224
|
+
const fm = parsePlanFrontmatter(content);
|
|
225
|
+
if (fm.phase) phase = fm.phase;
|
|
226
|
+
if (fm.truths && fm.truths.length > 0) allTruths.push(...fm.truths);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { moduleName, spec, truthCount } = generatePhaseSpec({ phase, truths: allTruths });
|
|
230
|
+
const ts = Date.now();
|
|
231
|
+
const outName = phase.replace(/[^a-zA-Z0-9-]/g, '_') + '-' + ts + '.tla';
|
|
232
|
+
const outPath = path.join(SCRATCH_DIR, outName);
|
|
233
|
+
|
|
234
|
+
if (isDryRun) {
|
|
235
|
+
process.stdout.write('[generate-phase-spec] DRY-RUN: would write ' + outPath + '\n');
|
|
236
|
+
process.stdout.write('[generate-phase-spec] Plan files: ' + planFiles.join(', ') + '\n');
|
|
237
|
+
process.stdout.write('[generate-phase-spec] Truths collected: ' + truthCount + '\n');
|
|
238
|
+
process.stdout.write(spec + '\n');
|
|
239
|
+
} else {
|
|
240
|
+
fs.mkdirSync(SCRATCH_DIR, { recursive: true });
|
|
241
|
+
fs.writeFileSync(outPath, spec, 'utf8');
|
|
242
|
+
process.stdout.write(
|
|
243
|
+
'[generate-phase-spec] Written: ' + outPath +
|
|
244
|
+
' (' + truthCount + ' truths from ' + planFiles.length + ' plan files → INVARIANT/PROPERTY stubs)\n'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = { generatePhaseSpec, classifyTruth, parsePlanFrontmatter };
|