@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,334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gate-a-grounding.cjs — Gate A grounding score computation.
|
|
6
|
+
*
|
|
7
|
+
* Measures alignment between L1 evidence (conformance traces) and L2 semantics
|
|
8
|
+
* (operational model). A trace event is "explained" iff:
|
|
9
|
+
* 1. Its action maps to a known entry in event-vocabulary.json (vocabulary_mapped = true)
|
|
10
|
+
* 2. Its XState transition is EITHER:
|
|
11
|
+
* a. Validated by fresh actor replay (xstate_valid = true), OR
|
|
12
|
+
* b. Correctly skipped as a mid-session event under H1 methodology (methodology_skip = true)
|
|
13
|
+
*
|
|
14
|
+
* Unexplained traces are classified into:
|
|
15
|
+
* - instrumentation_bug: action NOT in vocabulary
|
|
16
|
+
* - model_gap: action in vocabulary but XState replay fails
|
|
17
|
+
* - genuine_violation: model_gap event that violates a declared observed invariant
|
|
18
|
+
*
|
|
19
|
+
* Requirements: GATE-01
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node bin/gate-a-grounding.cjs # print summary to stdout
|
|
23
|
+
* node bin/gate-a-grounding.cjs --json # print full results JSON to stdout
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
|
|
30
|
+
const FORMAL = path.join(ROOT, '.planning', 'formal');
|
|
31
|
+
const GATES_DIR = path.join(FORMAL, 'gates');
|
|
32
|
+
const OUT_FILE = path.join(GATES_DIR, 'gate-a-grounding.json');
|
|
33
|
+
|
|
34
|
+
const JSON_FLAG = process.argv.includes('--json');
|
|
35
|
+
|
|
36
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* H1 methodology skip: mid-session events (phase !== 'IDLE' AND action !== 'quorum_start')
|
|
40
|
+
* cannot be validated with a fresh actor from IDLE.
|
|
41
|
+
*/
|
|
42
|
+
function isMethodologySkip(event) {
|
|
43
|
+
if (!event) return false;
|
|
44
|
+
if (event.action === 'quorum_start') return false;
|
|
45
|
+
if (event.phase && event.phase !== 'IDLE') return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a model_gap event violates an observed invariant.
|
|
51
|
+
* Returns the violated invariant name or null.
|
|
52
|
+
*/
|
|
53
|
+
function checkGenuineViolation(event, observedInvariants, sessionContext) {
|
|
54
|
+
if (!observedInvariants || observedInvariants.length === 0) return null;
|
|
55
|
+
|
|
56
|
+
for (const inv of observedInvariants) {
|
|
57
|
+
const expr = (inv.property_expression || '').toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Match invariant property_expression keywords against event action
|
|
60
|
+
if (inv.name === 'quorum_start_precedes_complete' || expr.includes('quorum_start always precedes quorum_complete')) {
|
|
61
|
+
if (event.action === 'quorum_complete' && sessionContext && !sessionContext.seenQuorumStart) {
|
|
62
|
+
return inv.name;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (inv.name === 'circuit_break_within_quorum_session' || expr.includes('circuit_break events only occur during active quorum')) {
|
|
67
|
+
if (event.action === 'circuit_break' && sessionContext && !sessionContext.seenQuorumStart) {
|
|
68
|
+
return inv.name;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Core computation ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function computeGateA(conformanceEvents, vocabulary, invariantCatalog, mismatchRegister) {
|
|
79
|
+
const warnings = [];
|
|
80
|
+
|
|
81
|
+
// Load mapToXStateEvent
|
|
82
|
+
const { mapToXStateEvent } = require(path.join(__dirname, 'validate-traces.cjs'));
|
|
83
|
+
|
|
84
|
+
// Load XState machine
|
|
85
|
+
const machinePath = (() => {
|
|
86
|
+
const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'nf-workflow.machine.js');
|
|
87
|
+
const installDist = path.join(__dirname, 'dist', 'machines', 'nf-workflow.machine.js');
|
|
88
|
+
return fs.existsSync(repoDist) ? repoDist : installDist;
|
|
89
|
+
})();
|
|
90
|
+
const { createActor, nfWorkflowMachine } = require(machinePath);
|
|
91
|
+
|
|
92
|
+
// Extract vocabulary actions set
|
|
93
|
+
const vocabActions = new Set();
|
|
94
|
+
if (vocabulary && vocabulary.vocabulary) {
|
|
95
|
+
for (const key of Object.keys(vocabulary.vocabulary)) {
|
|
96
|
+
vocabActions.add(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Extract observed invariants for genuine_violation check
|
|
101
|
+
let observedInvariants = [];
|
|
102
|
+
if (invariantCatalog && invariantCatalog.invariants) {
|
|
103
|
+
observedInvariants = invariantCatalog.invariants.filter(i => i.type === 'observed');
|
|
104
|
+
} else {
|
|
105
|
+
warnings.push('invariant-catalog.json not found or invalid, skipping genuine_violation reclassification');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!mismatchRegister) {
|
|
109
|
+
warnings.push('mismatch-register.jsonl not found, skipping mismatch incorporation');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Counters
|
|
113
|
+
let explained = 0;
|
|
114
|
+
let xstateValidated = 0;
|
|
115
|
+
let methodologySkips = 0;
|
|
116
|
+
let vocabularyMapped = 0;
|
|
117
|
+
|
|
118
|
+
const unexplainedCounts = { instrumentation_bug: 0, model_gap: 0, genuine_violation: 0 };
|
|
119
|
+
const unexplainedActions = { instrumentation_bug: {}, model_gap: {}, genuine_violation: {} };
|
|
120
|
+
const violatedInvariants = {};
|
|
121
|
+
|
|
122
|
+
// Simple session context tracking for genuine violation checks
|
|
123
|
+
// We track per-session whether quorum_start has been seen
|
|
124
|
+
let sessionContext = { seenQuorumStart: false };
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < conformanceEvents.length; i++) {
|
|
127
|
+
const event = conformanceEvents[i];
|
|
128
|
+
|
|
129
|
+
// Reset session context on IDLE phase events
|
|
130
|
+
if (event.phase === 'IDLE' && event.action === 'quorum_start') {
|
|
131
|
+
sessionContext = { seenQuorumStart: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Step 1: Is the action in the vocabulary?
|
|
135
|
+
const inVocab = vocabActions.has(event.action);
|
|
136
|
+
|
|
137
|
+
if (!inVocab) {
|
|
138
|
+
// NOT in vocabulary -> instrumentation_bug
|
|
139
|
+
unexplainedCounts.instrumentation_bug++;
|
|
140
|
+
unexplainedActions.instrumentation_bug[event.action] = (unexplainedActions.instrumentation_bug[event.action] || 0) + 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
vocabularyMapped++;
|
|
145
|
+
|
|
146
|
+
// Step 2: Map to XState event
|
|
147
|
+
const xstateEvent = mapToXStateEvent(event);
|
|
148
|
+
if (!xstateEvent) {
|
|
149
|
+
// In vocab but no XState mapping -> instrumentation_bug
|
|
150
|
+
unexplainedCounts.instrumentation_bug++;
|
|
151
|
+
unexplainedActions.instrumentation_bug[event.action + ':no_xstate_map'] = (unexplainedActions.instrumentation_bug[event.action + ':no_xstate_map'] || 0) + 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Step 3: H1 methodology skip?
|
|
156
|
+
if (isMethodologySkip(event)) {
|
|
157
|
+
methodologySkips++;
|
|
158
|
+
explained++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Step 4: Fresh actor replay
|
|
163
|
+
const actor = createActor(nfWorkflowMachine);
|
|
164
|
+
actor.start();
|
|
165
|
+
const beforeState = actor.getSnapshot().value;
|
|
166
|
+
actor.send(xstateEvent);
|
|
167
|
+
const afterState = actor.getSnapshot().value;
|
|
168
|
+
actor.stop();
|
|
169
|
+
|
|
170
|
+
// Check if the transition was accepted (state changed or self-loop is valid)
|
|
171
|
+
// A "valid" transition means the machine processed it without error
|
|
172
|
+
// For fresh-from-IDLE replay, the key test is: did the machine move to a state
|
|
173
|
+
// consistent with the event type?
|
|
174
|
+
const stateStr = typeof afterState === 'string' ? afterState : JSON.stringify(afterState);
|
|
175
|
+
|
|
176
|
+
// Consider the transition valid if the machine accepted the event
|
|
177
|
+
// (any state change from IDLE or valid self-loop like CIRCUIT_BREAK)
|
|
178
|
+
const transitionAccepted = (stateStr !== 'IDLE' || xstateEvent.type === 'CIRCUIT_BREAK');
|
|
179
|
+
|
|
180
|
+
if (transitionAccepted) {
|
|
181
|
+
xstateValidated++;
|
|
182
|
+
explained++;
|
|
183
|
+
} else {
|
|
184
|
+
// model_gap: in vocab, mapped, but replay fails
|
|
185
|
+
// Check for genuine_violation reclassification
|
|
186
|
+
const violatedInvariant = checkGenuineViolation(event, observedInvariants, sessionContext);
|
|
187
|
+
if (violatedInvariant) {
|
|
188
|
+
unexplainedCounts.genuine_violation++;
|
|
189
|
+
unexplainedActions.genuine_violation[event.action] = (unexplainedActions.genuine_violation[event.action] || 0) + 1;
|
|
190
|
+
violatedInvariants[violatedInvariant] = (violatedInvariants[violatedInvariant] || 0) + 1;
|
|
191
|
+
} else {
|
|
192
|
+
unexplainedCounts.model_gap++;
|
|
193
|
+
unexplainedActions.model_gap[event.action] = (unexplainedActions.model_gap[event.action] || 0) + 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const total = conformanceEvents.length;
|
|
199
|
+
const groundingScore = total > 0 ? explained / total : 0;
|
|
200
|
+
|
|
201
|
+
// Build top actions lists for unexplained summary
|
|
202
|
+
const topActions = (obj) => Object.entries(obj).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => ({ action: k, count: v }));
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
schema_version: '1',
|
|
206
|
+
generated: new Date().toISOString(),
|
|
207
|
+
grounding_score: groundingScore,
|
|
208
|
+
target: 0.80,
|
|
209
|
+
target_met: groundingScore >= 0.80,
|
|
210
|
+
explained,
|
|
211
|
+
total,
|
|
212
|
+
unexplained_counts: unexplainedCounts,
|
|
213
|
+
unexplained_summary: {
|
|
214
|
+
instrumentation_bug: { top_actions: topActions(unexplainedActions.instrumentation_bug), total: unexplainedCounts.instrumentation_bug },
|
|
215
|
+
model_gap: { top_mismatches: topActions(unexplainedActions.model_gap), total: unexplainedCounts.model_gap },
|
|
216
|
+
genuine_violation: { violated_invariants: Object.entries(violatedInvariants).map(([k, v]) => ({ invariant: k, count: v })), total: unexplainedCounts.genuine_violation }
|
|
217
|
+
},
|
|
218
|
+
methodology: {
|
|
219
|
+
explains_definition: 'vocabulary_mapped AND (xstate_valid OR methodology_skip)',
|
|
220
|
+
h1_methodology_skips: methodologySkips,
|
|
221
|
+
xstate_validated: xstateValidated,
|
|
222
|
+
vocabulary_mapped: vocabularyMapped
|
|
223
|
+
},
|
|
224
|
+
warnings
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
if (require.main === module) {
|
|
231
|
+
// Read conformance events
|
|
232
|
+
const pp = require(path.join(__dirname, 'planning-paths.cjs'));
|
|
233
|
+
const logPath = pp.resolveWithFallback(ROOT, 'conformance-events');
|
|
234
|
+
let conformanceEvents = [];
|
|
235
|
+
if (fs.existsSync(logPath)) {
|
|
236
|
+
const raw = fs.readFileSync(logPath, 'utf8');
|
|
237
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
try { conformanceEvents.push(JSON.parse(line)); } catch (_) { /* skip */ }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Read vocabulary
|
|
244
|
+
const vocabPath = path.join(FORMAL, 'evidence', 'event-vocabulary.json');
|
|
245
|
+
let vocabulary = null;
|
|
246
|
+
if (fs.existsSync(vocabPath)) {
|
|
247
|
+
try { vocabulary = JSON.parse(fs.readFileSync(vocabPath, 'utf8')); } catch (_) { /* fail-open */ }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Try to read invariant catalog (graceful degradation)
|
|
251
|
+
let invariantCatalog = null;
|
|
252
|
+
const invCatPath = path.join(FORMAL, 'semantics', 'invariant-catalog.json');
|
|
253
|
+
if (fs.existsSync(invCatPath)) {
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(fs.readFileSync(invCatPath, 'utf8'));
|
|
256
|
+
if (parsed.schema_version && Array.isArray(parsed.invariants)) {
|
|
257
|
+
invariantCatalog = parsed;
|
|
258
|
+
} else {
|
|
259
|
+
process.stderr.write('WARNING: invariant-catalog.json has invalid schema, skipping\n');
|
|
260
|
+
}
|
|
261
|
+
} catch (_) {
|
|
262
|
+
process.stderr.write('WARNING: invariant-catalog.json parse error, skipping\n');
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
process.stderr.write('WARNING: invariant-catalog.json not found, skipping genuine_violation reclassification\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Try to read mismatch register (graceful degradation)
|
|
269
|
+
let mismatchRegister = null;
|
|
270
|
+
const mmPath = path.join(FORMAL, 'semantics', 'mismatch-register.jsonl');
|
|
271
|
+
if (fs.existsSync(mmPath)) {
|
|
272
|
+
try {
|
|
273
|
+
const lines = fs.readFileSync(mmPath, 'utf8').trim().split('\n').filter(l => l.trim());
|
|
274
|
+
mismatchRegister = lines.map(l => {
|
|
275
|
+
const parsed = JSON.parse(l);
|
|
276
|
+
if (!parsed.resolution) throw new Error('Missing resolution field');
|
|
277
|
+
return parsed;
|
|
278
|
+
});
|
|
279
|
+
} catch (e) {
|
|
280
|
+
process.stderr.write('WARNING: mismatch-register.jsonl parse error, skipping mismatch incorporation\n');
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
process.stderr.write('WARNING: mismatch-register.jsonl not found, skipping mismatch incorporation\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const result = computeGateA(conformanceEvents, vocabulary, invariantCatalog, mismatchRegister);
|
|
287
|
+
|
|
288
|
+
// EARLY WARNING check
|
|
289
|
+
if (result.grounding_score < 0.50) {
|
|
290
|
+
process.stderr.write(`\nEARLY WARNING: grounding_score is ${(result.grounding_score * 100).toFixed(1)}% (< 50%). Re-examine explains definition!\n`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Write output
|
|
294
|
+
fs.mkdirSync(GATES_DIR, { recursive: true });
|
|
295
|
+
fs.writeFileSync(OUT_FILE, JSON.stringify(result, null, 2) + '\n');
|
|
296
|
+
|
|
297
|
+
// Emit check result via write-check-result
|
|
298
|
+
try {
|
|
299
|
+
const { writeCheckResult } = require(path.join(__dirname, 'write-check-result.cjs'));
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
writeCheckResult({
|
|
302
|
+
tool: 'gate-a-grounding',
|
|
303
|
+
formalism: 'trace',
|
|
304
|
+
result: result.target_met ? 'pass' : 'fail',
|
|
305
|
+
check_id: 'gate-a:grounding-score',
|
|
306
|
+
surface: 'trace',
|
|
307
|
+
property: `Gate A grounding score: ${(result.grounding_score * 100).toFixed(1)}% (target: 80%)`,
|
|
308
|
+
runtime_ms: Date.now() - startTime,
|
|
309
|
+
summary: `${result.target_met ? 'pass' : 'fail'}: grounding ${(result.grounding_score * 100).toFixed(1)}%, ${result.explained}/${result.total} explained`,
|
|
310
|
+
requirement_ids: ['GATE-01'],
|
|
311
|
+
metadata: { grounding_score: result.grounding_score, target: result.target, target_met: result.target_met }
|
|
312
|
+
});
|
|
313
|
+
} catch (e) {
|
|
314
|
+
process.stderr.write('WARNING: Could not write check result: ' + e.message + '\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (JSON_FLAG) {
|
|
318
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
319
|
+
} else {
|
|
320
|
+
console.log(`Gate A Grounding Score: ${(result.grounding_score * 100).toFixed(1)}%`);
|
|
321
|
+
console.log(` Target: >= 80% | Met: ${result.target_met}`);
|
|
322
|
+
console.log(` Explained: ${result.explained} / ${result.total}`);
|
|
323
|
+
console.log(` Unexplained: instrumentation_bug=${result.unexplained_counts.instrumentation_bug} model_gap=${result.unexplained_counts.model_gap} genuine_violation=${result.unexplained_counts.genuine_violation}`);
|
|
324
|
+
console.log(` Methodology: xstate_validated=${result.methodology.xstate_validated} h1_skips=${result.methodology.h1_methodology_skips} vocab_mapped=${result.methodology.vocabulary_mapped}`);
|
|
325
|
+
if (result.warnings.length > 0) {
|
|
326
|
+
console.log(` Warnings: ${result.warnings.join('; ')}`);
|
|
327
|
+
}
|
|
328
|
+
console.log(` Output: ${OUT_FILE}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
process.exit(0);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { computeGateA, isMethodologySkip, checkGenuineViolation };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gate-b-abstraction.cjs — Gate B L2-L3 traceability verification.
|
|
6
|
+
*
|
|
7
|
+
* Verifies every L3 reasoning artifact entry has valid derived_from links
|
|
8
|
+
* to L2 semantics sources. Reports orphaned hazards.
|
|
9
|
+
*
|
|
10
|
+
* Requirements: GATE-02
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node bin/gate-b-abstraction.cjs # print summary to stdout
|
|
14
|
+
* node bin/gate-b-abstraction.cjs --json # print full results JSON to stdout
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
|
|
21
|
+
const FORMAL = path.join(ROOT, '.planning', 'formal');
|
|
22
|
+
const GATES_DIR = path.join(FORMAL, 'gates');
|
|
23
|
+
const REASONING_DIR = path.join(FORMAL, 'reasoning');
|
|
24
|
+
const SEMANTICS_DIR = path.join(FORMAL, 'semantics');
|
|
25
|
+
const EVIDENCE_DIR = path.join(FORMAL, 'evidence');
|
|
26
|
+
const OUT_FILE = path.join(GATES_DIR, 'gate-b-abstraction.json');
|
|
27
|
+
|
|
28
|
+
const JSON_FLAG = process.argv.includes('--json');
|
|
29
|
+
|
|
30
|
+
// ── L2 artifact cache ──────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const l2Cache = {};
|
|
33
|
+
|
|
34
|
+
function loadL2Artifact(artifactRelPath) {
|
|
35
|
+
if (l2Cache[artifactRelPath]) return l2Cache[artifactRelPath];
|
|
36
|
+
|
|
37
|
+
const fullPath = path.join(FORMAL, artifactRelPath);
|
|
38
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
39
|
+
|
|
40
|
+
let data;
|
|
41
|
+
if (fullPath.endsWith('.jsonl')) {
|
|
42
|
+
data = fs.readFileSync(fullPath, 'utf8')
|
|
43
|
+
.trim().split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
44
|
+
} else {
|
|
45
|
+
data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
l2Cache[artifactRelPath] = data;
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Link resolution ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a derived_from link by loading the referenced L2 artifact and
|
|
56
|
+
* navigating to the ref path. Returns true if the link is valid.
|
|
57
|
+
*/
|
|
58
|
+
function resolveL2Link(link) {
|
|
59
|
+
if (!link || !link.artifact || !link.ref) return false;
|
|
60
|
+
|
|
61
|
+
const data = loadL2Artifact(link.artifact);
|
|
62
|
+
if (data === null) return false;
|
|
63
|
+
|
|
64
|
+
// For JSONL (mismatch-register), just verify the file loaded (array)
|
|
65
|
+
if (link.artifact.endsWith('.jsonl')) {
|
|
66
|
+
return Array.isArray(data) && data.length > 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// For L3 self-references (reasoning/ artifacts), verify file exists
|
|
70
|
+
if (link.layer === 'L3') {
|
|
71
|
+
const l3Path = path.join(FORMAL, link.artifact);
|
|
72
|
+
return fs.existsSync(l3Path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ref = link.ref;
|
|
76
|
+
|
|
77
|
+
// Handle array filter refs: invariants[name=X,config=Y] or invariants[config=X]
|
|
78
|
+
const arrayFilterMatch = ref.match(/^(\w+)\[(.+)\]$/);
|
|
79
|
+
if (arrayFilterMatch) {
|
|
80
|
+
const arrayKey = arrayFilterMatch[1];
|
|
81
|
+
const filterStr = arrayFilterMatch[2];
|
|
82
|
+
const arr = data[arrayKey];
|
|
83
|
+
if (!Array.isArray(arr)) return false;
|
|
84
|
+
|
|
85
|
+
// Parse filter conditions: key=value pairs
|
|
86
|
+
const conditions = filterStr.split(',').map(c => {
|
|
87
|
+
const [k, v] = c.trim().split('=');
|
|
88
|
+
return { key: k, value: v };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Find at least one item matching all conditions
|
|
92
|
+
return arr.some(item =>
|
|
93
|
+
conditions.every(cond => String(item[cond.key]) === cond.value)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle dot-path navigation: observed_transitions.IDLE.QUORUM_START
|
|
98
|
+
const parts = ref.split('.');
|
|
99
|
+
let current = data;
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
// Handle wildcard: sessions[*].actions
|
|
102
|
+
if (part === '*' || part.includes('[*]')) {
|
|
103
|
+
// For wildcard refs, just check the parent array/object exists
|
|
104
|
+
return current !== undefined && current !== null;
|
|
105
|
+
}
|
|
106
|
+
if (current === undefined || current === null) return false;
|
|
107
|
+
if (typeof current !== 'object') return false;
|
|
108
|
+
current = current[part];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return current !== undefined && current !== null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Gate B check ────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function checkGateB(l3Artifacts) {
|
|
117
|
+
let totalEntries = 0;
|
|
118
|
+
let groundedEntries = 0;
|
|
119
|
+
const orphans = [];
|
|
120
|
+
|
|
121
|
+
for (const { name, entries } of l3Artifacts) {
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
totalEntries++;
|
|
124
|
+
|
|
125
|
+
if (!entry.derived_from || !Array.isArray(entry.derived_from) || entry.derived_from.length === 0) {
|
|
126
|
+
orphans.push({
|
|
127
|
+
artifact: name,
|
|
128
|
+
entry_id: entry.id || 'unknown',
|
|
129
|
+
reason: 'missing or empty derived_from',
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let allValid = true;
|
|
135
|
+
for (const link of entry.derived_from) {
|
|
136
|
+
if (!resolveL2Link(link)) {
|
|
137
|
+
allValid = false;
|
|
138
|
+
orphans.push({
|
|
139
|
+
artifact: name,
|
|
140
|
+
entry_id: entry.id || 'unknown',
|
|
141
|
+
reason: `broken link: ${link.artifact}#${link.ref}`,
|
|
142
|
+
});
|
|
143
|
+
break; // Report first broken link only
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (allValid) groundedEntries++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const gateBScore = totalEntries > 0 ? groundedEntries / totalEntries : 0;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
schema_version: '1',
|
|
155
|
+
generated: new Date().toISOString(),
|
|
156
|
+
gate_b_score: Math.round(gateBScore * 10000) / 10000, // 4 decimal places
|
|
157
|
+
total_entries: totalEntries,
|
|
158
|
+
grounded_entries: groundedEntries,
|
|
159
|
+
orphaned_entries: orphans.length,
|
|
160
|
+
orphans,
|
|
161
|
+
target: 1.0,
|
|
162
|
+
target_met: gateBScore >= 1.0,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function main() {
|
|
169
|
+
// Load L3 reasoning artifacts
|
|
170
|
+
const l3Artifacts = [];
|
|
171
|
+
|
|
172
|
+
const hazardPath = path.join(REASONING_DIR, 'hazard-model.json');
|
|
173
|
+
if (fs.existsSync(hazardPath)) {
|
|
174
|
+
const hm = JSON.parse(fs.readFileSync(hazardPath, 'utf8'));
|
|
175
|
+
l3Artifacts.push({ name: 'hazard-model.json', entries: hm.hazards || [] });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fmPath = path.join(REASONING_DIR, 'failure-mode-catalog.json');
|
|
179
|
+
if (fs.existsSync(fmPath)) {
|
|
180
|
+
const fm = JSON.parse(fs.readFileSync(fmPath, 'utf8'));
|
|
181
|
+
l3Artifacts.push({ name: 'failure-mode-catalog.json', entries: fm.failure_modes || [] });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const rhPath = path.join(REASONING_DIR, 'risk-heatmap.json');
|
|
185
|
+
if (fs.existsSync(rhPath)) {
|
|
186
|
+
const rh = JSON.parse(fs.readFileSync(rhPath, 'utf8'));
|
|
187
|
+
l3Artifacts.push({ name: 'risk-heatmap.json', entries: rh.transitions || [] });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (l3Artifacts.length === 0) {
|
|
191
|
+
console.error('ERROR: No L3 reasoning artifacts found in', REASONING_DIR);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = checkGateB(l3Artifacts);
|
|
196
|
+
|
|
197
|
+
// Write output
|
|
198
|
+
fs.mkdirSync(GATES_DIR, { recursive: true });
|
|
199
|
+
fs.writeFileSync(OUT_FILE, JSON.stringify(result, null, 2) + '\n');
|
|
200
|
+
|
|
201
|
+
// Emit check result
|
|
202
|
+
try {
|
|
203
|
+
const { writeCheckResult } = require(path.join(__dirname, 'write-check-result.cjs'));
|
|
204
|
+
writeCheckResult({
|
|
205
|
+
tool: 'gate-b-abstraction',
|
|
206
|
+
formalism: 'trace',
|
|
207
|
+
result: result.target_met ? 'pass' : 'fail',
|
|
208
|
+
check_id: 'gate-b:abstraction-score',
|
|
209
|
+
surface: 'trace',
|
|
210
|
+
property: `Gate B traceability score: ${(result.gate_b_score * 100).toFixed(1)}% (target: 100%)`,
|
|
211
|
+
runtime_ms: 0,
|
|
212
|
+
summary: `${result.target_met ? 'pass' : 'fail'}: traceability ${(result.gate_b_score * 100).toFixed(1)}%, ${result.grounded_entries}/${result.total_entries} grounded`,
|
|
213
|
+
requirement_ids: ['GATE-02'],
|
|
214
|
+
metadata: { gate_b_score: result.gate_b_score, target: result.target, target_met: result.target_met },
|
|
215
|
+
});
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// write-check-result.cjs not available; skip
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (JSON_FLAG) {
|
|
221
|
+
process.stdout.write(JSON.stringify(result));
|
|
222
|
+
} else {
|
|
223
|
+
console.log(`Gate B: L2-L3 Traceability`);
|
|
224
|
+
console.log(` Score: ${(result.gate_b_score * 100).toFixed(1)}%`);
|
|
225
|
+
console.log(` Total entries: ${result.total_entries}`);
|
|
226
|
+
console.log(` Grounded: ${result.grounded_entries}`);
|
|
227
|
+
console.log(` Orphaned: ${result.orphaned_entries}`);
|
|
228
|
+
console.log(` Target met: ${result.target_met}`);
|
|
229
|
+
if (result.orphans.length > 0) {
|
|
230
|
+
console.log(` Orphan details:`);
|
|
231
|
+
for (const o of result.orphans) {
|
|
232
|
+
console.log(` - ${o.artifact} / ${o.entry_id}: ${o.reason}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log(` Output: ${OUT_FILE}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.exit(result.target_met ? 0 : 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (require.main === module) main();
|
|
242
|
+
|
|
243
|
+
module.exports = { checkGateB, resolveL2Link, main };
|