feat: remove formal/informal LDA analysis from the bicorder (v1.3.0)

The two-family cluster classification is a research finding, not a
diagnostic; embedding it in the tool caused recurring bugs:

- ascii_bicorder.py had an inverted LDA sign (institutional mapped to 9,
  not 1) and a stale term check ('bureaucratic') that silently disabled
  the calculation after the Feb 2026 rename — output was always null
- The web app and Python script had divergent sign conventions and
  divergent version-mismatch behavior (skip vs. continue with stale model)

Changes:
- bicorder.json: drop the formal/informal analysis gradient; version 1.3.0
- ascii_bicorder.py: remove all LDA/model machinery; keep hardness and
  polarization as the only automated analyses
- App.svelte: remove classifier import, model constant, LDA calculation,
  and form-recommendation reactive block; dispatch automated analyses by
  term_left instead of array index
- Delete bicorder-classifier.ts and FormRecommendation.svelte (the latter
  was imported but never rendered)
- AnalysisTransitionBanner: remove recommendation alert and prop; fix
  hardcoded index checks that referenced the removed gradient
- vite.config.ts / vite-env.d.ts: stop loading bicorder_model.json

The cluster classifier lives on as research in analysis/ (scripts and
model untouched there). bicorder.txt regenerated.
This commit is contained in:
Protocolbot committed 2026-09-23 07:58:11 -06:00
1 parent fb3bebcea0
commit 8c40ca076b
9 files changed
+67 -1040

No files matched your search

+4 -106
View File
@@ -6,93 +6,9 @@ Generate bicorder.txt from bicorder.json
import json
import argparse
import sys
import os
from pathlib import Path
# Simple version-based approach
#
# The model includes a 'bicorder_version' field indicating which version of
# bicorder.json it was trained on. The code checks that versions match before
# calculating. This ensures the gradient structure is compatible.
#
# When bicorder.json changes (gradients added/removed/reordered), update the
# version number and retrain the model.
def load_classifier_model():
"""Load the LDA model from bicorder_model.json"""
# Try to find the model file
script_dir = Path(__file__).parent
model_paths = [
script_dir / 'analysis' / 'bicorder_model.json',
script_dir / 'bicorder_model.json',
Path('analysis/bicorder_model.json'),
Path('bicorder_model.json'),
]
for path in model_paths:
if path.exists():
with open(path, 'r') as f:
return json.load(f)
return None
def calculate_lda_score(values_array, model):
"""
Calculate LDA score from an array of values using the model.
Args:
values_array: list of 23 values (1-9) in the order expected by the model
model: loaded classifier model
Returns:
LDA score (float), or None if insufficient data
"""
if model is None:
return None
if len(values_array) != len(model['dimensions']):
return None
# Standardize using model scaler
mean = model['scaler']['mean']
scale = model['scaler']['scale']
scaled = [(values_array[i] - mean[i]) / scale[i] for i in range(len(values_array))]
# Calculate LDA score: coef · x + intercept
coef = model['lda']['coefficients']
intercept = model['lda']['intercept']
# Dot product
lda_score = sum(coef[i] * scaled[i] for i in range(len(scaled))) + intercept
return lda_score
def lda_score_to_scale(lda_score):
"""
Convert LDA score to 1-9 scale.
LDA scores typically range from -4 to +4 (8 range)
Target scale is 1 to 9 (8 range)
Formula: value = 5 + (lda_score * 4/3)
- LDA -3 or less → 1 (bureaucratic)
- LDA 0 → 5 (boundary)
- LDA +3 or more → 9 (relational)
"""
if lda_score is None:
return None
# Scale: value = 5 + (lda_score * 1.33)
value = 5 + (lda_score * 4.0 / 3.0)
# Clamp to 1-9 range and round
value = max(1, min(9, value))
return round(value)
def calculate_hardness(diagnostic_values):
"""Calculate hardness/softness (mean of all diagnostic values)"""
if not diagnostic_values:
@@ -133,34 +49,24 @@ def calculate_automated_analysis(json_data):
"""
Calculate values for automated analysis fields.
Modifies json_data in place.
Note: the formal/informal (LDA classifier) analysis was removed from the
bicorder in v1.3.0. The cluster classification lives on as research in
analysis/ (see scripts/bicorder_classifier.py).
"""
# Collect all diagnostic values in order
diagnostic_values = []
values_array = []
for diagnostic_set in json_data.get("diagnostic", []):
for gradient in diagnostic_set.get("gradients", []):
value = gradient.get("value")
if value is not None:
diagnostic_values.append(value)
values_array.append(float(value))
else:
# Fill missing with neutral value
values_array.append(5.0)
# Only calculate if we have diagnostic values
if not diagnostic_values:
return
# Load classifier model
model = load_classifier_model()
# Check version compatibility
bicorder_version = json_data.get("version", "unknown")
model_version = model.get("bicorder_version", "unknown") if model else "unknown"
version_mismatch = (model and bicorder_version != model_version)
# Calculate each automated analysis field
for analysis_item in json_data.get("analysis", []):
if not analysis_item.get("automated", False):
@@ -173,14 +79,6 @@ def calculate_automated_analysis(json_data):
analysis_item["value"] = calculate_hardness(diagnostic_values)
elif term_left == "polarized":
analysis_item["value"] = calculate_polarization(diagnostic_values)
elif term_left == "bureaucratic":
if version_mismatch:
# Skip calculation if versions don't match
print(f"Warning: Model version ({model_version}) doesn't match bicorder version ({bicorder_version}). Skipping bureaucratic/relational calculation.")
analysis_item["value"] = None
elif model:
lda_score = calculate_lda_score(values_array, model)
analysis_item["value"] = lda_score_to_scale(lda_score)
def center_text(text, width):