@nforma.ai/nforma 0.2.1 → 0.29.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-preflight.cjs +89 -0
- 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 +36 -86
- 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/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +6 -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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Requirements: SIG-01
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
8
|
-
// node bin/detect-coverage-gaps.cjs [--spec=
|
|
8
|
+
// node bin/detect-coverage-gaps.cjs [--spec=NFQuorum] [--log=path]
|
|
9
9
|
//
|
|
10
10
|
// Output:
|
|
11
11
|
// .planning/formal/coverage-gaps.md — structured test backlog when gaps exist
|
|
@@ -19,15 +19,15 @@ const path = require('path');
|
|
|
19
19
|
// Maps each TLA+ spec to its state variable name and value-to-name mapping.
|
|
20
20
|
// Source of truth: state comments in the TLA+ spec files.
|
|
21
21
|
const STATE_MAPS = {
|
|
22
|
-
'
|
|
22
|
+
'NFQuorum': {
|
|
23
23
|
variable: 's',
|
|
24
24
|
values: { '0': 'COLLECTING_VOTES', '1': 'DECIDED', '2': 'DELIBERATING' },
|
|
25
25
|
},
|
|
26
|
-
'
|
|
26
|
+
'NFStopHook': {
|
|
27
27
|
variable: 'phase',
|
|
28
28
|
values: { '0': 'IDLE', '1': 'READING', '2': 'DECIDING', '3': 'BLOCKED' },
|
|
29
29
|
},
|
|
30
|
-
'
|
|
30
|
+
'NFCircuitBreaker': {
|
|
31
31
|
variable: 'state',
|
|
32
32
|
values: { '0': 'MONITORING', '1': 'TRIGGERED', '2': 'RECOVERING' },
|
|
33
33
|
},
|
|
@@ -51,7 +51,7 @@ const ACTION_TO_STATE = {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* parseTlcStates(specName) — returns the full set of named states for a spec.
|
|
54
|
-
* @param {string} specName - TLA+ spec name (e.g. '
|
|
54
|
+
* @param {string} specName - TLA+ spec name (e.g. 'NFQuorum')
|
|
55
55
|
* @returns {{ specName: string, states: Set<string>, variable: string } | null}
|
|
56
56
|
*/
|
|
57
57
|
function parseTlcStates(specName) {
|
|
@@ -102,7 +102,7 @@ function parseTraceStates(logPath) {
|
|
|
102
102
|
* @returns {{ status: string, gaps?: string[], outputPath?: string, reason?: string }}
|
|
103
103
|
*/
|
|
104
104
|
function detectCoverageGaps(options = {}) {
|
|
105
|
-
const specName = options.specName || '
|
|
105
|
+
const specName = options.specName || 'NFQuorum';
|
|
106
106
|
const pp2 = require('./planning-paths.cjs');
|
|
107
107
|
const logPath = options.logPath || pp2.resolveWithFallback(process.cwd(), 'conformance-events');
|
|
108
108
|
const outputPath = options.outputPath || path.join(process.cwd(), '.planning', 'formal', 'coverage-gaps.md');
|
|
@@ -186,7 +186,7 @@ if (require.main === module) {
|
|
|
186
186
|
const specArg = args.find(a => a.startsWith('--spec='));
|
|
187
187
|
const logArg = args.find(a => a.startsWith('--log='));
|
|
188
188
|
|
|
189
|
-
const specName = specArg ? specArg.split('=')[1] : '
|
|
189
|
+
const specName = specArg ? specArg.split('=')[1] : 'NFQuorum';
|
|
190
190
|
const logPath = logArg ? logArg.split('=')[1] : undefined;
|
|
191
191
|
|
|
192
192
|
const result = detectCoverageGaps({ specName, logPath });
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* failure-mode-catalog.cjs — Failure mode enumeration for Layer 3 (Reasoning).
|
|
6
|
+
*
|
|
7
|
+
* Enumerates concrete failure modes (omission, commission, corruption) per
|
|
8
|
+
* L2 state-event pair. Uses hazard-model.json severity scores for corruption
|
|
9
|
+
* thresholds and observed-fsm.json model_comparison for commission conditions.
|
|
10
|
+
*
|
|
11
|
+
* Requirements: RSN-02
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node bin/failure-mode-catalog.cjs # print summary to stdout
|
|
15
|
+
* node bin/failure-mode-catalog.cjs --json # print full results JSON to stdout
|
|
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 REASONING_DIR = path.join(FORMAL, 'reasoning');
|
|
24
|
+
const OUT_FILE = path.join(REASONING_DIR, 'failure-mode-catalog.json');
|
|
25
|
+
|
|
26
|
+
const JSON_FLAG = process.argv.includes('--json');
|
|
27
|
+
|
|
28
|
+
// ── Severity class mapping ──────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function classifySeverity(severity, mode) {
|
|
31
|
+
if (mode === 'commission') return 'model_gap';
|
|
32
|
+
if (severity >= 8) return 'critical';
|
|
33
|
+
if (severity >= 6) return 'stalled';
|
|
34
|
+
if (severity >= 4) return 'degraded';
|
|
35
|
+
if (severity >= 2) return 'cosmetic';
|
|
36
|
+
return 'cosmetic';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Failure mode descriptions ───────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function omissionDescription(fromState, event, toState) {
|
|
42
|
+
return `Transition ${fromState} --[${event}]--> ${toState} does not fire when expected`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function omissionEffect(fromState) {
|
|
46
|
+
return `System stays in ${fromState}; expected state change does not occur`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function commissionDescription(fromState, event, toState) {
|
|
50
|
+
return `Transition ${fromState} --[${event}]--> ${toState} fires but is not modeled in XState`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function commissionEffect(fromState, event, toState) {
|
|
54
|
+
return `Unmodeled state transition from ${fromState} to ${toState} on ${event}; behavior diverges from spec`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function corruptionDescription(fromState, event, toState) {
|
|
58
|
+
return `Transition ${fromState} --[${event}]--> fires but produces wrong target state (not ${toState})`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function corruptionEffect(fromState, toState) {
|
|
62
|
+
return `System enters incorrect state instead of ${toState}; downstream behavior undefined`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Mismatch enrichment ─────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function findMismatches(fromState, event, mismatches) {
|
|
68
|
+
return mismatches.filter(m => {
|
|
69
|
+
// Mismatches have expected/actual states; match if the event context aligns
|
|
70
|
+
// Since mismatches don't have from/event fields directly, we match on state overlap
|
|
71
|
+
return m.expected_state === fromState || m.actual_state === fromState;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Core enumeration ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function enumerateFailureModes(observedFsm, hazardModel, mismatches) {
|
|
78
|
+
const missingInModel = new Set(
|
|
79
|
+
(observedFsm.model_comparison?.missing_in_model || [])
|
|
80
|
+
.map(m => `${m.from}-${m.event}`)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Build severity lookup from hazard model
|
|
84
|
+
const severityMap = {};
|
|
85
|
+
for (const h of (hazardModel?.hazards || [])) {
|
|
86
|
+
severityMap[`${h.state}-${h.event}`] = h.severity;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const failureModes = [];
|
|
90
|
+
|
|
91
|
+
for (const [fromState, events] of Object.entries(observedFsm.observed_transitions)) {
|
|
92
|
+
for (const [event, data] of Object.entries(events)) {
|
|
93
|
+
const key = `${fromState}-${event}`;
|
|
94
|
+
const severity = severityMap[key] || 4;
|
|
95
|
+
const toState = data.to_state;
|
|
96
|
+
const relMismatches = findMismatches(fromState, event, mismatches);
|
|
97
|
+
const mismatchNote = relMismatches.length > 0
|
|
98
|
+
? ` (${relMismatches.length} observed mismatch(es): ${relMismatches.map(m => m.id).join(', ')})`
|
|
99
|
+
: '';
|
|
100
|
+
|
|
101
|
+
// 1. Omission (always)
|
|
102
|
+
failureModes.push({
|
|
103
|
+
id: `FM-${fromState}-${event}-OMISSION`,
|
|
104
|
+
state: fromState,
|
|
105
|
+
event,
|
|
106
|
+
to_state: toState,
|
|
107
|
+
failure_mode: 'omission',
|
|
108
|
+
description: omissionDescription(fromState, event, toState) + mismatchNote,
|
|
109
|
+
effect: omissionEffect(fromState),
|
|
110
|
+
severity_class: classifySeverity(severity, 'omission'),
|
|
111
|
+
derived_from: [
|
|
112
|
+
{ layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `observed_transitions.${fromState}.${event}` },
|
|
113
|
+
{ layer: 'L3', artifact: 'reasoning/hazard-model.json', ref: `hazards[id=HAZARD-${fromState}-${event}]` },
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 2. Commission (conditional: only if in missing_in_model)
|
|
118
|
+
if (missingInModel.has(key)) {
|
|
119
|
+
failureModes.push({
|
|
120
|
+
id: `FM-${fromState}-${event}-COMMISSION`,
|
|
121
|
+
state: fromState,
|
|
122
|
+
event,
|
|
123
|
+
to_state: toState,
|
|
124
|
+
failure_mode: 'commission',
|
|
125
|
+
description: commissionDescription(fromState, event, toState) + mismatchNote,
|
|
126
|
+
effect: commissionEffect(fromState, event, toState),
|
|
127
|
+
severity_class: classifySeverity(severity, 'commission'),
|
|
128
|
+
derived_from: [
|
|
129
|
+
{ layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `model_comparison.missing_in_model` },
|
|
130
|
+
{ layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `observed_transitions.${fromState}.${event}` },
|
|
131
|
+
{ layer: 'L3', artifact: 'reasoning/hazard-model.json', ref: `hazards[id=HAZARD-${fromState}-${event}]` },
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. Corruption (conditional: only if severity >= 6)
|
|
137
|
+
if (severity >= 6) {
|
|
138
|
+
failureModes.push({
|
|
139
|
+
id: `FM-${fromState}-${event}-CORRUPTION`,
|
|
140
|
+
state: fromState,
|
|
141
|
+
event,
|
|
142
|
+
to_state: toState,
|
|
143
|
+
failure_mode: 'corruption',
|
|
144
|
+
description: corruptionDescription(fromState, event, toState) + mismatchNote,
|
|
145
|
+
effect: corruptionEffect(fromState, toState),
|
|
146
|
+
severity_class: classifySeverity(severity, 'corruption'),
|
|
147
|
+
derived_from: [
|
|
148
|
+
{ layer: 'L2', artifact: 'semantics/observed-fsm.json', ref: `observed_transitions.${fromState}.${event}` },
|
|
149
|
+
{ layer: 'L3', artifact: 'reasoning/hazard-model.json', ref: `hazards[id=HAZARD-${fromState}-${event}]` },
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Count by mode and severity class
|
|
157
|
+
const byMode = { omission: 0, commission: 0, corruption: 0 };
|
|
158
|
+
const bySeverityClass = {};
|
|
159
|
+
for (const fm of failureModes) {
|
|
160
|
+
byMode[fm.failure_mode] = (byMode[fm.failure_mode] || 0) + 1;
|
|
161
|
+
bySeverityClass[fm.severity_class] = (bySeverityClass[fm.severity_class] || 0) + 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
schema_version: '1',
|
|
166
|
+
generated: new Date().toISOString(),
|
|
167
|
+
failure_modes: failureModes,
|
|
168
|
+
summary: {
|
|
169
|
+
total: failureModes.length,
|
|
170
|
+
by_mode: byMode,
|
|
171
|
+
by_severity_class: bySeverityClass,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function main() {
|
|
179
|
+
// Load L2 observed FSM
|
|
180
|
+
const fsmPath = path.join(FORMAL, 'semantics', 'observed-fsm.json');
|
|
181
|
+
if (!fs.existsSync(fsmPath)) {
|
|
182
|
+
console.error('ERROR: observed-fsm.json not found at', fsmPath);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
const observedFsm = JSON.parse(fs.readFileSync(fsmPath, 'utf8'));
|
|
186
|
+
|
|
187
|
+
// Load L3 hazard model (produced by hazard-model.cjs)
|
|
188
|
+
const hazardPath = path.join(REASONING_DIR, 'hazard-model.json');
|
|
189
|
+
if (!fs.existsSync(hazardPath)) {
|
|
190
|
+
console.error('ERROR: hazard-model.json not found at', hazardPath);
|
|
191
|
+
console.error('Run bin/hazard-model.cjs first.');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
const hazardModel = JSON.parse(fs.readFileSync(hazardPath, 'utf8'));
|
|
195
|
+
|
|
196
|
+
// Load mismatch register
|
|
197
|
+
const mismatchPath = path.join(FORMAL, 'semantics', 'mismatch-register.jsonl');
|
|
198
|
+
let mismatches = [];
|
|
199
|
+
if (fs.existsSync(mismatchPath)) {
|
|
200
|
+
mismatches = fs.readFileSync(mismatchPath, 'utf8')
|
|
201
|
+
.trim().split('\n')
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.map(line => JSON.parse(line));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const output = enumerateFailureModes(observedFsm, hazardModel, mismatches);
|
|
207
|
+
|
|
208
|
+
// Write output
|
|
209
|
+
fs.mkdirSync(REASONING_DIR, { recursive: true });
|
|
210
|
+
fs.writeFileSync(OUT_FILE, JSON.stringify(output, null, 2) + '\n');
|
|
211
|
+
|
|
212
|
+
if (JSON_FLAG) {
|
|
213
|
+
process.stdout.write(JSON.stringify(output));
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`Failure Mode Catalog`);
|
|
216
|
+
console.log(` Total failure modes: ${output.summary.total}`);
|
|
217
|
+
console.log(` By mode: ${JSON.stringify(output.summary.by_mode)}`);
|
|
218
|
+
console.log(` By severity class: ${JSON.stringify(output.summary.by_severity_class)}`);
|
|
219
|
+
console.log(` Output: ${OUT_FILE}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (require.main === module) main();
|
|
226
|
+
|
|
227
|
+
module.exports = { enumerateFailureModes, classifySeverity };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/failure-taxonomy.cjs
|
|
4
|
+
// Classifies check-result failures into 5 categories:
|
|
5
|
+
// crash, timeout, logic_violation, drift, degradation
|
|
6
|
+
//
|
|
7
|
+
// Requirement: EVID-03
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
|
|
13
|
+
const EVIDENCE_DIR = path.join(ROOT, '.planning', 'formal', 'evidence');
|
|
14
|
+
const CHECK_RESULTS_PATH = path.join(ROOT, '.planning', 'formal', 'check-results.ndjson');
|
|
15
|
+
const DEBT_PATH = path.join(ROOT, '.planning', 'formal', 'debt.json');
|
|
16
|
+
const OUTPUT_PATH = path.join(EVIDENCE_DIR, 'failure-taxonomy.json');
|
|
17
|
+
|
|
18
|
+
const JSON_FLAG = process.argv.includes('--json');
|
|
19
|
+
|
|
20
|
+
// Timeout threshold in ms (> 60 seconds suggests TLC state explosion)
|
|
21
|
+
const TIMEOUT_THRESHOLD_MS = 60000;
|
|
22
|
+
|
|
23
|
+
// ── Classification rules ────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Classify a check-result failure into exactly one category.
|
|
27
|
+
* Decision rules per research Pitfall 5:
|
|
28
|
+
*
|
|
29
|
+
* - crash: formalism tool itself crashed (non-zero exit, no structured result, stack trace)
|
|
30
|
+
* - timeout: runtime_ms > threshold AND result=fail (TLC state explosion, model checker timeout)
|
|
31
|
+
* - logic_violation: TLC/Alloy counterexample, assertion failure
|
|
32
|
+
* - drift: validate-traces reports divergence (formalism "trace" with divergence count)
|
|
33
|
+
* - degradation: metrics trending worse (reserved; falls back to logic_violation when no baseline)
|
|
34
|
+
*/
|
|
35
|
+
function classifyFailure(entry) {
|
|
36
|
+
// Check for crash indicators
|
|
37
|
+
if (entry.metadata && entry.metadata.stack_trace) {
|
|
38
|
+
return { category: 'crash', reason: 'Stack trace present in metadata' };
|
|
39
|
+
}
|
|
40
|
+
if (entry.summary && /error|crash|exception|ENOENT|spawn/i.test(entry.summary) &&
|
|
41
|
+
!/counterexample|divergence|fail:/i.test(entry.summary)) {
|
|
42
|
+
return { category: 'crash', reason: `Error pattern in summary: ${entry.summary.substring(0, 80)}` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check for timeout
|
|
46
|
+
if (entry.runtime_ms && entry.runtime_ms > TIMEOUT_THRESHOLD_MS) {
|
|
47
|
+
return { category: 'timeout', reason: `runtime_ms=${entry.runtime_ms} exceeds ${TIMEOUT_THRESHOLD_MS}ms threshold` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for drift (trace formalism with divergences)
|
|
51
|
+
if (entry.formalism === 'trace') {
|
|
52
|
+
const divMatch = entry.summary && entry.summary.match(/(\d+)\s+divergence/);
|
|
53
|
+
if (divMatch) {
|
|
54
|
+
return { category: 'drift', reason: `Trace divergence: ${divMatch[1]} divergence(s) detected` };
|
|
55
|
+
}
|
|
56
|
+
return { category: 'drift', reason: 'Trace formalism failure (drift)' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Degradation: reserved, falls back to logic_violation when no baseline
|
|
60
|
+
// (no baseline data exists in current codebase)
|
|
61
|
+
|
|
62
|
+
// Default: logic_violation (counterexample, assertion failure, etc.)
|
|
63
|
+
return { category: 'logic_violation', reason: entry.summary
|
|
64
|
+
? `Logic violation: ${entry.summary.substring(0, 100)}`
|
|
65
|
+
: 'Logic violation: check-result failure without specific categorization' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function main() {
|
|
71
|
+
if (!fs.existsSync(EVIDENCE_DIR)) {
|
|
72
|
+
fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Read check-results.ndjson
|
|
76
|
+
const failures = [];
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(CHECK_RESULTS_PATH)) {
|
|
79
|
+
const lines = fs.readFileSync(CHECK_RESULTS_PATH, 'utf8').split('\n').filter(l => l.trim());
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
try {
|
|
82
|
+
const entry = JSON.parse(line);
|
|
83
|
+
if (entry.result === 'fail') {
|
|
84
|
+
failures.push(entry);
|
|
85
|
+
}
|
|
86
|
+
} catch (_) {}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Read debt.json for additional failure sources
|
|
91
|
+
if (fs.existsSync(DEBT_PATH)) {
|
|
92
|
+
try {
|
|
93
|
+
const debt = JSON.parse(fs.readFileSync(DEBT_PATH, 'utf8'));
|
|
94
|
+
if (Array.isArray(debt.debt_entries)) {
|
|
95
|
+
for (const entry of debt.debt_entries) {
|
|
96
|
+
failures.push({
|
|
97
|
+
...entry,
|
|
98
|
+
result: 'fail',
|
|
99
|
+
formalism: entry.formalism || 'unknown',
|
|
100
|
+
tool: entry.tool || 'debt-entry',
|
|
101
|
+
summary: entry.summary || entry.description || 'Debt entry',
|
|
102
|
+
timestamp: entry.timestamp || debt.last_updated,
|
|
103
|
+
_source: 'debt.json',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (_) {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Classify each failure
|
|
111
|
+
const categories = {
|
|
112
|
+
crash: [],
|
|
113
|
+
timeout: [],
|
|
114
|
+
logic_violation: [],
|
|
115
|
+
drift: [],
|
|
116
|
+
degradation: [],
|
|
117
|
+
};
|
|
118
|
+
const unclassified = [];
|
|
119
|
+
|
|
120
|
+
for (const failure of failures) {
|
|
121
|
+
const { category, reason } = classifyFailure(failure);
|
|
122
|
+
const classified = { ...failure, category, classification_reason: reason };
|
|
123
|
+
|
|
124
|
+
if (category in categories) {
|
|
125
|
+
categories[category].push(classified);
|
|
126
|
+
} else {
|
|
127
|
+
unclassified.push(classified);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Count per category
|
|
132
|
+
const categoryCounts = {};
|
|
133
|
+
for (const [cat, entries] of Object.entries(categories)) {
|
|
134
|
+
categoryCounts[cat] = entries.length;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Build result
|
|
138
|
+
const result = {
|
|
139
|
+
schema_version: '1',
|
|
140
|
+
generated: new Date().toISOString(),
|
|
141
|
+
total_failures: failures.length,
|
|
142
|
+
categories,
|
|
143
|
+
category_counts: categoryCounts,
|
|
144
|
+
degradation_fallback_note: 'When no baseline exists, potential degradation entries are classified as logic_violation',
|
|
145
|
+
unclassified,
|
|
146
|
+
summary: `${failures.length} failures classified: ` +
|
|
147
|
+
Object.entries(categoryCounts).map(([k, v]) => `${k}=${v}`).join(', ') +
|
|
148
|
+
`. ${unclassified.length} unclassified.`,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(result, null, 2) + '\n', 'utf8');
|
|
152
|
+
|
|
153
|
+
if (JSON_FLAG) {
|
|
154
|
+
console.log(JSON.stringify(result, null, 2));
|
|
155
|
+
} else {
|
|
156
|
+
console.log('Failure Taxonomy Generated');
|
|
157
|
+
console.log(` Total failures: ${failures.length}`);
|
|
158
|
+
for (const [cat, count] of Object.entries(categoryCounts)) {
|
|
159
|
+
console.log(` ${cat}: ${count}`);
|
|
160
|
+
}
|
|
161
|
+
if (unclassified.length > 0) {
|
|
162
|
+
console.log(` UNCLASSIFIED: ${unclassified.length}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Exit 1 if any unclassified
|
|
167
|
+
if (unclassified.length > 0) {
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Export for testing
|
|
173
|
+
module.exports = { classifyFailure, TIMEOUT_THRESHOLD_MS };
|
|
174
|
+
|
|
175
|
+
if (require.main === module) {
|
|
176
|
+
main();
|
|
177
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const SPEC_DIR = path.join(process.cwd(), '.planning', 'formal', 'spec');
|
|
8
|
+
|
|
9
|
+
function printHelp() {
|
|
10
|
+
console.log(`Usage: node bin/formal-scope-scan.cjs --description "text" [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--description "text" Description to match against (required)
|
|
14
|
+
--files file1,file2 Source files to check for overlap (optional)
|
|
15
|
+
--format json|lines Output format (default: json)
|
|
16
|
+
--help Show this help message
|
|
17
|
+
|
|
18
|
+
Matching algorithm (any signal fires):
|
|
19
|
+
1. Source file overlap: --files matched against module source_files globs
|
|
20
|
+
2. Concept matching: exact token match against curated concepts
|
|
21
|
+
3. Module name match: exact token match against module directory name
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
node bin/formal-scope-scan.cjs --description "fix quorum deliberation bug"
|
|
25
|
+
node bin/formal-scope-scan.cjs --description "update breaker" --format lines
|
|
26
|
+
node bin/formal-scope-scan.cjs --files "hooks/nf-stop.js" --description "something"
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = { description: '', files: [], format: 'json', help: false };
|
|
32
|
+
for (let i = 2; i < argv.length; i++) {
|
|
33
|
+
if (argv[i] === '--help' || argv[i] === '-h') {
|
|
34
|
+
args.help = true;
|
|
35
|
+
} else if (argv[i] === '--description' && argv[i + 1]) {
|
|
36
|
+
args.description = argv[++i];
|
|
37
|
+
} else if (argv[i] === '--files' && argv[i + 1]) {
|
|
38
|
+
args.files = argv[++i].split(',').map(f => f.trim()).filter(Boolean);
|
|
39
|
+
} else if (argv[i] === '--format' && argv[i + 1]) {
|
|
40
|
+
args.format = argv[++i];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return args;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function globToRegex(glob) {
|
|
47
|
+
let regex = '';
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < glob.length) {
|
|
50
|
+
if (glob[i] === '*' && glob[i + 1] === '*') {
|
|
51
|
+
regex += '.*';
|
|
52
|
+
i += 2;
|
|
53
|
+
if (glob[i] === '/') i++; // skip trailing slash after **
|
|
54
|
+
} else if (glob[i] === '*') {
|
|
55
|
+
regex += '[^/]*';
|
|
56
|
+
i++;
|
|
57
|
+
} else if (glob[i] === '?') {
|
|
58
|
+
regex += '[^/]';
|
|
59
|
+
i++;
|
|
60
|
+
} else if ('.+^${}()|[]\\'.includes(glob[i])) {
|
|
61
|
+
regex += '\\' + glob[i];
|
|
62
|
+
i++;
|
|
63
|
+
} else {
|
|
64
|
+
regex += glob[i];
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return new RegExp('^' + regex + '$');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function matchesSourceFiles(providedFiles, moduleSourceFiles) {
|
|
72
|
+
for (const pf of providedFiles) {
|
|
73
|
+
for (const sf of moduleSourceFiles) {
|
|
74
|
+
const re = globToRegex(sf);
|
|
75
|
+
if (re.test(pf)) return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function matchesConcepts(descLower, tokens, concepts) {
|
|
82
|
+
for (const concept of concepts) {
|
|
83
|
+
const conceptLower = concept.toLowerCase();
|
|
84
|
+
// Exact token match
|
|
85
|
+
if (tokens.includes(conceptLower)) return true;
|
|
86
|
+
// Multi-word concept substring match against raw description
|
|
87
|
+
if (conceptLower.includes('-') || conceptLower.includes(' ')) {
|
|
88
|
+
if (descLower.includes(conceptLower)) return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function matchesModuleName(tokens, moduleName) {
|
|
95
|
+
return tokens.includes(moduleName.toLowerCase());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function main() {
|
|
99
|
+
const args = parseArgs(process.argv);
|
|
100
|
+
|
|
101
|
+
if (args.help) {
|
|
102
|
+
printHelp();
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!args.description) {
|
|
107
|
+
console.error('Error: --description is required');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fail-open: if spec dir doesn't exist, output empty
|
|
112
|
+
if (!fs.existsSync(SPEC_DIR)) {
|
|
113
|
+
if (args.format === 'lines') {
|
|
114
|
+
// no output
|
|
115
|
+
} else {
|
|
116
|
+
console.log('[]');
|
|
117
|
+
}
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const descLower = args.description.toLowerCase();
|
|
122
|
+
const tokens = descLower.split(/[\s\-_]+/).filter(t => t.length > 0);
|
|
123
|
+
|
|
124
|
+
const modules = fs.readdirSync(SPEC_DIR, { withFileTypes: true })
|
|
125
|
+
.filter(d => d.isDirectory())
|
|
126
|
+
.map(d => d.name);
|
|
127
|
+
|
|
128
|
+
const matches = [];
|
|
129
|
+
|
|
130
|
+
for (const mod of modules) {
|
|
131
|
+
const scopePath = path.join(SPEC_DIR, mod, 'scope.json');
|
|
132
|
+
if (!fs.existsSync(scopePath)) {
|
|
133
|
+
process.stderr.write(`Warning: ${scopePath} not found, skipping module ${mod}\n`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let scope;
|
|
138
|
+
try {
|
|
139
|
+
scope = JSON.parse(fs.readFileSync(scopePath, 'utf8'));
|
|
140
|
+
} catch (e) {
|
|
141
|
+
process.stderr.write(`Warning: Failed to parse ${scopePath}: ${e.message}\n`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const invariantsPath = `.planning/formal/spec/${mod}/invariants.md`;
|
|
146
|
+
let matchedBy = null;
|
|
147
|
+
|
|
148
|
+
// Priority 1: Source file overlap
|
|
149
|
+
if (args.files.length > 0 && scope.source_files && matchesSourceFiles(args.files, scope.source_files)) {
|
|
150
|
+
matchedBy = 'source_file';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Priority 2: Concept matching
|
|
154
|
+
if (!matchedBy && scope.concepts && matchesConcepts(descLower, tokens, scope.concepts)) {
|
|
155
|
+
matchedBy = 'concept';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Priority 3: Module name match (exact token only)
|
|
159
|
+
if (!matchedBy && matchesModuleName(tokens, mod)) {
|
|
160
|
+
matchedBy = 'module_name';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (matchedBy) {
|
|
164
|
+
matches.push({ module: mod, path: invariantsPath, matched_by: matchedBy });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (args.format === 'lines') {
|
|
169
|
+
for (const m of matches) {
|
|
170
|
+
console.log(`${m.module}\t${m.path}`);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
console.log(JSON.stringify(matches, null, 2));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
main();
|