docs(rectification): narrower delivery brief + width funnel and karaka-interval research

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
This commit is contained in:
Jesse_Chen
2026-10-16 23:21:23 +08:00
co-authored by Claude Opus 5.5
parent 9b7509155f
commit 712a0fd2a1
11 changed files with 736 additions and 0 deletions
@@ -0,0 +1,32 @@
import json, sys, random
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import karaka_interval as K
asc = json.load(open('asc_signs.json'))
cases = json.loads((K.WT/'references/real_case_calibration/minute_rectification_holdout_v5.json').read_text())['cases']
shard, n = map(int, sys.argv[1].split('/'))
out = []
def seg(cid, R, sel):
win = {int(k): v for k, v in asc[cid].items() if abs(int(k)) <= R*60}
if len(set(win.values())) < 2: return None
t = win[0]; p = {win[g] for g in sel}
return 'right' if p == {t} else 'tie' if t in p else 'wrong'
for i, c in enumerate(cases):
if i % n != shard: continue
evs = [e for e in c['events'] if e['precision'] == 'day']
if len(evs) < 3: continue
for R in (10, 30, 60, 75):
for lv in (3, 4):
for km in ('P', 'PS'):
r = K.run_case(c, R, lv, km, [dict(e) for e in evs])
rng = random.Random(f"day|{c['case_id']}|{R}|{lv}|{km}")
size = 1 if km == 'P' else 2
pl = []
for _ in range(30):
q = K.run_case(c, R, lv, km, [{**e, 'karaka_set': set(rng.sample(K.PLANETS, size))} for e in evs])
pl.append((q['covered_tol'], q['width'], seg(c['case_id'], R, q['sel'])))
out.append(dict(case=c['case_id'], n_ev=len(evs), R=R, lv=lv, km=km, cov=r['covered'], cov_tol=r['covered_tol'],
width=r['width'], window=r['window'], all_expl=r['all_explained'], seg=seg(c['case_id'], R, r['sel']),
pl_cov_tol=sum(p[0] for p in pl)/30, pl_width=sum(p[1] for p in pl)/30,
pl_seg={k: sum(p[2] == k for p in pl)/30 for k in ('right', 'tie', 'wrong')}))
Path(f'day_shard{shard}.json').write_text(json.dumps(out)); print('DONE')
@@ -0,0 +1,67 @@
"""Why does the delivered interval stay wide? Per-question funnel on v5 (research harness, static probe list)."""
import json, sys, statistics as st
from pathlib import Path
WT = Path('/workspace/Jyotisha/.worktrees/karaka-interval-20261016')
sys.path.insert(0, str(WT)); sys.path.insert(0, str(WT/'scripts'))
from scripts.research import cluster_width_probe as cwp
from scripts.research import probe_supply_after_six as ps
from scripts.research import cluster_width_lib as cl
cases = json.loads((WT/'references/real_case_calibration/minute_rectification_holdout_v5.json').read_text())['cases']
shard, n = map(int, sys.argv[1].split('/'))
MAXQ = 12
out = []
def clock(t): h, m = map(int, t.split(':')); return h*60+m
for i, case in enumerate(cases):
if i % n != shard: continue
true_time = str(case['birth']['time'])[:5]
for R in (10, 30, 60):
req = cwp.scoring_request_for(case, R)
built = cwp.build_event_contribution_matrix(req)
rows = cwp.score_from_matrix(req, built)
contexts = list(built.get('static_contexts') or [])
window_times = [str(r['time'])[:5] for r in rows]
raw = cwp.raw_signature_clusters(contexts)
probes = cwp.discriminating_event_probes({**req, 'refresh_probes': False}, built, scan=cwp.window_scan(built),
candidate_times=list(window_times), representative_time=true_time, today=cwp.TODAY)
merged, _ = cwp.merge_adjacent_traced(raw, cwp._by_time_rows(rows), max_clusters=cwp.Variant('baseline').max_clusters,
max_peak_delta=cwp.Variant('baseline').max_peak_delta)
public = cwp.public_from_clusters(merged, rows)
reps = [str(r['time'])[:5] for r in public]
size = {str(r['time'])[:5]: len(r.get('cluster_times') or [1]) for r in public}
truth_rep = next((str(r['time'])[:5] for r in public if true_time in (r.get('cluster_times') or [str(r['time'])[:5]])), None)
prior = {t: float(r.get('score') or 0) for t, r in zip(reps, public)}
scores = dict(prior); conflicts = {t: 0 for t in reps}; elim = set()
prior_spread = max(prior.values()) - min(prior.values())
prior_truth_gap = max(prior.values()) - prior.get(truth_rep, 0)
steps = []
for q, probe in enumerate(probes[:MAXQ]):
alive = [t for t in reps if t not in elim]
yes, no = ps.outcome_groups(probe)
ans = ps.optimal_answer(probe, true_time)
sides = {}
for t in alive:
d = 'support' if t in yes else 'conflict' if t in no else ps.inherit_direction(t, yes, no)
sides[t] = d
truth_d = 'support' if ans == 'yes' else 'conflict' if ans == 'no' else 'neutral'
same = sum(size[t] for t in alive if sides[t] == truth_d) ; opp = sum(size[t] for t in alive if sides[t] not in (truth_d, 'neutral'))
neu = sum(size[t] for t in alive if sides[t] == 'neutral')
if ans is not None:
scores, conflicts, elim = ps.apply_answer(scores, conflicts, elim, probe, ans, reps)
valid = cl.still_valid_public(public, scores, elim, lead=8)
d = cl.delivery_from_public(valid)
steps.append(dict(q=q+1, answer=ans, same=same, opp=opp, neutral=neu, alive_min=sum(size[t] for t in reps if t not in elim),
span8=d['width'], set8=len(d['times']), domain=probe.get('domain') or probe.get('kind')))
def deliver(lead, upto):
sc = dict(prior); cf = {t: 0 for t in reps}; el = set()
for probe in probes[:upto]:
a = ps.optimal_answer(probe, true_time)
if a: sc, cf, el = ps.apply_answer(sc, cf, el, probe, a, reps)
v = cl.still_valid_public(public, sc, el, lead=lead); d = cl.delivery_from_public(v)
alive = [t for t in reps if t not in el]
best = max(sc[t] for t in alive); leaders = [t for t in alive if sc[t] == best]
return dict(span=d['width'], set=len(d['times']), cov=true_time in d['times'], top1=leaders == [truth_rep])
out.append(dict(case=case['case_id'], R=R, true=true_time, n_reps=len(reps), n_probes=len(probes),
prior_spread=prior_spread, prior_truth_gap=prior_truth_gap, steps=steps,
alt={f'L{lead}_Q{q}': deliver(lead, q) for lead in (8, 4, 2) for q in (6, 12)}))
print(case['case_id'], flush=True)
Path(f'funnel_shard{shard}.json').write_text(json.dumps(out)); print('DONE')
@@ -0,0 +1,68 @@
"""Why does the delivered interval stay wide? Per-question funnel on v5 (research harness, static probe list)."""
import json, sys, statistics as st
from pathlib import Path
WT = Path('/workspace/Jyotisha/.worktrees/karaka-interval-20261016')
sys.path.insert(0, str(WT)); sys.path.insert(0, str(WT/'scripts'))
from scripts.research import cluster_width_probe as cwp
from scripts.research import probe_supply_after_six as ps
from scripts.research import cluster_width_lib as cl
cases = json.loads((WT/'references/real_case_calibration/minute_rectification_holdout_v5.json').read_text())['cases']
shard, n = map(int, sys.argv[1].split('/'))
MAXQ = 12
out = []
def clock(t): h, m = map(int, t.split(':')); return h*60+m
for i, case in enumerate(cases):
if i % n != shard: continue
true_time = str(case['birth']['time'])[:5]
for R in (10, 30, 60):
req = cwp.scoring_request_for(case, R)
built = cwp.build_event_contribution_matrix(req)
rows = cwp.score_from_matrix(req, built)
contexts = list(built.get('static_contexts') or [])
window_times = [str(r['time'])[:5] for r in rows]
raw = cwp.raw_signature_clusters(contexts)
probes = cwp.discriminating_event_probes({**req, 'refresh_probes': False}, built, scan=cwp.window_scan(built),
candidate_times=list(window_times), representative_time=true_time, today=cwp.TODAY)
merged, _ = cwp.merge_adjacent_traced(raw, cwp._by_time_rows(rows), max_clusters=cwp.Variant('baseline').max_clusters,
max_peak_delta=cwp.Variant('baseline').max_peak_delta)
public = cwp.public_from_clusters(merged, rows)
reps = [str(r['time'])[:5] for r in public]
size = {str(r['time'])[:5]: len(r.get('cluster_times') or [1]) for r in public}
truth_rep = next((str(r['time'])[:5] for r in public if true_time in (r.get('cluster_times') or [str(r['time'])[:5]])), None)
prior = {t: float(r.get('score') or 0) for t, r in zip(reps, public)}
scores = dict(prior); conflicts = {t: 0 for t in reps}; elim = set()
prior_spread = max(prior.values()) - min(prior.values())
prior_truth_gap = max(prior.values()) - prior.get(truth_rep, 0)
steps = []
for q, probe in enumerate(probes[:MAXQ]):
alive = [t for t in reps if t not in elim]
yes, no = ps.outcome_groups(probe)
ans = ps.optimal_answer(probe, true_time)
sides = {}
for t in alive:
d = 'support' if t in yes else 'conflict' if t in no else ps.inherit_direction(t, yes, no)
sides[t] = d
truth_d = 'support' if ans == 'yes' else 'conflict' if ans == 'no' else 'neutral'
same = sum(size[t] for t in alive if sides[t] == truth_d) ; opp = sum(size[t] for t in alive if sides[t] not in (truth_d, 'neutral'))
neu = sum(size[t] for t in alive if sides[t] == 'neutral')
if ans is not None:
scores, conflicts, elim = ps.apply_answer(scores, conflicts, elim, probe, ans, reps)
valid = cl.still_valid_public(public, scores, elim, lead=8)
d = cl.delivery_from_public(valid)
steps.append(dict(q=q+1, answer=ans, same=same, opp=opp, neutral=neu, alive_min=sum(size[t] for t in reps if t not in elim),
span8=d['width'], set8=len(d['times']), domain=probe.get('domain') or probe.get('kind')))
def deliver(lead, upto, flip=None):
sc = dict(prior); cf = {t: 0 for t in reps}; el = set()
for k, probe in enumerate(probes[:upto]):
a = ps.optimal_answer(probe, true_time)
if a and flip is not None and k == flip: a = 'no' if a == 'yes' else 'yes'
if a: sc, cf, el = ps.apply_answer(sc, cf, el, probe, a, reps)
v = cl.still_valid_public(public, sc, el, lead=lead); d = cl.delivery_from_public(v)
alive = [t for t in reps if t not in el]
best = max(sc[t] for t in alive); leaders = [t for t in alive if sc[t] == best]
return dict(span=d['width'], set=len(d['times']), cov=true_time in d['times'], top1=leaders == [truth_rep])
out.append(dict(case=case['case_id'], R=R, true=true_time, n_reps=len(reps), n_probes=len(probes),
prior_spread=prior_spread, prior_truth_gap=prior_truth_gap, steps=steps,
alt={f'L{lead}_Q{q}_F{f}': deliver(lead, q, None if f < 0 else f) for lead in (8, 6, 5, 4) for q in (6, 12) for f in (-1, 0, 2, 4)}))
print(case['case_id'], flush=True)
Path(f'funnel2_shard{shard}.json').write_text(json.dumps(out)); print('DONE')
@@ -0,0 +1,199 @@
{
"dataset": "minute_rectification_holdout_v5.json",
"staging_head": "9b7509155",
"harness": "cluster_width_probe static probe list, simulated correct answers; one-wrong = flip answer #1/#3/#5",
"cells": {
"pm10_lead8_q6": {
"n": 77,
"live_minutes_median": 14,
"span_median": 13,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.983,
"top1": 51
},
"pm10_lead8_q12": {
"n": 77,
"live_minutes_median": 12,
"span_median": 11,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.987,
"top1": 53
},
"pm10_lead6_q6": {
"n": 77,
"live_minutes_median": 10,
"span_median": 11,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.961,
"top1": 51
},
"pm10_lead6_q12": {
"n": 77,
"live_minutes_median": 10,
"span_median": 9,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.97,
"top1": 53
},
"pm10_lead5_q6": {
"n": 77,
"live_minutes_median": 10,
"span_median": 9,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.926,
"top1": 51
},
"pm10_lead5_q12": {
"n": 77,
"live_minutes_median": 10,
"span_median": 9,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.931,
"top1": 53
},
"pm10_lead4_q6": {
"n": 77,
"live_minutes_median": 8,
"span_median": 9,
"truth_in_all_correct": 74,
"truth_in_one_wrong_rate": 0.866,
"top1": 51
},
"pm10_lead4_q12": {
"n": 77,
"live_minutes_median": 8,
"span_median": 7,
"truth_in_all_correct": 74,
"truth_in_one_wrong_rate": 0.87,
"top1": 53
},
"pm30_lead8_q6": {
"n": 77,
"live_minutes_median": 26,
"span_median": 35,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.952,
"top1": 41
},
"pm30_lead8_q12": {
"n": 77,
"live_minutes_median": 22,
"span_median": 33,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.961,
"top1": 46
},
"pm30_lead6_q6": {
"n": 77,
"live_minutes_median": 20,
"span_median": 33,
"truth_in_all_correct": 75,
"truth_in_one_wrong_rate": 0.892,
"top1": 41
},
"pm30_lead6_q12": {
"n": 77,
"live_minutes_median": 18,
"span_median": 25,
"truth_in_all_correct": 76,
"truth_in_one_wrong_rate": 0.926,
"top1": 46
},
"pm30_lead5_q6": {
"n": 77,
"live_minutes_median": 16,
"span_median": 31,
"truth_in_all_correct": 73,
"truth_in_one_wrong_rate": 0.823,
"top1": 41
},
"pm30_lead5_q12": {
"n": 77,
"live_minutes_median": 14,
"span_median": 15,
"truth_in_all_correct": 75,
"truth_in_one_wrong_rate": 0.883,
"top1": 46
},
"pm30_lead4_q6": {
"n": 77,
"live_minutes_median": 12,
"span_median": 19,
"truth_in_all_correct": 72,
"truth_in_one_wrong_rate": 0.732,
"top1": 41
},
"pm30_lead4_q12": {
"n": 77,
"live_minutes_median": 12,
"span_median": 13,
"truth_in_all_correct": 75,
"truth_in_one_wrong_rate": 0.818,
"top1": 46
},
"pm60_lead8_q6": {
"n": 77,
"live_minutes_median": 42,
"span_median": 71,
"truth_in_all_correct": 75,
"truth_in_one_wrong_rate": 0.931,
"top1": 30
},
"pm60_lead8_q12": {
"n": 77,
"live_minutes_median": 38,
"span_median": 59,
"truth_in_all_correct": 75,
"truth_in_one_wrong_rate": 0.952,
"top1": 36
},
"pm60_lead6_q6": {
"n": 77,
"live_minutes_median": 32,
"span_median": 59,
"truth_in_all_correct": 75,
"truth_in_one_wrong_rate": 0.857,
"top1": 30
},
"pm60_lead6_q12": {
"n": 77,
"live_minutes_median": 28,
"span_median": 51,
"truth_in_all_correct": 75,
"truth_in_one_wrong_rate": 0.883,
"top1": 36
},
"pm60_lead5_q6": {
"n": 77,
"live_minutes_median": 28,
"span_median": 53,
"truth_in_all_correct": 73,
"truth_in_one_wrong_rate": 0.74,
"top1": 30
},
"pm60_lead5_q12": {
"n": 77,
"live_minutes_median": 22,
"span_median": 39,
"truth_in_all_correct": 74,
"truth_in_one_wrong_rate": 0.814,
"top1": 36
},
"pm60_lead4_q6": {
"n": 77,
"live_minutes_median": 22,
"span_median": 45,
"truth_in_all_correct": 68,
"truth_in_one_wrong_rate": 0.606,
"top1": 30
},
"pm60_lead4_q12": {
"n": 77,
"live_minutes_median": 16,
"span_median": 27,
"truth_in_all_correct": 70,
"truth_in_one_wrong_rate": 0.701,
"top1": 36
}
}
}
@@ -0,0 +1,109 @@
"""Author's method on v5: natural-karaka sub-period interval intersection.
For each candidate birth moment (15 s grid inside truth±R), an event is 'explained' when a
Vimshottari period at level L ruled by the event's karaka overlaps the event's date span.
Delivered set = grid points explaining the maximum number of events (author tolerates
unexplained events). Truth is used only for the window centre (same as production replay)
and for scoring afterwards.
"""
import json, sys, random, calendar, statistics
from datetime import datetime, timedelta
from pathlib import Path
HERE = Path(__file__).parent
WT = Path('/workspace/Jyotisha/.worktrees/karaka-interval-20261016')
sys.path.insert(0, str(WT)); sys.path.insert(0, str(WT/'scripts')); sys.path.insert(0, str(HERE))
import dasha_analyzer
from scripts.research.nadi_seconds_lib import sky_at, _timeline
from karaka_rules import classify
PLANETS = list(dasha_analyzer.DASHA_ORDER)
STEP_S = 15
def event_span(e):
if e['precision'] == 'day':
a = datetime.strptime(e['date'], '%Y-%m-%d'); return a, a + timedelta(days=1)
y, m = map(int, e['date'].split('-')[:2])
a = datetime(y, m, 1); return a, a + timedelta(days=calendar.monthrange(y, m)[1])
def lords_overlapping(timeline, a, b, level):
"""Set of lords at `level` (1=MD..4=SD) whose period overlaps [a,b)."""
periods = [p for p in timeline if p['start'] < b and p['end'] > a]
for _ in range(level - 1):
nxt = []
for p in periods:
nxt += [q for q in dasha_analyzer.build_antardasha(dict(p)) if q['start'] < b and q['end'] > a]
periods = nxt
return {p['lord'] for p in periods}
def moon_track(birth, radius):
base = datetime.strptime(f"{birth['date']} {birth['time'][:5]}", '%Y-%m-%d %H:%M')
knots = list(range(-radius - 10, radius + 11, 10))
vals = []
for k in knots:
vals.append(sky_at(birth, base + timedelta(minutes=k)))
def at(sec):
m = sec / 60.0
i = min(max(int((m - knots[0]) // 10), 0), len(knots) - 2)
lo, hi = vals[i]['moon_lon'], vals[i + 1]['moon_lon']
if hi < lo - 180: hi += 360
f = (m - knots[i]) / 10.0
return (lo + (hi - lo) * f) % 360.0, vals[i]['asc_index'] if f < .5 else vals[i+1]['asc_index']
return base, at
def run_case(case, radius, level, karaka_mode, events_override=None):
birth = case['birth']
evs = events_override if events_override is not None else [e for e in case['events'] if e['precision'] in ('day', 'month')]
if not evs: return None
base, at = moon_track(birth, radius)
spans = [event_span(e) for e in evs]
ks = []
for e in evs:
if 'karaka_set' in e: ks.append(e['karaka_set']); continue
_, p, s = classify(e['description'])
ks.append({p} if karaka_mode == 'P' else {p, s})
grid = list(range(-radius * 60, radius * 60 + 1, STEP_S))
counts = []
for sec in grid:
moon, _ = at(sec)
bd = (base + timedelta(seconds=sec)).date().isoformat()
tl = _timeline(bd, moon)
counts.append(sum(bool(lords_overlapping(tl, a, b, level) & k) for (a, b), k in zip(spans, ks)))
best = max(counts)
sel = [g for g, c in zip(grid, counts) if c == best]
runs = 1 + sum(1 for x, y in zip(sel, sel[1:]) if y - x > STEP_S)
width_min = len(sel) * STEP_S / 60.0
return {'n_events': len(evs), 'best': best, 'all_explained': best == len(evs),
'covered': 0 in sel, 'width': width_min, 'window': (2 * radius * 60 + STEP_S) / 60.0,
'runs': runs, 'span': (min(sel) / 60, max(sel) / 60),
'center_err': abs((min(sel) + max(sel)) / 2 / 60.0), 'counts_truth': counts[grid.index(0)],
'covered_tol': any(abs(g) <= 150 for g in sel), 'sel': sel}
if __name__ == '__main__':
cases = json.loads((WT/'references/real_case_calibration/minute_rectification_holdout_v5.json').read_text())['cases']
radii = [int(x) for x in sys.argv[1].split(',')]
shard, nshard = map(int, sys.argv[3].split('/'))
cases = [c for i, c in enumerate(cases) if i % nshard == shard]
configs = [(lv, km) for lv in (2, 3, 4) for km in ('P', 'PS')]
n_placebo = int(sys.argv[2]) if len(sys.argv) > 2 else 20
out = []
for case in cases:
for R in radii:
for lv, km in configs:
r = run_case(case, R, lv, km)
if r is None: continue
# placebo: random karaka per event, same set size
pc = []
rng = random.Random(f"{case['case_id']}|{R}|{lv}|{km}")
evs = [e for e in case['events'] if e['precision'] in ('day', 'month')]
size = 1 if km == 'P' else 2
for _ in range(n_placebo):
ov = [{**e, 'karaka_set': set(rng.sample(PLANETS, size))} for e in evs]
q = run_case(case, R, lv, km, ov)
pc.append({'covered': q['covered'], 'covered_tol': q['covered_tol'], 'width': q['width']})
r.update(case_id=case['case_id'], radius=R, level=lv, karaka=km,
placebo_cov=sum(p['covered'] for p in pc) / len(pc),
placebo_width=statistics.mean(p['width'] for p in pc),
placebo_cov_tol=sum(p['covered_tol'] for p in pc) / len(pc))
out.append(r)
print(case['case_id'], flush=True)
(HERE / f"karaka_shard{shard}.json").write_text(json.dumps(out, indent=0, default=list))
print('DONE')
@@ -0,0 +1,36 @@
"""Pre-registered natural-karaka table (frozen before any run against truth).
Event -> one primary karaka (author's method: Mercury=exam/maths, Mars=surgery, Ketu=separation).
Rules are ordered; first match wins. Classical natural significations (BPHS / common usage).
"""
import re
RULES = [
# (tag, regex, primary karaka, secondary karaka)
("native_death", r"^(died|death|was killed|killed)\b|assassinat", "Saturn", "Mars"),
("father_event", r"\bfather\b", "Sun", "Saturn"),
("mother_event", r"\bmother\b", "Moon", "Saturn"),
("sibling_event", r"\b(brother|sister|sibling)", "Mars", "Saturn"),
("spouse_death", r"\b(husband|wife)\b.*\b(died|death|killed)", "Venus", "Saturn"),
("child_birth", r"\b(son|daughter|child|children|twins|baby)\b.*\b(born|birth)|\bbirth of\b|\bgave birth", "Jupiter", "Moon"),
("separation", r"divorc|separat|break ?up|split|annul|estrang|ended (his|her|their) (relationship|marriage)", "Ketu", "Saturn"),
("marriage", r"marri|wed|engag|relationship|romance|dating|partner", "Venus", "Jupiter"),
("finance", r"bankrupt|debt|default|loan|bought|purchas|sold|earn|fortune|forbes|million|deal|invest|franchise|wealth|donat|prize money|inherit", "Jupiter", "Rahu"),
("award", r"award|prize|oscar|grammy|emmy|medal|honou?r|nominat|nobel|\bwon\b|\bwins?\b|champion|title", "Sun", "Jupiter"),
("authority", r"elect|president|prime minister|minister|appoint|named|crown|king|queen|governor|senat|deput|chair|director of|head of|knight|ambassador", "Sun", "Saturn"),
("surgery_injury", r"surg|injur|accident|crash|broke|fractur|wound|burn|operation|amputat|shot\b|stab", "Mars", "Saturn"),
("illness", r"\bill(ness)?\b|sick|hospital|cancer|stroke|disease|diagnos|tumou?r|polio|addict|overdose|heart|depress|breakdown|tubercul|infect|rehab", "Saturn", "Rahu"),
("legal_confinement", r"arrest|prison|jail|convict|sentenc|exile|lawsuit|trial|court|indict|charged|impeach|deport|coup|ousted|resign", "Saturn", "Rahu"),
("higher_degree", r"doctor of|doctorate|phd|ph\.d|juris|law degree|m\.b\.|master'?s degree|postgraduate|dissertation", "Jupiter", "Mercury"),
("education", r"graduat|school|universit|college|academ|degree|stud(y|ied)|exam|enrol|attend|educat|conservator|institut|lycée|seminar", "Mercury", "Jupiter"),
("relocation", r"moved|emigrat|immigrat|relocat|settled|returned|migrat|fled|left for|sent to", "Rahu", "Moon"),
("writing", r"publish|wrote|writ|book|novel|essay|poem|journal", "Mercury", "Jupiter"),
("arts", r"film|movie|starr|act(ed|ing|ress)|debut|premier|perform|sing|song|album|record|concert|tour|model|show|music|paint|exhibit|danc|theat", "Venus", "Mercury"),
("career_default", r".", "Saturn", "Sun"),
]
COMPILED = [(t, re.compile(rx, re.I), p, s) for t, rx, p, s in RULES]
def classify(description: str):
for tag, rx, primary, secondary in COMPILED:
if rx.search(description or ""):
return tag, primary, secondary
raise AssertionError
@@ -0,0 +1,36 @@
"""Per-case production replay (baseline variant) on v5, current staging code."""
import json, sys, time, traceback
from pathlib import Path
WT = Path('/workspace/Jyotisha/.worktrees/karaka-interval-20261016')
sys.path.insert(0, str(WT)); sys.path.insert(0, str(WT/'scripts'))
from scripts.research import cluster_width_probe as cwp
HOLD = WT/'references/real_case_calibration/minute_rectification_holdout_v5.json'
cases = json.loads(HOLD.read_text())['cases']
radii = [int(r) for r in (sys.argv[1] if len(sys.argv)>1 else '10,30,60').split(',')]
out = []
variant = cwp.Variant('baseline')
t0=time.time()
for case in cases:
cid = case['case_id']; true_time = str(case['birth']['time'])[:5]
for radius in radii:
try:
request = cwp.scoring_request_for(case, radius)
built = cwp.build_event_contribution_matrix(request)
rows = cwp.score_from_matrix(request, built)
contexts = list(built.get('static_contexts') or [])
window_times = [str(r['time'])[:5] for r in rows]
raw = cwp.raw_signature_clusters(contexts)
probes = cwp.discriminating_event_probes({**request,'refresh_probes':False}, built,
scan=cwp.window_scan(built), candidate_times=list(window_times),
representative_time=true_time, today=cwp.TODAY)
s = cwp.score_variant(raw_clusters=raw, rows=rows, variant=variant, true_time=true_time,
window_times=window_times, probes=probes)
r = s['replay']
out.append({'case_id':cid,'radius':radius,'true':true_time,
'engine':{k:s['engine'][k] for k in ('top1_hit','coverage','width')},
'replay':{k:r.get(k) for k in ('top1_hit','coverage','width','asked','eliminated_count','union_alive_width')}})
except Exception as e:
out.append({'case_id':cid,'radius':radius,'error':f'{type(e).__name__}: {e}','trace':traceback.format_exc()[-800:]})
print(cid, round(time.time()-t0), flush=True)
Path('prod_per_case.json').write_text(json.dumps(out, ensure_ascii=False, indent=1))
print('DONE')
@@ -0,0 +1,28 @@
import json, sys, random
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import karaka_interval as K
asc = json.load(open('asc_signs.json'))
cases = json.loads((K.WT/'references/real_case_calibration/minute_rectification_holdout_v5.json').read_text())['cases']
shard, n = map(int, sys.argv[1].split('/'))
out = []
def outcome(cid, R, sel):
win = {int(k): v for k, v in asc[cid].items() if abs(int(k)) <= R*60}
if len(set(win.values())) < 2: return None
t = win[0]; p = {win[g] for g in sel}
return 'right' if p == {t} else 'tie' if t in p else 'wrong'
for i, c in enumerate(cases):
if i % n != shard: continue
evs = [e for e in c['events'] if e['precision'] in ('day', 'month')]
for R in (30, 60, 75):
for lv in (3, 4):
o = outcome(c['case_id'], R, K.run_case(c, R, lv, 'PS')['sel'])
if o is None: continue
rng = random.Random(f"seg|{c['case_id']}|{R}|{lv}")
pl = []
for _ in range(40):
ov = [{**e, 'karaka_set': set(rng.sample(K.PLANETS, 2))} for e in evs]
pl.append(outcome(c['case_id'], R, K.run_case(c, R, lv, 'PS', ov)['sel']))
out.append(dict(case=c['case_id'], R=R, lv=lv, real=o, pl={k: pl.count(k)/40 for k in ('right','tie','wrong')}))
Path(f'seg_shard{shard}.json').write_text(json.dumps(out))
print('DONE')
@@ -0,0 +1,71 @@
# 生时校正:交付区间为什么宽 + 「象征星区间求交」对照(2026-10-16)
基线 `origin/staging` @ `9b7509155`。数据集 `references/real_case_calibration/minute_rectification_holdout_v5.json`(77 例公开 AA 名人)。脚本在 `docs/research/artifacts/rectification-width-funnel-2026-10-16/`(脚本里 `WT` 指向当时的只读工作树,复跑时改成本地检出路径)。纯研究,不改线上。
## 1. 起因
产品 10-16 转来一篇外部文章:用自然象征星(Karaka)所主小运求区间交集,把单个案例从 150 分钟收到 3 分钟。产品要求按该方法跑一遍,并与本仓校正对比。之后产品追问:本仓交付 75 分钟,等于没校正,问题在哪。
## 2. 象征星区间求交:复现结论
做法:用关键词规则把事件映射到象征星(`karaka_rules.py`,先计算 sha256 冻结再跑)。只用日或月精度事件(387 件)。网格 15 秒,取「解释事件数最多」的时刻集合,允许丢掉解释不了的事件,与原文一致。对照组是随机象征星,每例 30 次。
| 设置(起始 ±75 分钟) | 交付中位 | 真值在内 | 随机象征星 |
|---|---|---|---|
| 第三层小运 | 17 分钟 | 15/77 | 15.0/77 |
| 第四层小运 | 5 分钟 | 3/77 | 3.4/77 |
- 只用日精度事件(≥3 件的 43 例):16 组设置无一显著好于随机。第四层还略差于随机(z 低至 −2.2)。
- 上升星座二选一:第三层小运 z≈1.3,三档窗口方向一致但样本重叠,不显著;第四层无信号。
- 文章称「1 分钟 = 4.63 天」,只对其个案成立。77 例实测 1.36–5.60 天,中位 3.84 天。
- 同宽度对比(±60,同为约 4 分钟一段):文章方法 1~2/43,随机划段约 1.2/43,本仓头名簇 16/43。
结论:该方法在分钟级上的命中与随机无异,不立单。与 `rectification_nadi_seconds_2026_10_05.md` 同属「更细层小运 + 日精度」路线,同样不过。
## 3. 本仓交付区间宽的拆解
方法:`funnel.py` / `funnel2.py`,cluster_width_probe 研究回放。说明三点:题目列表一次性生成,不中途刷新;回答按真值模拟(全对);「答错一题」为分别翻转第 1、3、5 题。候选步长 2 分钟,「实际剩余」等于活着的候选戳数 × 2。
### 3.1 交付按段内外包络显示
`scripts/rectification/candidate_window.py::interval_union_width` 与前端 `unionWindowIntervals` 按 `segment_index` 合并。`segment_index` 是用户声明的窗口段,不是候选簇,所以同一窗口内被排除的空档也被包进去了。
| 窗口 | 现在显示(最早–最晚) | 实际剩余分钟 |
|---|---|---|
| ±10 | 13 | 14 |
| ±30 | 35 | 26 |
| ±60 | 71 | 42 |
### 3.2 题目切不动
- 供给:可出题中位 7~8 道。±10 窗口 30/77 例不足 6 道,12 例为 0 道。
- 切分:Q1–Q2 约 55:45;到 Q5–Q6,真值一侧已占 76–90%,一道题只排掉一两成。
- 题数从 6 加到 12(静态列表,题够的例子才能问到):±60 实际剩余 42→38 分钟,头名 30→36。
### 3.3 进区间门槛 8 分 = 至少两道题反向
一道题拉开 4 分(+2 / −2),所以一个候选至少要两道题都站错边才出区间。降门槛的代价是答错一题时容易把真值挤出去:
| 窗口 | 门槛 / 题数 | 实际剩余 | 全答对真值在内 | 答错一题真值在内 |
|---|---|---|---|---|
| ±10 | 8 / 6(现状) | 14 | 76/77 | 98% |
| ±10 | 6 / 12 | 10 | 76/77 | 97% |
| ±10 | 4 / 12 | 8 | 74/77 | 87% |
| ±30 | 8 / 6(现状) | 26 | 76/77 | 95% |
| ±30 | 6 / 12 | 18 | 76/77 | 93% |
| ±30 | 5 / 12 | 14 | 75/77 | 88% |
| ±30 | 4 / 12 | 12 | 75/77 | 82% |
| ±60 | 8 / 6(现状) | 42 | 75/77 | 93% |
| ±60 | 8 / 12 | 38 | 75/77 | 95% |
| ±60 | 6 / 12 | 28 | 75/77 | 88% |
| ±60 | 4 / 12 | 16 | 70/77 | 70% |
全表见 `funnel_summary.json`。门槛 4 在 ±30 / ±60 下,答错一题就挤掉两到三成真值,不可用。门槛 6 是折中。
### 3.4 物理下限
出生时间差 1 分钟,大运边界平均只平移约 3.8 天。用户记忆以月为单位,出题闸 `REFRESH_MIN_BOUNDARY_DAYS=30` 相当于约 8 分钟。所以靠年月问答,最好约 10 分钟,到不了 3–4 分钟。日精度加更细层的路线已两次证伪(纳迪 10-05、象征星本文)。
## 4. 去向
实现单 `docs/tasks/TASK-rectification-narrower-delivery-20261016.md`:分段显示、按剩余候选对半出题并放宽题数、交付区间门槛 8→6(`MIN_SEPARATION_LEAD` 不动)。
+1
View File
@@ -36,6 +36,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-rectification-narrower-delivery-20261016.md` | — | **交付区间收窄**:显示改按活着的簇分段(±60 显示 71→实际 42 分钟)、按剩余候选对半出题并放宽到 12 轮(少数侧 <20% 不出题)、新增 `DELIVERY_RANGE_LEAD=6` 只管进区间(`MIN_SEPARATION_LEAD=8` 不动,写读同口径)。红线:PG 回放真值保留全对 ≥基线−1、答错一题 ≥基线−5pp,不过则该档回 8。证据 `docs/research/rectification_width_funnel_2026_10_16.md`(含象征星区间求交复现=随机)。BUG 1367 起 | 待领取 | — |
| —(子代理直接执行,无任务书) | `PROGRESS-bug-1060-d9-double-transit-20260927.md` | **双重过运 D9 目标错配(BUG-1060)**:D9 第 N 宫目标误用 D9 上升、D9 宫主目标误用 D1 度数,改为标签所写的 D9 星座;D1 / CL 层逐字节不变;数据卡 golden 重采集(一张盘跨层结论 9→10);另记 BUG-1061 跨层数字比对(未修) | 已验收(待部署) | `codex/bug-1060-d9-double-transit-20260927`(未推送) |
| —(子代理直接执行,无任务书;产品 2026-09-27 授权) | `PROGRESS-bug-1061-cross-layer-pairing-20260927.md` | **双重过运跨层配对按名字数字比对(BUG-1061)**:改为结构化身份配对(D1 第 N 宫↔D9 第 N 宫;D1 第 N 宫主↔同一颗星的 D9 目标);各层结果与 D1/CL golden 逐字节不变;48 样本跨层条目 31→38;数据卡 golden 重采集(乔布斯 6→9、泰勒 9→10) | 已验收(待部署) | `codex/bug-1061-cross-layer-pairing-20260927`(未推送) |
| `TASK-rectification-fewer-probes-card-20260926.md` | [PROGRESS](PROGRESS-rectification-fewer-probes-card-20260926.md) | **减少无效追问 + 卡片区间为主**:引导补件离线新增达标 0 → 上限 6→2、定向题问完门槛未达直接出卡;卡片区间为主标题、代表分钟副标题,第一二名差距 ≥5 个百分点才显示百分比,否则写「目前区分不开」。不放宽任何置信度。先做 | 已验收(真机清单欠) | `4e6e8d87` + 验收补改 `7203d94e`(区分不开时隐藏「最可能」),deploy-staging run 2933 已部署;回放真值 ±30/±60 19→20、宽度中位 ±1 分钟、提问 11.4→7.8;Skill 10.0.30;真机清单 `docs/testing/rectification-fewer-probes-card-20260926.md` 未走 |
@@ -0,0 +1,89 @@
# TASK:生时校正交付区间收窄(分段显示 + 对半出题 + 交付门槛 8→6)2026-10-16
## 0. 基线
- `origin/staging` @ `9b7509155`(开工时以最新 `origin/staging` 为准,并在进度记录里写实测基线 SHA)。
- 证据与脚本:`docs/research/rectification_width_funnel_2026_10_16.md`,以及 `docs/research/artifacts/rectification-width-funnel-2026-10-16/`(`funnel_summary.json` 是本单的预期数字来源)。
## 1. 事故实证
产品反馈:前后各 60 分钟的窗口问完题,交付约 75 分钟,用户觉得「跟没校正一样」。v5 回放(77 例、模拟答对)现状:
| 窗口 | 显示宽度(中位) | 实际剩余分钟(中位) | 真值在区间 | 答错一题真值在区间 |
|---|---|---|---|---|
| ±10 | 13 | 14 | 76/77 | 98% |
| ±30 | 35 | 26 | 76/77 | 95% |
| ±60 | 71 | 42 | 75/77 | 93% |
## 2. 根因(按符号定位)
1. **显示按段内外包络。** `scripts/rectification/candidate_window.py::interval_union_width` 的 docstring 写着「Keep historical within-segment envelope」。前端 `frontend/src/lib/rectification-agentic/core/candidate-window.ts::unionWindowIntervals` 按 `segment_index` 把同一段内所有区间合成最早–最晚。`segment_index` 是用户声明的窗口段(只在跨午夜等情况下才有多段),不是候选簇。所以交付卡 `frontend/src/components/rectification-range-delivery.tsx` 的 `rangeLabel`(经 `windowIntervalsLabel`)把中间已出局的空档一起显示了出来。±60 窗口显示 71 分钟,实际只剩 42 分钟。
2. **题目切不动、题数少。** 可出题中位只有 7~8 道(`scripts/rectification/event_probes.py`:`MAX_PROBES=8`、`REFRESH_MAX_PROBES=12`、`MAX_PROBES_PER_DOMAIN=3`)。Q5–Q6 时剩余候选中 76–90% 与真值同侧,一题只排除一两成。前端轮数上限是 `core/types.ts::DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8`。
3. **进区间门槛 8 分。** `core/credible-range.ts::unionStillValidIntervals / unionStillValidRange / stillValidCandidates` 用 `MIN_SEPARATION_LEAD = 8`(`core/candidate-separation.ts`)。一道题只拉开 4 分(`SCORE_DELTA` ±2),所以一个候选至少要两道题都反向才出区间。
4. **物理下限(不修)。** 1 分钟约 3.8 天,加上月级记忆和 `REFRESH_MIN_BOUNDARY_DAYS=30`,问答路线最好约 10 分钟。
## 3. 决策记录(产品 2026-10-16 授权)
- **D1 交付分段显示,推翻「段内外包络」。** 用户可见的范围(交付卡标题、盘型交付 `RectificationSegmentDelivery` 的 `rangeLabel`、旁白和 spoken-grounding 里的范围句、报告里的范围、会话标题)一律按**实际活着的簇区间**显示。间隔 ≤4 分钟的相邻区间合并。合并后超过 3 段时,只显示最靠前的 3 段,再加「等 N 段,共约 X 分钟」。宽度统计(遥测 `width`、交付文案里的「约 X 分钟」)改用实际剩余分钟数。`interval_union_width` 等**决策层**的宽度口径本轮不改(收敛判定、档位、闸门仍按旧口径),在进度记录里列清所有调用点和各自采用的口径。
- **D2 题数上限放宽 + 按剩余候选对半出题。** `DEFAULT_MAX_DISCRIMINATION_ROUNDS` 从 8 提到 12,探针供给相应放宽(`REFRESH_MAX_PROBES`、每域上限由执行方实测后定,写进进度记录)。每出一题都按当前未淘汰候选的后验质量选题,优先取两侧质量最接近 50:50 的。**少数一侧质量 < 20% 的题不出**,宁可停。这条和 09-26 fewer-probes「减少无效追问」不冲突:那一单管的是引导补件,本单只加能切开剩余候选的点选题。
- **D3 交付区间门槛 8→6,`MIN_SEPARATION_LEAD` 不动。** 新增常量 `DELIVERY_RANGE_LEAD = 6`,只用于「谁进交付区间」:`credible_range`、`credible_intervals`、`stillValidCandidates` 的交付用途,以及 D1 的显示。下面这些继续用 `MIN_SEPARATION_LEAD = 8`:分离状态(`evaluateCandidateSeparation`)、采用 / 确认 / 熔断门、`segment-summary.ts` 的盘型段汇总(其档位按 8 校准)。
- 这样不违反 BUG-560 的防复发条「不得改 `MIN_SEPARATION_LEAD=8`」,也不违反 BUG-423 的「不得用改 `MIN_SEPARATION_LEAD` 绕过死锁」。
- 但 BUG-423 的教训必须遵守:**写盘与读盘的 `credible_range` / `credible_intervals` 必须用同一个 lead。** `inference-adapter.ts` 里 `consistent` 的比对两边都换成 `DELIVERY_RANGE_LEAD`,不得出现一边 8、一边 6。
- **D4 窗口分档回退。** 验收回放里某一档(±10 / ±30 / ±60)在门槛 6 下过不了 §4 红线 2,该档就回到 8。分档写成一张表,挂在 `DELIVERY_RANGE_LEAD` 旁边。
- **D5 口径。** 交付文案不得承诺「定到分钟」。可以写「剩余约 X 分钟,分 N 段」。
## 4. 硬红线
1. 不改引擎打分:`SCORE_DELTA`、`STRONG_CONFLICT_ELIMINATION_COUNT=3`、`MIN_BOUNDARY_DAYS=45` / `REFRESH_MIN_BOUNDARY_DAYS=30`、`_relative_support`、采用 / 确认门全部不动。不放宽任何置信度。
2. **真值保留**:v5 77 例,用生产代码路径回放(PG 持久回放 `frontend/tests/research/rectification-segment-persisted-replay.ts` + `scripts/research/varga_resolution_persisted_replay.py`,按段选题开关保持默认)。每档同时满足两条:
- 全答对时,真值在交付区间内的例数 ≥ 基线 −1;
- 答错一题(分别翻转第 1、3、5 题)时,真值在区间内的比例 ≥ 基线 −5 个百分点。
基线在同一 harness 上用改动前代码实测,不用本单研究数字代替。
3. 预期(研究回放,静态题单,仅作参照):门槛 6、12 题时实际剩余分钟 ±10 约 10、±30 约 18、±60 约 28。生产回放达不到也不得为了凑数改红线 1 的任何一项,如实报告即可。
4. 前端按 AGENTS §7:tsc 0、lint 0 error、测试总数不减,既有断言改动写「原值 / 新值 / 原因」,`/` 保持 Static,gzip ±2%。
5. 校正 Skill 若 bump,必须验证旧 Case 能从列表打开(BUG-621)。
6. 不动数据库结构;遥测只是写入的数值换口径,若需新列,单独说明并跑 test:db。
## 5. 任务分解
- **T0 基线**:在开工 SHA 上跑 §4 红线 2 的回放,三档 × {全对, 翻 1/3/5},产出 `docs/research/rectification_narrower_delivery_baseline_2026_10_16.json`。
- 验收:数字齐全;有可复跑的命令。
- **T1 分段显示(D1)**:在 Python 与前端各加一个「活着的区间合并」函数(间隔 ≤4 分钟合并),只供显示与遥测使用;按 D1 列出的所有用户可见出口逐个切换;`windowIntervalsLabel` 支持「等 N 段」。
- 验收:
- 单测覆盖三种情况:单段、跨空档两段、5 段截 3 段;
- 用 golden 中一例 ±60 回放得到的候选构造交付卡,标题段数与实际剩余分钟数和回放一致;
- 列出所有 `interval_union_width` / `unionWindowIntervals` 调用点及各自采用的口径;
- `frontend/DESIGN.md` 与 `frontend/docs/VOICE.md` 同步更新。
- **T2 对半选题(D2)**:刷新探针时按剩余后验质量的对半程度排序,少数一侧 < 20% 的题不出;放宽轮数和供给上限。
- 验收:
- 回放报告给出每题「真值同侧占比」中位,Q4–Q8 必须比基线更接近 0.5;
- 平均提问数与问满题的例数写进报告;
- 不出现「少数一侧 < 20%」的题(加断言)。
- **T3 交付门槛(D3 / D4)**:新增 `DELIVERY_RANGE_LEAD` 及分档表,写盘、读盘同口径。
- 验收:
- BUG-423 合同测试仍绿,并新增「写 6 读 6」的一致性测试;
- 分离状态、采用门、segment-summary 仍用 8 的断言;
- 回放过 §4 红线 2,否则按 D4 回退并在报告里写明是哪一档。
- **T4 回放判定**:T1+T2+T3 合起来跑 §4 红线 2 的全套,结果写进 `docs/research/rectification_narrower_delivery_replay_2026_10_16.json`。
- 验收:三档逐格对照基线,给出实际剩余分钟、显示宽度、头名命中、平均提问数。
- **T5 记录**:写 `PROGRESS-rectification-narrower-delivery-20261016.md`、`BUG_HISTORY`(3 条:显示包络、选题不对半、交付门槛)、`CHANGELOG`;真机清单写到 `docs/testing/rectification-narrower-delivery-20261016.md`(±30 窗口完整走一次,看段数和题数)。
## 6. 让步顺序
时间不够时:先交 T0+T1(纯显示,风险最低,单独就能把 ±60 从 71 降到约 42),再交 T3,最后交 T2。任何一项过不了红线 2 就撤掉该项,不能为了保住其他项去放宽红线。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-narrower-delivery-20261016 .worktrees/rectification-narrower-delivery-20261016 origin/staging
cd .worktrees/rectification-narrower-delivery-20261016
grep -n "BUG-423\|BUG-560\|BUG-653\|BUG-1143\|BUG-1144" docs/BUG_HISTORY.md # 读完命中记录
grep -rn "unionWindowIntervals\|interval_union_width\|unionStillValid\|MIN_SEPARATION_LEAD\|DEFAULT_MAX_DISCRIMINATION_ROUNDS" frontend/src scripts tests frontend/tests | wc -l
```
本机没有 Docker 时,PG 回放按 `docs/research/` 中盘型口径研究记录的「无 Docker 验收法」(预编译 PG17 + psql / docker 替身)跑。
## 8. BUG 编号起点
BUG-1367 起,开工时用 `docs/BUG_HISTORY.md` 的最大号核对一遍。