@nforma.ai/nforma 0.2.1 → 0.28.0
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/README.md +2 -2
- package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
- package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
- package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
- package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
- package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
- package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
- package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
- package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
- package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
- package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
- package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
- package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
- package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
- package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
- package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
- package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
- package/bin/accept-debug-invariant.cjs +2 -2
- package/bin/account-manager.cjs +10 -10
- package/bin/aggregate-requirements.cjs +1 -1
- package/bin/analyze-assumptions.cjs +3 -3
- package/bin/analyze-state-space.cjs +14 -14
- package/bin/assumption-register.cjs +146 -0
- package/bin/attribute-trace-divergence.cjs +1 -1
- package/bin/auth-drivers/gh-cli.cjs +1 -1
- package/bin/auth-drivers/pool.cjs +1 -1
- package/bin/autoClosePtoF.cjs +3 -3
- package/bin/budget-tracker.cjs +77 -0
- package/bin/build-layer-manifest.cjs +153 -0
- package/bin/call-quorum-slot.cjs +3 -3
- package/bin/ccr-secure-config.cjs +5 -5
- package/bin/check-bundled-sdks.cjs +1 -1
- package/bin/check-mcp-health.cjs +1 -1
- package/bin/check-provider-health.cjs +6 -6
- package/bin/check-spec-sync.cjs +26 -26
- package/bin/check-trace-schema-drift.cjs +5 -5
- package/bin/conformance-schema.cjs +2 -2
- package/bin/cross-layer-dashboard.cjs +297 -0
- package/bin/design-impact.cjs +377 -0
- package/bin/detect-coverage-gaps.cjs +7 -7
- package/bin/failure-mode-catalog.cjs +227 -0
- package/bin/failure-taxonomy.cjs +177 -0
- package/bin/formal-scope-scan.cjs +179 -0
- package/bin/gate-a-grounding.cjs +334 -0
- package/bin/gate-b-abstraction.cjs +243 -0
- package/bin/gate-c-validation.cjs +166 -0
- package/bin/generate-formal-specs.cjs +17 -17
- package/bin/generate-petri-net.cjs +3 -3
- package/bin/generate-tla-cfg.cjs +5 -5
- package/bin/git-heatmap.cjs +571 -0
- package/bin/harness-diagnostic.cjs +326 -0
- package/bin/hazard-model.cjs +261 -0
- package/bin/install-formal-tools.cjs +1 -1
- package/bin/install.js +184 -139
- package/bin/instrumentation-map.cjs +178 -0
- package/bin/invariant-catalog.cjs +437 -0
- package/bin/issue-classifier.cjs +2 -2
- package/bin/load-baseline-requirements.cjs +4 -4
- package/bin/manage-agents-core.cjs +32 -32
- package/bin/migrate-to-slots.cjs +39 -39
- package/bin/mismatch-register.cjs +217 -0
- package/bin/nForma.cjs +176 -81
- package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
- package/bin/observe-config.cjs +8 -0
- package/bin/observe-debt-writer.cjs +1 -1
- package/bin/observe-handler-deps.cjs +356 -0
- package/bin/observe-handler-grafana.cjs +2 -17
- package/bin/observe-handler-internal.cjs +5 -5
- package/bin/observe-handler-logstash.cjs +2 -17
- package/bin/observe-handler-prometheus.cjs +2 -17
- package/bin/observe-handler-upstream.cjs +251 -0
- package/bin/observe-handlers.cjs +12 -33
- package/bin/observe-render.cjs +68 -22
- package/bin/observe-utils.cjs +37 -0
- package/bin/observed-fsm.cjs +324 -0
- package/bin/planning-paths.cjs +6 -0
- package/bin/polyrepo.cjs +1 -1
- package/bin/probe-quorum-slots.cjs +1 -1
- package/bin/promote-gate-maturity.cjs +274 -0
- package/bin/promote-model.cjs +1 -1
- package/bin/propose-debug-invariants.cjs +1 -1
- package/bin/quorum-cache.cjs +144 -0
- package/bin/quorum-consensus-gate.cjs +1 -1
- package/bin/quorum-slot-dispatch.cjs +6 -6
- package/bin/requirements-core.cjs +1 -1
- package/bin/review-mcp-logs.cjs +1 -1
- package/bin/risk-heatmap.cjs +151 -0
- package/bin/run-account-manager-tlc.cjs +4 -4
- package/bin/run-account-pool-alloy.cjs +2 -2
- package/bin/run-alloy.cjs +2 -2
- package/bin/run-audit-alloy.cjs +2 -2
- package/bin/run-breaker-tlc.cjs +3 -3
- package/bin/run-formal-check.cjs +9 -9
- package/bin/run-formal-verify.cjs +30 -9
- package/bin/run-installer-alloy.cjs +2 -2
- package/bin/run-oscillation-tlc.cjs +4 -4
- package/bin/run-phase-tlc.cjs +1 -1
- package/bin/run-protocol-tlc.cjs +4 -4
- package/bin/run-quorum-composition-alloy.cjs +2 -2
- package/bin/run-sensitivity-sweep.cjs +2 -2
- package/bin/run-stop-hook-tlc.cjs +3 -3
- package/bin/run-tlc.cjs +21 -21
- package/bin/run-transcript-alloy.cjs +2 -2
- package/bin/secrets.cjs +5 -5
- package/bin/security-sweep.cjs +238 -0
- package/bin/sensitivity-report.cjs +3 -3
- package/bin/set-secret.cjs +5 -5
- package/bin/setup-telemetry-cron.sh +3 -3
- package/bin/stall-detector.cjs +126 -0
- package/bin/state-candidates.cjs +206 -0
- package/bin/sync-baseline-requirements.cjs +1 -1
- package/bin/telemetry-collector.cjs +1 -1
- package/bin/test-changed.cjs +111 -0
- package/bin/test-recipe-gen.cjs +250 -0
- package/bin/trace-corpus-stats.cjs +211 -0
- package/bin/unified-mcp-server.mjs +3 -3
- package/bin/update-scoreboard.cjs +1 -1
- package/bin/validate-memory.cjs +2 -2
- package/bin/validate-traces.cjs +10 -10
- package/bin/verify-quorum-health.cjs +66 -5
- package/bin/xstate-to-tla.cjs +4 -4
- package/bin/xstate-trace-walker.cjs +3 -3
- package/commands/{qgsd → nf}/add-phase.md +3 -3
- package/commands/{qgsd → nf}/add-requirement.md +3 -3
- package/commands/{qgsd → nf}/add-todo.md +3 -3
- package/commands/{qgsd → nf}/audit-milestone.md +4 -4
- package/commands/{qgsd → nf}/check-todos.md +3 -3
- package/commands/{qgsd → nf}/cleanup.md +3 -3
- package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
- package/commands/{qgsd → nf}/complete-milestone.md +9 -9
- package/commands/{qgsd → nf}/debug.md +9 -9
- package/commands/{qgsd → nf}/discuss-phase.md +3 -3
- package/commands/{qgsd → nf}/execute-phase.md +15 -15
- package/commands/{qgsd → nf}/fix-tests.md +3 -3
- package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
- package/commands/{qgsd → nf}/health.md +3 -3
- package/commands/{qgsd → nf}/help.md +3 -3
- package/commands/{qgsd → nf}/insert-phase.md +3 -3
- package/commands/nf/join-discord.md +18 -0
- package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
- package/commands/{qgsd → nf}/map-codebase.md +7 -7
- package/commands/{qgsd → nf}/map-requirements.md +3 -3
- package/commands/{qgsd → nf}/mcp-restart.md +3 -3
- package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
- package/commands/{qgsd → nf}/mcp-setup.md +63 -63
- package/commands/{qgsd → nf}/mcp-status.md +3 -3
- package/commands/{qgsd → nf}/mcp-update.md +7 -7
- package/commands/{qgsd → nf}/new-milestone.md +8 -8
- package/commands/{qgsd → nf}/new-project.md +8 -8
- package/commands/{qgsd → nf}/observe.md +49 -16
- package/commands/{qgsd → nf}/pause-work.md +3 -3
- package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
- package/commands/{qgsd → nf}/plan-phase.md +6 -6
- package/commands/{qgsd → nf}/polyrepo.md +2 -2
- package/commands/{qgsd → nf}/progress.md +3 -3
- package/commands/{qgsd → nf}/queue.md +2 -2
- package/commands/{qgsd → nf}/quick.md +8 -8
- package/commands/{qgsd → nf}/quorum-test.md +10 -10
- package/commands/{qgsd → nf}/quorum.md +40 -40
- package/commands/{qgsd → nf}/reapply-patches.md +2 -2
- package/commands/{qgsd → nf}/remove-phase.md +3 -3
- package/commands/{qgsd → nf}/research-phase.md +12 -12
- package/commands/{qgsd → nf}/resume-work.md +3 -3
- package/commands/nf/review-requirements.md +31 -0
- package/commands/{qgsd → nf}/set-profile.md +3 -3
- package/commands/{qgsd → nf}/settings.md +6 -6
- package/commands/{qgsd → nf}/solve.md +35 -35
- package/commands/{qgsd → nf}/sync-baselines.md +4 -4
- package/commands/{qgsd → nf}/triage.md +10 -10
- package/commands/{qgsd → nf}/update.md +3 -3
- package/commands/{qgsd → nf}/verify-work.md +5 -5
- package/hooks/dist/config-loader.js +188 -32
- package/hooks/dist/conformance-schema.cjs +2 -2
- package/hooks/dist/gsd-context-monitor.js +118 -13
- package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
- package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
- package/hooks/dist/nf-circuit-breaker.test.js +1002 -0
- package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/nf-precompact.test.js +227 -0
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-prompt.test.js +698 -0
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/nf-session-start.test.js +354 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/nf-slot-correlator.test.js +85 -0
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/nf-spec-regen.test.js +73 -0
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/nf-statusline.test.js +157 -0
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/nf-stop.test.js +1388 -0
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/nf-token-collector.test.js +262 -0
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +4 -4
- package/scripts/build-hooks.js +13 -6
- package/scripts/secret-audit.sh +1 -1
- package/scripts/verify-hooks-sync.cjs +90 -0
- package/templates/{qgsd.json → nf.json} +4 -4
- package/commands/qgsd/join-discord.md +0 -18
- package/hooks/dist/qgsd-session-start.js +0 -122
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* observed-fsm.cjs — Derives an observed-behavior FSM from traces using
|
|
6
|
+
* dual-mode replay (per-event isolation + per-session chains).
|
|
7
|
+
*
|
|
8
|
+
* Requirements: SEM-04
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node bin/observed-fsm.cjs # print summary to stdout
|
|
12
|
+
* node bin/observed-fsm.cjs --json # print full FSM JSON to stdout
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
|
|
19
|
+
const FORMAL = path.join(ROOT, '.planning', 'formal');
|
|
20
|
+
const OUT_DIR = path.join(FORMAL, 'semantics');
|
|
21
|
+
const OUT_FILE = path.join(OUT_DIR, 'observed-fsm.json');
|
|
22
|
+
|
|
23
|
+
const JSON_FLAG = process.argv.includes('--json');
|
|
24
|
+
|
|
25
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function stateToString(value) {
|
|
28
|
+
if (typeof value === 'string') return value;
|
|
29
|
+
if (typeof value === 'object' && value !== null) return JSON.stringify(value);
|
|
30
|
+
return String(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Model extraction ────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function extractModelTransitions() {
|
|
36
|
+
const machinePath = (() => {
|
|
37
|
+
const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'nf-workflow.machine.js');
|
|
38
|
+
const installDist = path.join(__dirname, 'dist', 'machines', 'nf-workflow.machine.js');
|
|
39
|
+
return fs.existsSync(repoDist) ? repoDist : installDist;
|
|
40
|
+
})();
|
|
41
|
+
const { nfWorkflowMachine } = require(machinePath);
|
|
42
|
+
|
|
43
|
+
const config = nfWorkflowMachine.config;
|
|
44
|
+
const transitions = [];
|
|
45
|
+
|
|
46
|
+
for (const [stateName, stateDef] of Object.entries(config.states)) {
|
|
47
|
+
const on = stateDef.on || {};
|
|
48
|
+
for (const [eventName, targets] of Object.entries(on)) {
|
|
49
|
+
if (Array.isArray(targets)) {
|
|
50
|
+
for (const t of targets) {
|
|
51
|
+
if (t.target) transitions.push({ from: stateName, event: eventName, to: t.target });
|
|
52
|
+
}
|
|
53
|
+
} else if (typeof targets === 'string') {
|
|
54
|
+
transitions.push({ from: stateName, event: eventName, to: targets });
|
|
55
|
+
} else if (targets && targets.target) {
|
|
56
|
+
transitions.push({ from: stateName, event: eventName, to: targets.target });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { states: Object.keys(config.states), transitions };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Mode A: Per-event isolation ─────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function replayPerEvent(conformanceEvents, mapToXStateEvent, createActor, nfWorkflowMachine) {
|
|
67
|
+
const adjacency = {}; // { fromState: { eventType: { toState, count } } }
|
|
68
|
+
let totalMapped = 0;
|
|
69
|
+
|
|
70
|
+
for (const event of conformanceEvents) {
|
|
71
|
+
const xstateEvent = mapToXStateEvent(event);
|
|
72
|
+
if (!xstateEvent) continue;
|
|
73
|
+
totalMapped++;
|
|
74
|
+
|
|
75
|
+
const actor = createActor(nfWorkflowMachine);
|
|
76
|
+
actor.start();
|
|
77
|
+
const beforeState = stateToString(actor.getSnapshot().value);
|
|
78
|
+
actor.send(xstateEvent);
|
|
79
|
+
const afterState = stateToString(actor.getSnapshot().value);
|
|
80
|
+
actor.stop();
|
|
81
|
+
|
|
82
|
+
if (!adjacency[beforeState]) adjacency[beforeState] = {};
|
|
83
|
+
const key = xstateEvent.type;
|
|
84
|
+
if (!adjacency[beforeState][key]) {
|
|
85
|
+
adjacency[beforeState][key] = { to_state: afterState, count: 0 };
|
|
86
|
+
}
|
|
87
|
+
adjacency[beforeState][key].count++;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { adjacency, totalMapped };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Mode B: Per-session running actor ───────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function replayPerSession(conformanceEvents, traceStats, mapToXStateEvent, createActor, nfWorkflowMachine) {
|
|
96
|
+
const adjacency = {};
|
|
97
|
+
let sessionsReplayed = 0;
|
|
98
|
+
|
|
99
|
+
const sessions = (traceStats && traceStats.sessions) || [];
|
|
100
|
+
for (const session of sessions) {
|
|
101
|
+
const sessionStart = new Date(session.start).getTime();
|
|
102
|
+
const sessionEnd = new Date(session.end).getTime();
|
|
103
|
+
const sessionEvents = conformanceEvents.filter(e => {
|
|
104
|
+
const t = new Date(e.ts).getTime();
|
|
105
|
+
return t >= sessionStart && t <= sessionEnd;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (sessionEvents.length === 0) continue;
|
|
109
|
+
|
|
110
|
+
const actor = createActor(nfWorkflowMachine);
|
|
111
|
+
actor.start();
|
|
112
|
+
sessionsReplayed++;
|
|
113
|
+
|
|
114
|
+
for (const event of sessionEvents) {
|
|
115
|
+
const xstateEvent = mapToXStateEvent(event);
|
|
116
|
+
if (!xstateEvent) continue;
|
|
117
|
+
|
|
118
|
+
const beforeState = stateToString(actor.getSnapshot().value);
|
|
119
|
+
actor.send(xstateEvent);
|
|
120
|
+
const afterState = stateToString(actor.getSnapshot().value);
|
|
121
|
+
|
|
122
|
+
const key = xstateEvent.type;
|
|
123
|
+
if (!adjacency[beforeState]) adjacency[beforeState] = {};
|
|
124
|
+
if (!adjacency[beforeState][key]) {
|
|
125
|
+
adjacency[beforeState][key] = { to_state: afterState, count: 0 };
|
|
126
|
+
}
|
|
127
|
+
adjacency[beforeState][key].count++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
actor.stop();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { adjacency, sessionsReplayed };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Merge ───────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function mergeAdjacency(perEvent, perSession) {
|
|
139
|
+
const merged = {};
|
|
140
|
+
|
|
141
|
+
// Add per-event transitions
|
|
142
|
+
for (const [from, events] of Object.entries(perEvent)) {
|
|
143
|
+
if (!merged[from]) merged[from] = {};
|
|
144
|
+
for (const [event, data] of Object.entries(events)) {
|
|
145
|
+
merged[from][event] = { ...data, source: 'per_event' };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Merge per-session transitions
|
|
150
|
+
for (const [from, events] of Object.entries(perSession)) {
|
|
151
|
+
if (!merged[from]) merged[from] = {};
|
|
152
|
+
for (const [event, data] of Object.entries(events)) {
|
|
153
|
+
if (merged[from][event]) {
|
|
154
|
+
// Both modes have this transition
|
|
155
|
+
merged[from][event].source = 'both';
|
|
156
|
+
// Keep per-event count as authoritative
|
|
157
|
+
} else {
|
|
158
|
+
merged[from][event] = { ...data, source: 'per_session' };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Model comparison ────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function compareWithModel(mergedAdjacency, modelTransitions) {
|
|
169
|
+
const observedSet = new Set();
|
|
170
|
+
for (const [from, events] of Object.entries(mergedAdjacency)) {
|
|
171
|
+
for (const [event, data] of Object.entries(events)) {
|
|
172
|
+
observedSet.add(`${from}::${event}::${data.to_state}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const modelSet = new Set();
|
|
177
|
+
for (const t of modelTransitions) {
|
|
178
|
+
modelSet.add(`${t.from}::${t.event}::${t.to}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const matching = [];
|
|
182
|
+
const missingInObserved = [];
|
|
183
|
+
const missingInModel = [];
|
|
184
|
+
|
|
185
|
+
for (const key of modelSet) {
|
|
186
|
+
const [from, event, to] = key.split('::');
|
|
187
|
+
if (observedSet.has(key)) {
|
|
188
|
+
matching.push({ from, event, to });
|
|
189
|
+
} else {
|
|
190
|
+
missingInObserved.push({ from, event, to });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const key of observedSet) {
|
|
195
|
+
if (!modelSet.has(key)) {
|
|
196
|
+
const [from, event, to] = key.split('::');
|
|
197
|
+
missingInModel.push({ from, event, to });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { matching, missing_in_observed: missingInObserved, missing_in_model: missingInModel };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Build FSM ───────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
function buildObservedFSM(conformanceEvents, traceStats) {
|
|
207
|
+
const { mapToXStateEvent } = require(path.join(__dirname, 'validate-traces.cjs'));
|
|
208
|
+
const machinePath = (() => {
|
|
209
|
+
const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'nf-workflow.machine.js');
|
|
210
|
+
const installDist = path.join(__dirname, 'dist', 'machines', 'nf-workflow.machine.js');
|
|
211
|
+
return fs.existsSync(repoDist) ? repoDist : installDist;
|
|
212
|
+
})();
|
|
213
|
+
const { createActor, nfWorkflowMachine } = require(machinePath);
|
|
214
|
+
|
|
215
|
+
// Mode A: Per-event isolation
|
|
216
|
+
const perEventResult = replayPerEvent(conformanceEvents, mapToXStateEvent, createActor, nfWorkflowMachine);
|
|
217
|
+
|
|
218
|
+
// Mode B: Per-session running actor
|
|
219
|
+
const perSessionResult = replayPerSession(conformanceEvents, traceStats, mapToXStateEvent, createActor, nfWorkflowMachine);
|
|
220
|
+
|
|
221
|
+
// Merge
|
|
222
|
+
const mergedAdjacency = mergeAdjacency(perEventResult.adjacency, perSessionResult.adjacency);
|
|
223
|
+
|
|
224
|
+
// Extract observed states
|
|
225
|
+
const statesObserved = new Set();
|
|
226
|
+
for (const [from, events] of Object.entries(mergedAdjacency)) {
|
|
227
|
+
statesObserved.add(from);
|
|
228
|
+
for (const data of Object.values(events)) {
|
|
229
|
+
statesObserved.add(data.to_state);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Model comparison
|
|
234
|
+
const model = extractModelTransitions();
|
|
235
|
+
const comparison = compareWithModel(mergedAdjacency, model.transitions);
|
|
236
|
+
|
|
237
|
+
// Coverage metrics
|
|
238
|
+
const totalEvents = conformanceEvents.length;
|
|
239
|
+
const mappedEvents = perEventResult.totalMapped;
|
|
240
|
+
const unmappedEvents = totalEvents - mappedEvents;
|
|
241
|
+
const modelTransitionCount = model.transitions.length;
|
|
242
|
+
const exercisedCount = comparison.matching.length;
|
|
243
|
+
|
|
244
|
+
// Count per-event and per-session unique transitions
|
|
245
|
+
const perEventTransitions = new Set();
|
|
246
|
+
for (const [from, events] of Object.entries(perEventResult.adjacency)) {
|
|
247
|
+
for (const [event, data] of Object.entries(events)) {
|
|
248
|
+
perEventTransitions.add(`${from}::${event}::${data.to_state}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const perSessionTransitions = new Set();
|
|
252
|
+
for (const [from, events] of Object.entries(perSessionResult.adjacency)) {
|
|
253
|
+
for (const [event, data] of Object.entries(events)) {
|
|
254
|
+
perSessionTransitions.add(`${from}::${event}::${data.to_state}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
schema_version: '1',
|
|
260
|
+
generated: new Date().toISOString(),
|
|
261
|
+
observed_transitions: mergedAdjacency,
|
|
262
|
+
states_observed: [...statesObserved].sort(),
|
|
263
|
+
model_comparison: comparison,
|
|
264
|
+
coverage: {
|
|
265
|
+
model_coverage: modelTransitionCount > 0 ? exercisedCount / modelTransitionCount : 0,
|
|
266
|
+
vocabulary_coverage: totalEvents > 0 ? mappedEvents / totalEvents : 0,
|
|
267
|
+
total_events: totalEvents,
|
|
268
|
+
mapped_events: mappedEvents,
|
|
269
|
+
unmapped_events: unmappedEvents
|
|
270
|
+
},
|
|
271
|
+
replay_modes: {
|
|
272
|
+
per_event_transitions: perEventTransitions.size,
|
|
273
|
+
per_session_transitions: perSessionTransitions.size,
|
|
274
|
+
sessions_replayed: perSessionResult.sessionsReplayed
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
if (require.main === module) {
|
|
282
|
+
// Read conformance events
|
|
283
|
+
const pp = require(path.join(__dirname, 'planning-paths.cjs'));
|
|
284
|
+
const logPath = pp.resolveWithFallback(ROOT, 'conformance-events');
|
|
285
|
+
let conformanceEvents = [];
|
|
286
|
+
if (fs.existsSync(logPath)) {
|
|
287
|
+
const raw = fs.readFileSync(logPath, 'utf8');
|
|
288
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
289
|
+
for (const line of lines) {
|
|
290
|
+
try { conformanceEvents.push(JSON.parse(line)); } catch (_) { /* skip */ }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Read trace stats
|
|
295
|
+
const statsPath = path.join(FORMAL, 'evidence', 'trace-corpus-stats.json');
|
|
296
|
+
let traceStats = null;
|
|
297
|
+
if (fs.existsSync(statsPath)) {
|
|
298
|
+
traceStats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const fsm = buildObservedFSM(conformanceEvents, traceStats);
|
|
302
|
+
|
|
303
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
304
|
+
fs.writeFileSync(OUT_FILE, JSON.stringify(fsm, null, 2) + '\n');
|
|
305
|
+
|
|
306
|
+
if (JSON_FLAG) {
|
|
307
|
+
process.stdout.write(JSON.stringify(fsm, null, 2) + '\n');
|
|
308
|
+
} else {
|
|
309
|
+
console.log(`Observed FSM written to ${path.relative(ROOT, OUT_FILE)}`);
|
|
310
|
+
console.log(` States observed: ${fsm.states_observed.length} (${fsm.states_observed.join(', ')})`);
|
|
311
|
+
console.log(` Model coverage: ${(fsm.coverage.model_coverage * 100).toFixed(1)}%`);
|
|
312
|
+
console.log(` Vocabulary coverage: ${(fsm.coverage.vocabulary_coverage * 100).toFixed(1)}%`);
|
|
313
|
+
console.log(` Events: ${fsm.coverage.total_events} total, ${fsm.coverage.mapped_events} mapped, ${fsm.coverage.unmapped_events} unmapped`);
|
|
314
|
+
console.log(` Replay modes: per_event=${fsm.replay_modes.per_event_transitions} per_session=${fsm.replay_modes.per_session_transitions} sessions=${fsm.replay_modes.sessions_replayed}`);
|
|
315
|
+
console.log(` Model comparison: matching=${fsm.model_comparison.matching.length} missing_in_observed=${fsm.model_comparison.missing_in_observed.length} missing_in_model=${fsm.model_comparison.missing_in_model.length}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
buildObservedFSM, extractModelTransitions, replayPerEvent, replayPerSession,
|
|
323
|
+
mergeAdjacency, compareWithModel, stateToString
|
|
324
|
+
};
|
package/bin/planning-paths.cjs
CHANGED
|
@@ -84,6 +84,12 @@ const TYPES = {
|
|
|
84
84
|
legacy: (root, p) => path.join(root, '.planning', `${p.version}-INTEGRATION-REPORT.md`),
|
|
85
85
|
},
|
|
86
86
|
|
|
87
|
+
// Quorum cache
|
|
88
|
+
'quorum-cache': {
|
|
89
|
+
canonical: (root) => path.join(root, '.planning', '.quorum-cache'),
|
|
90
|
+
legacy: (root) => path.join(root, '.planning', '.quorum-cache'),
|
|
91
|
+
},
|
|
92
|
+
|
|
87
93
|
// State backups
|
|
88
94
|
'state-backup': {
|
|
89
95
|
canonical: (root, p) => path.join(root, '.planning', 'archive', 'state-backups', `STATE.md.bak-${p.timestamp}`),
|
package/bin/polyrepo.cjs
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
|
|
8
|
-
const TAG = '[
|
|
8
|
+
const TAG = '[nf-polyrepo]';
|
|
9
9
|
const POLYREPOS_DIR = path.join(os.homedir(), '.claude', 'polyrepos');
|
|
10
10
|
const MARKER_FILE = 'polyrepo.json';
|
|
11
11
|
|
|
@@ -27,7 +27,7 @@ const os = require('os');
|
|
|
27
27
|
function findProviders() {
|
|
28
28
|
const searchPaths = [
|
|
29
29
|
path.join(__dirname, 'providers.json'),
|
|
30
|
-
path.join(os.homedir(), '.claude', '
|
|
30
|
+
path.join(os.homedir(), '.claude', 'nf-bin', 'providers.json'),
|
|
31
31
|
];
|
|
32
32
|
try {
|
|
33
33
|
const claudeJson = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* promote-gate-maturity.cjs — Gate maturity level promotion CLI.
|
|
6
|
+
*
|
|
7
|
+
* Manages per-model gate enforcement levels: ADVISORY -> SOFT_GATE -> HARD_GATE.
|
|
8
|
+
*
|
|
9
|
+
* Requirements: GATE-04
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node bin/promote-gate-maturity.cjs --model "alloy/quorum-votes.als" --level SOFT_GATE
|
|
13
|
+
* node bin/promote-gate-maturity.cjs --check # validate all models
|
|
14
|
+
* node bin/promote-gate-maturity.cjs --check --fix # demote violating models
|
|
15
|
+
* node bin/promote-gate-maturity.cjs --json # JSON output mode
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
|
|
22
|
+
const FORMAL = path.join(ROOT, '.planning', 'formal');
|
|
23
|
+
const REGISTRY_PATH = path.join(FORMAL, 'model-registry.json');
|
|
24
|
+
const CHECK_RESULTS_PATH = path.join(FORMAL, 'check-results.ndjson');
|
|
25
|
+
|
|
26
|
+
const JSON_FLAG = process.argv.includes('--json');
|
|
27
|
+
const CHECK_FLAG = process.argv.includes('--check');
|
|
28
|
+
const FIX_FLAG = process.argv.includes('--fix');
|
|
29
|
+
|
|
30
|
+
const LEVELS = ['ADVISORY', 'SOFT_GATE', 'HARD_GATE'];
|
|
31
|
+
const LEVEL_RANK = { ADVISORY: 0, SOFT_GATE: 1, HARD_GATE: 2 };
|
|
32
|
+
|
|
33
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function getModelKeys(registry) {
|
|
36
|
+
return Object.keys(registry).filter(k => k.startsWith('.'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getModelLevel(model) {
|
|
40
|
+
return model.gate_maturity || 'ADVISORY';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Infer source_layer from the model path.
|
|
45
|
+
*/
|
|
46
|
+
function inferSourceLayer(modelPath) {
|
|
47
|
+
if (modelPath.includes('evidence/') || modelPath.includes('L1') || modelPath.includes('conformance')) return 'L1';
|
|
48
|
+
if (modelPath.includes('semantics/') || modelPath.includes('L2')) return 'L2';
|
|
49
|
+
if (modelPath.includes('reasoning/') || modelPath.includes('L3')) return 'L3';
|
|
50
|
+
// TLA+ and Alloy models are L2 by default (formal specs of the operational model)
|
|
51
|
+
if (modelPath.endsWith('.tla') || modelPath.endsWith('.als') || modelPath.endsWith('.props')) return 'L2';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Validation criteria ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validate that a model meets its current (or target) gate_maturity criteria.
|
|
59
|
+
* Returns { valid: boolean, reason: string }
|
|
60
|
+
*/
|
|
61
|
+
function validateCriteria(modelPath, model, targetLevel, checkResults) {
|
|
62
|
+
const level = targetLevel || getModelLevel(model);
|
|
63
|
+
|
|
64
|
+
if (level === 'ADVISORY') {
|
|
65
|
+
return { valid: true, reason: 'ADVISORY: no criteria required' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// SOFT_GATE: requires source_layer
|
|
69
|
+
const sourceLayer = model.source_layer || inferSourceLayer(modelPath);
|
|
70
|
+
if (level === 'SOFT_GATE' || level === 'HARD_GATE') {
|
|
71
|
+
if (!sourceLayer) {
|
|
72
|
+
return { valid: false, reason: 'SOFT_GATE requires source_layer assignment' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// HARD_GATE: requires SOFT_GATE + passing check-result
|
|
77
|
+
if (level === 'HARD_GATE') {
|
|
78
|
+
const modelLower = modelPath.toLowerCase();
|
|
79
|
+
const hasPassingCheck = checkResults.some(cr => {
|
|
80
|
+
if (cr.result !== 'pass') return false;
|
|
81
|
+
const toolMatch = cr.tool && modelLower.includes(cr.tool.toLowerCase());
|
|
82
|
+
const checkIdPart = cr.check_id ? (cr.check_id.split(':')[1] || '') : '';
|
|
83
|
+
const checkIdMatch = checkIdPart && modelLower.includes(checkIdPart.toLowerCase());
|
|
84
|
+
return toolMatch || checkIdMatch;
|
|
85
|
+
});
|
|
86
|
+
if (!hasPassingCheck) {
|
|
87
|
+
return { valid: false, reason: 'HARD_GATE requires at least one passing check-result' };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { valid: true, reason: `${level}: all criteria met` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Promote a single model ──────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function promoteModel(registry, modelPath, targetLevel, checkResults) {
|
|
97
|
+
const model = registry[modelPath];
|
|
98
|
+
if (!model) {
|
|
99
|
+
return { success: false, error: `Model not found: ${modelPath}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const currentLevel = getModelLevel(model);
|
|
103
|
+
const currentRank = LEVEL_RANK[currentLevel] ?? 0;
|
|
104
|
+
const targetRank = LEVEL_RANK[targetLevel];
|
|
105
|
+
|
|
106
|
+
if (targetRank === undefined) {
|
|
107
|
+
return { success: false, error: `Invalid level: ${targetLevel}. Must be one of: ${LEVELS.join(', ')}` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (targetRank <= currentRank) {
|
|
111
|
+
return { success: false, error: `Cannot promote: ${currentLevel} -> ${targetLevel} is not a promotion` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Validate target level criteria
|
|
115
|
+
const validation = validateCriteria(modelPath, model, targetLevel, checkResults);
|
|
116
|
+
if (!validation.valid) {
|
|
117
|
+
return { success: false, error: `Criteria not met for ${targetLevel}: ${validation.reason}` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Apply promotion
|
|
121
|
+
model.gate_maturity = targetLevel;
|
|
122
|
+
model.last_updated = new Date().toISOString();
|
|
123
|
+
if (!model.source_layer) {
|
|
124
|
+
model.source_layer = inferSourceLayer(modelPath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { success: true, from: currentLevel, to: targetLevel };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Check all models ────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function checkAllModels(registry, checkResults, fix) {
|
|
133
|
+
const modelKeys = getModelKeys(registry);
|
|
134
|
+
const violations = [];
|
|
135
|
+
const demotions = [];
|
|
136
|
+
|
|
137
|
+
for (const modelPath of modelKeys) {
|
|
138
|
+
const model = registry[modelPath];
|
|
139
|
+
const level = getModelLevel(model);
|
|
140
|
+
|
|
141
|
+
if (level === 'ADVISORY') continue; // Always valid
|
|
142
|
+
|
|
143
|
+
const validation = validateCriteria(modelPath, model, level, checkResults);
|
|
144
|
+
if (!validation.valid) {
|
|
145
|
+
violations.push({
|
|
146
|
+
model: modelPath,
|
|
147
|
+
level,
|
|
148
|
+
reason: validation.reason,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (fix) {
|
|
152
|
+
// Demote to highest valid level
|
|
153
|
+
if (level === 'HARD_GATE') {
|
|
154
|
+
const softCheck = validateCriteria(modelPath, model, 'SOFT_GATE', checkResults);
|
|
155
|
+
if (softCheck.valid) {
|
|
156
|
+
model.gate_maturity = 'SOFT_GATE';
|
|
157
|
+
model.last_updated = new Date().toISOString();
|
|
158
|
+
demotions.push({ model: modelPath, from: level, to: 'SOFT_GATE' });
|
|
159
|
+
} else {
|
|
160
|
+
model.gate_maturity = 'ADVISORY';
|
|
161
|
+
model.last_updated = new Date().toISOString();
|
|
162
|
+
demotions.push({ model: modelPath, from: level, to: 'ADVISORY' });
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
model.gate_maturity = 'ADVISORY';
|
|
166
|
+
model.last_updated = new Date().toISOString();
|
|
167
|
+
demotions.push({ model: modelPath, from: level, to: 'ADVISORY' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
total: modelKeys.length,
|
|
175
|
+
checked: modelKeys.filter(k => getModelLevel(registry[k]) !== 'ADVISORY').length,
|
|
176
|
+
violations,
|
|
177
|
+
demotions,
|
|
178
|
+
by_level: {
|
|
179
|
+
ADVISORY: modelKeys.filter(k => getModelLevel(registry[k]) === 'ADVISORY').length,
|
|
180
|
+
SOFT_GATE: modelKeys.filter(k => getModelLevel(registry[k]) === 'SOFT_GATE').length,
|
|
181
|
+
HARD_GATE: modelKeys.filter(k => getModelLevel(registry[k]) === 'HARD_GATE').length,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Load check results ──────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
function loadCheckResults() {
|
|
189
|
+
if (!fs.existsSync(CHECK_RESULTS_PATH)) return [];
|
|
190
|
+
return fs.readFileSync(CHECK_RESULTS_PATH, 'utf8')
|
|
191
|
+
.trim().split('\n').filter(Boolean)
|
|
192
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
function main() {
|
|
199
|
+
if (!fs.existsSync(REGISTRY_PATH)) {
|
|
200
|
+
console.error('ERROR: model-registry.json not found at', REGISTRY_PATH);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
|
205
|
+
const checkResults = loadCheckResults();
|
|
206
|
+
|
|
207
|
+
if (CHECK_FLAG) {
|
|
208
|
+
const result = checkAllModels(registry, checkResults, FIX_FLAG);
|
|
209
|
+
|
|
210
|
+
if (FIX_FLAG && result.demotions.length > 0) {
|
|
211
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (JSON_FLAG) {
|
|
215
|
+
process.stdout.write(JSON.stringify(result));
|
|
216
|
+
} else {
|
|
217
|
+
console.log(`Gate Maturity Check`);
|
|
218
|
+
console.log(` Total models: ${result.total}`);
|
|
219
|
+
console.log(` By level: ${JSON.stringify(result.by_level)}`);
|
|
220
|
+
console.log(` Violations: ${result.violations.length}`);
|
|
221
|
+
if (result.violations.length > 0) {
|
|
222
|
+
for (const v of result.violations) {
|
|
223
|
+
console.log(` - ${v.model}: ${v.reason}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (result.demotions.length > 0) {
|
|
227
|
+
console.log(` Demotions applied:`);
|
|
228
|
+
for (const d of result.demotions) {
|
|
229
|
+
console.log(` - ${d.model}: ${d.from} -> ${d.to}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
process.exit(result.violations.length > 0 && !FIX_FLAG ? 1 : 0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Promotion mode
|
|
238
|
+
const modelIdx = process.argv.indexOf('--model');
|
|
239
|
+
const levelIdx = process.argv.indexOf('--level');
|
|
240
|
+
|
|
241
|
+
if (modelIdx === -1 || levelIdx === -1) {
|
|
242
|
+
console.error('Usage: --model <path> --level <ADVISORY|SOFT_GATE|HARD_GATE>');
|
|
243
|
+
console.error(' or: --check [--fix] [--json]');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const modelPath = process.argv[modelIdx + 1];
|
|
248
|
+
const targetLevel = process.argv[levelIdx + 1];
|
|
249
|
+
|
|
250
|
+
const result = promoteModel(registry, modelPath, targetLevel, checkResults);
|
|
251
|
+
|
|
252
|
+
if (result.success) {
|
|
253
|
+
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
254
|
+
|
|
255
|
+
if (JSON_FLAG) {
|
|
256
|
+
process.stdout.write(JSON.stringify(result));
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`Promoted: ${modelPath}`);
|
|
259
|
+
console.log(` ${result.from} -> ${result.to}`);
|
|
260
|
+
}
|
|
261
|
+
process.exit(0);
|
|
262
|
+
} else {
|
|
263
|
+
if (JSON_FLAG) {
|
|
264
|
+
process.stdout.write(JSON.stringify(result));
|
|
265
|
+
} else {
|
|
266
|
+
console.error(`Promotion failed: ${result.error}`);
|
|
267
|
+
}
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (require.main === module) main();
|
|
273
|
+
|
|
274
|
+
module.exports = { promoteModel, validateCriteria, checkAllModels, getModelKeys, inferSourceLayer, loadCheckResults };
|
package/bin/promote-model.cjs
CHANGED
|
@@ -20,7 +20,7 @@ const ROOT = path.join(__dirname, '..');
|
|
|
20
20
|
// ── Find .planning/formal/ root from target path ───────────────────────────────────────
|
|
21
21
|
// Walks up from a given path until finding .planning/formal/, then
|
|
22
22
|
// returns the grandparent of that directory as the project root.
|
|
23
|
-
// Falls back to ROOT (the
|
|
23
|
+
// Falls back to ROOT (the nForma project root) if no .planning/formal/ ancestor is found.
|
|
24
24
|
function findProjectRoot(startPath) {
|
|
25
25
|
let current = path.dirname(startPath);
|
|
26
26
|
while (true) {
|
|
@@ -13,7 +13,7 @@ const fs = require('fs');
|
|
|
13
13
|
const NON_INTERACTIVE = process.argv.includes('--non-interactive');
|
|
14
14
|
const DEBUG_ARTIFACT_PATH = path.join(process.cwd(), '.planning', 'quick', 'quorum-debug-latest.md');
|
|
15
15
|
const ACCEPT_SCRIPT = path.join(__dirname, 'accept-debug-invariant.cjs');
|
|
16
|
-
const DEFAULT_SPEC = path.join(process.cwd(), '.planning', 'formal', 'tla', '
|
|
16
|
+
const DEFAULT_SPEC = path.join(process.cwd(), '.planning', 'formal', 'tla', 'NFQuorum.tla');
|
|
17
17
|
|
|
18
18
|
function sanitizeTlaName(str) {
|
|
19
19
|
return str.replace(/[^a-zA-Z0-9]+/g, '_').slice(0, 40).replace(/^_+|_+$/g, '') || 'Unknown';
|