@nforma.ai/nforma 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric threshold heuristic for P->F residual layer
|
|
3
|
+
* Determines if a formal_ref points to a numeric parameter (auto-updatable)
|
|
4
|
+
* vs a correctness invariant (requires investigation)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { parseFormalRef } = require('./extractFormalExpected.cjs');
|
|
10
|
+
const { extractFormalExpected } = require('./extractFormalExpected.cjs');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a formal_ref points to a numeric threshold/parameter
|
|
14
|
+
* @param {string} formalRef - Formal reference string
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {string} [options.specDir] - Override spec directory (for testing)
|
|
17
|
+
* @returns {boolean} true if the ref points to a numeric value
|
|
18
|
+
*/
|
|
19
|
+
function isNumericThreshold(formalRef, options = {}) {
|
|
20
|
+
const parsed = parseFormalRef(formalRef);
|
|
21
|
+
if (!parsed) return false;
|
|
22
|
+
|
|
23
|
+
// Requirements are text, not numeric thresholds
|
|
24
|
+
if (parsed.type === 'requirement') return false;
|
|
25
|
+
|
|
26
|
+
// Spec without param key = invariant reference
|
|
27
|
+
if (parsed.type === 'spec' && !parsed.param) return false;
|
|
28
|
+
|
|
29
|
+
// Try to extract the value and check if it is numeric
|
|
30
|
+
const value = extractFormalExpected(formalRef, options);
|
|
31
|
+
return typeof value === 'number';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { isNumericThreshold };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* issue-classifier.cjs
|
|
6
|
+
*
|
|
7
|
+
* Reads .planning/telemetry/report.json, ranks operational issues by severity,
|
|
8
|
+
* writes .planning/telemetry/pending-fixes.json with up to 3 prioritized issues.
|
|
9
|
+
*
|
|
10
|
+
* NEVER invokes Claude or any MCP tool. Pure disk I/O.
|
|
11
|
+
*
|
|
12
|
+
* Priority scoring:
|
|
13
|
+
* alwaysFailing server: 100 (token waste on every quorum round)
|
|
14
|
+
* circuitBreaker.active: 90 (oscillation active — blocks progress)
|
|
15
|
+
* hangCount > 5 for server: 80 (degrades quorum latency)
|
|
16
|
+
* quorumFailureRate > 0.5: 70 (majority of rounds have no quorum)
|
|
17
|
+
* slowServer with p95 > 30s: 60 (chronic latency)
|
|
18
|
+
* circuitBreaker.triggerCount > 3: 50 (repeated oscillation history)
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node bin/issue-classifier.cjs
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const PROJECT_DIR = process.cwd();
|
|
28
|
+
const TELEMETRY_DIR = path.join(PROJECT_DIR, '.planning', 'telemetry');
|
|
29
|
+
const REPORT_PATH = path.join(TELEMETRY_DIR, 'report.json');
|
|
30
|
+
const FIXES_PATH = path.join(TELEMETRY_DIR, 'pending-fixes.json');
|
|
31
|
+
|
|
32
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
33
|
+
function slug(str) {
|
|
34
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeEmpty(reason) {
|
|
38
|
+
if (reason) process.stderr.write('[issue-classifier] ' + reason + '\n');
|
|
39
|
+
fs.mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
40
|
+
const out = { generatedAt: new Date().toISOString(), issues: [] };
|
|
41
|
+
fs.writeFileSync(FIXES_PATH, JSON.stringify(out, null, 2), 'utf8');
|
|
42
|
+
console.log('[issue-classifier] pending-fixes.json written (0 issues)');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Load report.json ─────────────────────────────────────────────────────────
|
|
47
|
+
let report;
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(REPORT_PATH)) writeEmpty('report.json not found');
|
|
50
|
+
report = JSON.parse(fs.readFileSync(REPORT_PATH, 'utf8'));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
writeEmpty('Failed to parse report.json: ' + err.message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mcp = report.mcp || {};
|
|
56
|
+
const quorum = report.quorum || {};
|
|
57
|
+
const circuitBreaker = report.circuitBreaker || {};
|
|
58
|
+
|
|
59
|
+
const issues = [];
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
|
|
62
|
+
// ─── Rule 1: Always-failing servers (100 pts each) ────────────────────────────
|
|
63
|
+
for (const serverName of (mcp.alwaysFailing || [])) {
|
|
64
|
+
issues.push({
|
|
65
|
+
id: 'mcp-always-failing-' + slug(serverName),
|
|
66
|
+
priority: 100,
|
|
67
|
+
description: `MCP server "${serverName}" has never succeeded — it wastes tokens on every quorum round.`,
|
|
68
|
+
action: `Remove or disable "${serverName}" from ~/.claude.json mcpServers, or investigate its endpoint.`,
|
|
69
|
+
surfaced: false,
|
|
70
|
+
detectedAt: now,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Rule 2: Circuit breaker active (90 pts) ──────────────────────────────────
|
|
75
|
+
if (circuitBreaker.active === true) {
|
|
76
|
+
issues.push({
|
|
77
|
+
id: 'circuit-breaker-active',
|
|
78
|
+
priority: 90,
|
|
79
|
+
description: 'Circuit breaker is currently active — oscillation was detected and execution is paused.',
|
|
80
|
+
action: 'Run /qgsd:debug to diagnose the oscillation root cause, then run `npx qgsd --reset-breaker`.',
|
|
81
|
+
surfaced: false,
|
|
82
|
+
detectedAt: now,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Rule 3: High hang count (80 pts) ─────────────────────────────────────────
|
|
87
|
+
for (const [serverName, stats] of Object.entries(mcp.servers || {})) {
|
|
88
|
+
if ((stats.hangCount || 0) > 5) {
|
|
89
|
+
issues.push({
|
|
90
|
+
id: 'mcp-high-hangs-' + slug(serverName),
|
|
91
|
+
priority: 80,
|
|
92
|
+
description: `MCP server "${serverName}" hung ${stats.hangCount} times (>60s) — it degrades quorum latency significantly.`,
|
|
93
|
+
action: `Raise CLAUDE_MCP_TIMEOUT_MS or switch "${serverName}" to a faster provider in ~/.claude.json.`,
|
|
94
|
+
surfaced: false,
|
|
95
|
+
detectedAt: now,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Rule 4: High quorum failure rate (70 pts) ───────────────────────────────
|
|
101
|
+
if ((quorum.quorumFailureRate || 0) > 0.5 && (quorum.totalRounds || 0) > 0) {
|
|
102
|
+
const pct = Math.round(quorum.quorumFailureRate * 100);
|
|
103
|
+
issues.push({
|
|
104
|
+
id: 'quorum-high-failure-rate',
|
|
105
|
+
priority: 70,
|
|
106
|
+
description: `${pct}% of quorum rounds had no available external models — consensus quality is severely degraded.`,
|
|
107
|
+
action: 'Check provider API keys and quotas; run `node bin/check-mcp-health.cjs` to identify unavailable models.',
|
|
108
|
+
surfaced: false,
|
|
109
|
+
detectedAt: now,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Rule 5: Slow server p95 > 30s (60 pts) ──────────────────────────────────
|
|
114
|
+
for (const slow of (mcp.slowServers || [])) {
|
|
115
|
+
if ((slow.p95Ms || 0) > 30000) {
|
|
116
|
+
const p95s = (slow.p95Ms / 1000).toFixed(0);
|
|
117
|
+
issues.push({
|
|
118
|
+
id: 'mcp-slow-server-' + slug(slow.name),
|
|
119
|
+
priority: 60,
|
|
120
|
+
description: `MCP server "${slow.name}" has p95 latency of ${p95s}s — it chronically slows quorum rounds.`,
|
|
121
|
+
action: `Raise CLAUDE_MCP_TIMEOUT_MS or route "${slow.name}" via a faster provider endpoint.`,
|
|
122
|
+
surfaced: false,
|
|
123
|
+
detectedAt: now,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Rule 6: Repeated circuit breaker triggers (50 pts) ──────────────────────
|
|
129
|
+
if (!circuitBreaker.active && (circuitBreaker.triggerCount || 0) > 3) {
|
|
130
|
+
issues.push({
|
|
131
|
+
id: 'circuit-breaker-repeated-triggers',
|
|
132
|
+
priority: 50,
|
|
133
|
+
description: `Circuit breaker has triggered ${circuitBreaker.triggerCount} times — recurring oscillation pattern detected.`,
|
|
134
|
+
action: 'Run /qgsd:discuss-phase to review recent commit patterns; consider adding explicit done-criteria to plans.',
|
|
135
|
+
surfaced: false,
|
|
136
|
+
detectedAt: now,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Sort by priority desc, take top 3 ───────────────────────────────────────
|
|
141
|
+
issues.sort((a, b) => b.priority - a.priority);
|
|
142
|
+
const top3 = issues.slice(0, 3);
|
|
143
|
+
|
|
144
|
+
// ─── Write output ─────────────────────────────────────────────────────────────
|
|
145
|
+
fs.mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
146
|
+
const output = {
|
|
147
|
+
generatedAt: now,
|
|
148
|
+
issues: top3,
|
|
149
|
+
};
|
|
150
|
+
fs.writeFileSync(FIXES_PATH, JSON.stringify(output, null, 2), 'utf8');
|
|
151
|
+
console.log('[issue-classifier] pending-fixes.json written (' + top3.length + ' issues)');
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Levenshtein distance and similarity functions
|
|
3
|
+
* Used by the dedup engine for near-duplicate detection on debt entry titles
|
|
4
|
+
*
|
|
5
|
+
* Algorithm: Wagner-Fischer with two-row space optimization — O(m*n) time, O(min(m,n)) space
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compute Levenshtein edit distance between two strings
|
|
10
|
+
* @param {string} a - First string
|
|
11
|
+
* @param {string} b - Second string
|
|
12
|
+
* @returns {number} Integer edit distance
|
|
13
|
+
*/
|
|
14
|
+
function levenshteinDistance(a, b) {
|
|
15
|
+
// Early exits
|
|
16
|
+
if (a === b) return 0;
|
|
17
|
+
if (a.length === 0) return b.length;
|
|
18
|
+
if (b.length === 0) return a.length;
|
|
19
|
+
|
|
20
|
+
// Ensure a is the shorter string for O(min(m,n)) space
|
|
21
|
+
if (a.length > b.length) {
|
|
22
|
+
[a, b] = [b, a];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const m = a.length;
|
|
26
|
+
const n = b.length;
|
|
27
|
+
|
|
28
|
+
// Two-row optimization: only keep previous and current row
|
|
29
|
+
let prev = new Array(m + 1);
|
|
30
|
+
let curr = new Array(m + 1);
|
|
31
|
+
|
|
32
|
+
// Initialize first row: distance from empty string to a[0..i]
|
|
33
|
+
for (let i = 0; i <= m; i++) {
|
|
34
|
+
prev[i] = i;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fill matrix row by row
|
|
38
|
+
for (let j = 1; j <= n; j++) {
|
|
39
|
+
curr[0] = j; // distance from b[0..j] to empty string
|
|
40
|
+
|
|
41
|
+
for (let i = 1; i <= m; i++) {
|
|
42
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
43
|
+
curr[i] = Math.min(
|
|
44
|
+
prev[i] + 1, // deletion
|
|
45
|
+
curr[i - 1] + 1, // insertion
|
|
46
|
+
prev[i - 1] + cost // substitution
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Swap rows
|
|
51
|
+
[prev, curr] = [curr, prev];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Result is in prev (after swap) at position m
|
|
55
|
+
return prev[m];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compute normalized Levenshtein similarity between two strings
|
|
60
|
+
* @param {string} a - First string
|
|
61
|
+
* @param {string} b - Second string
|
|
62
|
+
* @returns {number} Similarity score between 0.0 (completely different) and 1.0 (identical)
|
|
63
|
+
*/
|
|
64
|
+
function levenshteinSimilarity(a, b) {
|
|
65
|
+
// Both empty = identical
|
|
66
|
+
if (a.length === 0 && b.length === 0) return 1.0;
|
|
67
|
+
|
|
68
|
+
const maxLen = Math.max(a.length, b.length);
|
|
69
|
+
const distance = levenshteinDistance(a, b);
|
|
70
|
+
|
|
71
|
+
return 1 - (distance / maxLen);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { levenshteinDistance, levenshteinSimilarity };
|