Files
Maven/cmd/semantic-router-experiment/slice21_emit.py
T
claude fa98e4722e router/semantic: slice 21 deterministic execution-frame guard engine, fixtures, runner and emit step
The guard answers one question — may this utterance become an executable
action — as a three-way policy gate (permissive / blocked / ambiguous) and
never decides what the utterance is. Rules are the encoding of the measured
slice-20 dev-pool discriminators: 126 capability-question rows are 42/42/42
addressed / bare-ability / bare-future; 127 bare можешь+пожалуйста rows are
100% action; can-you-please is 100% action. Reuses the shipped prohibition
parser, morph finiteness and lexicon fillers; reason vocabulary is closed.

Slice 21 (task/725, brief after the accepted slice 20).
2026-09-07 23:43:47 +04:00

135 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
Slice 21 emit: deterministic execution-frame guard — data files for the Go harness
==================================================================================
Slice 18 showed the sparse lexical gate owns the aggregate boundary (PR-AUC
0.838, strict operating point at threshold 0.715 with P>=0.95 | R=0.264) and
slice 19/20 showed learning heads collapse on capability-question LOFO. Slice 21
tests the deterministic alternative: a rule engine over existing parsers that
decides execution eligibility as a three-way gate (permissive / blocked /
ambiguous), never itself routing.
This script only repackages the frozen dev pool for the Go harness. It reuses
slice 18's feature builders and grouped-CV and slice 19's pair builder verbatim,
so the numbers the Go side reports are the same populations the accepts
measured. It writes:
/tmp/mvn-s21/pool.json dev rows: idx, text, n_text, route, y, tags,
cv_fold, split_group, source_id, family
/tmp/mvn-s21/pairs.json capability-vs-action pairs (slice-19 builder)
/tmp/mvn-s21/sparse_oof.json slice-18 "both" grouped-CV OOF proba per row
(for the §17 guard+sparse composition)
No training happens here and no label is changed. The guard itself is Go.
"""
import json
import os
import re
import sys
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import slice18_sparse # noqa: E402
import slice19_main # noqa: E402
OUT_DIR = "/tmp/mvn-s21"
SPARSE_THRESHOLD = 0.715 # slice-18 §4 strict-operating-point (P>=0.95 best recall)
def main():
# Population = the exact slice-20 dev pool (s19.load_dev): every dev_pool
# row, fast-path included. The guard is evaluated on what slice 20 measured.
meta, examples = slice18_sparse.load_data()
dev = slice18_sparse.filter_dev_pool(examples)
print(f"dev pool (all dev_pool rows): {len(dev)} rows")
print(f"corpus meta: {meta.get('dev_count', '?')} dev rows declared, "
f"{meta.get('route_counts', {}).get('action', '?')} action declared")
n_texts = [slice18_sparse.normalize_match_text(e["text"]) for e in dev]
rows = []
by_route = {}
by_family = {}
for i, (e, nt) in enumerate(zip(dev, n_texts)):
tags = sorted(set(e.get("tags", [])))
route = e["route"]
fam = slice19_main.family_of(set(tags))
by_route[route] = by_route.get(route, 0) + 1
by_family[fam] = by_family.get(fam, 0) + 1
rows.append({
"idx": i,
"text": e["text"],
"n_text": nt,
"route": route,
"y": 1 if route == "action" else 0,
"tags": tags,
"cv_fold": e["cv_fold"],
"split_group": e["split_group"],
"source_id": e["source_id"],
})
print("routes:", by_route)
print("families:", by_family)
# ── pairs (slice-19 builder, exact population) ─────────────────────────
ldev = [{
"text_orig": nt,
"route": r["route"],
"y": r["y"],
"cv_fold": r["cv_fold"],
"tags": set(r["tags"]),
"source_id": r["source_id"],
} for r, nt in zip(rows, n_texts)]
pairs = slice19_main.build_pairs(ldev, n_texts)
print(f"pairs: {len(pairs)}")
# ── slice-18 "both" grouped-CV OOF proba, aligned to row index ────────
y = [1 if r["route"] == "action" else 0 for r in rows]
folds = [r["cv_fold"] for r in rows]
X, _vec = slice18_sparse.build_features(n_texts, "both")
print(f"sparse 'both' X: {X.shape}")
yb = np.array(y)
folds_arr = np.array(folds)
idx_proba = {}
for te_fold in sorted(set(folds)):
tr = folds_arr != te_fold
te = folds_arr == te_fold
clf = slice18_sparse.LogisticRegression(
C=1.0, max_iter=2000, solver="lbfgs", random_state=42)
clf.fit(X[tr], yb[tr])
p = clf.predict_proba(X[te])[:, 1]
te_idx = np.where(te)[0]
for k, i in enumerate(te_idx):
idx_proba[int(i)] = float(p[k])
assert len(idx_proba) == len(rows)
sparse_oof = [{"idx": i, "proba": idx_proba[i]} for i in range(len(rows))]
pred = [1 if idx_proba[i] >= 0.5 else 0 for i in range(len(rows))]
tp = sum(1 for i in range(len(rows)) if y[i] == 1 and pred[i] == 1)
fp = sum(1 for i in range(len(rows)) if y[i] == 0 and pred[i] == 1)
fn = sum(1 for i in range(len(rows)) if y[i] == 1 and pred[i] == 0)
print(f"sparse both OOF @0.5: P={tp/max(tp+fp,1):.3f} R={tp/max(tp+fn,1):.3f} "
f"FA={fp} ({fp/len(rows):.4f})")
pred21 = [1 if idx_proba[i] >= SPARSE_THRESHOLD else 0 for i in range(len(rows))]
tp = sum(1 for i in range(len(rows)) if y[i] == 1 and pred21[i] == 1)
fp = sum(1 for i in range(len(rows)) if y[i] == 0 and pred21[i] == 1)
fn = sum(1 for i in range(len(rows)) if y[i] == 1 and pred21[i] == 0)
print(f"sparse both OOF @{SPARSE_THRESHOLD}: P={tp/max(tp+fp,1):.3f} "
f"R={tp/max(tp+fn,1):.3f} FA={fp} ({fp/len(rows):.4f})")
os.makedirs(OUT_DIR, exist_ok=True)
with open(os.path.join(OUT_DIR, "pool.json"), "w") as f:
json.dump(rows, f, ensure_ascii=False, indent=1)
with open(os.path.join(OUT_DIR, "pairs.json"), "w") as f:
json.dump([[c, a] for c, a in pairs], f)
with open(os.path.join(OUT_DIR, "sparse_oof.json"), "w") as f:
json.dump(sparse_oof, f)
print(f"wrote {OUT_DIR}/{{pool,pairs,sparse_oof}}.json")
if __name__ == "__main__":
main()