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:
co-authored by
Claude Opus 5.5
parent
9b7509155f
commit
712a0fd2a1
@@ -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` 不动)。
|
||||
@@ -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` 的最大号核对一遍。
|
||||
Reference in New Issue
Block a user