feat(factors): close D5 C5 — four-leg reconciliation green across all eleven factors - #113
Merged
Merged
Conversation
…after
The close book mode used to write the with-book reports under the SHARED
`{stem}_exec_with_book.*` names and `os.replace` them to `_bookclose`
afterwards. Running the two book modes in the order decision -> close
therefore destroyed the decision run's three with-book artifacts: the close
run overwrote them, then moved them aside. All eleven factors lost their
`exec_with_book.*` in the D5 C5 full run, and the reconcile's reports leg
died on a bare FileNotFoundError naming a file that had existed minutes
earlier.
The suffix now travels INTO the write-out layer
(`run_exec_basis_evaluation(..., with_book_suffix=)`, default "" so every
existing caller keeps its byte-identical paths), so each book mode only ever
writes its own file names and the run order stops mattering.
Two tests, at the two levels the behaviour spans: the writer's own
(two real runs into one report dir, first run's bytes must survive) and the
runner's (decision then close, with the stub applying the suffix at write
time exactly as the real writer does, so a wrong suffix writes wrong names).
…oncile
Three judgement gaps the D5 C5 full reconciliation exposed. Each one is
bounded on every axis it can be bounded on, and each bound has a reverse test
that fails one step outside it.
F2 - `threshold_flip_contamination` gains a BARS-ONLY arm: the same
600623.SH critical-bar flip already registered for the cross-sectional
factors, on factors that have no per-date OLS to spread it. Only the directly
affected symbol qualifies; the bound is RELATIVE and per factor
(peak_interval_kurtosis 5e-3, valley_relative_vwap 1e-5) because the two live
on unrelated value scales - measured |diff| 1.07e-2 vs 1.53e-6, so one
absolute bound cannot serve both. At most 25 cells; an unregistered factor
never gets the class.
F3 - two more surfaces of the anchor truncation. (1) frozen-finite ->
new-NaN joins the warmup direction set for VALID-DAY POOLED factors inside
the early region only: their emission is gated by a count of valid days, so
the two geometries can accumulate a different number of them at the anchor
edge. A bounded factor has no such gate and keeps failing. (2) A new class
`warmup_sparse_valid_day_tail` for the sparse names that carry that early
difference past the early window: window [2021-11-01, 2021-11-12], a symbol
whitelist, at most 20 cells, pooled factors only. Every (factor, symbol) pair
in the class sits below that factor's own median emission density - including
600906.SH, which is below it for peak_ridge and above it for ridge, and
appears only in the former's cluster.
F4 - the ABSOLUTE float-dust predicate moves ahead of every region branch, so
a cell is classified by its mechanism rather than by where it lands.
Machine-precision dust is uniform across the grid; letting the region decide
first made a handful of dust cells count as warmup cells, and their monthly
counts then failed the pooled non-increasing gate on noise
(intraday_amp_cut: 2021-08/09/10 = 8/3/9 cells, every one |diff| <= 1e-12).
Measured on the same served panels, before -> after: amp_cut's months
{07:8198, 08:11, 09:3, 10:9} -> {07:8198} and its float tail 758 -> 798 of
1000; vpq 883 -> 925 of 1000; jump, which sits exactly ON its cap of 101,
gains nothing. The relative arm stays where it was.
Also prints the per-class max relative difference in the panels summary
(review NIT-1): the headline max is taken before any bucketing, so a 9.03e-01
that lies entirely inside a registered class was being printed next to
`unclassified=0` and reading like an ungated tolerance. One-sided cells
report inf, never 0.0.
Names every missing decision-mode artifact and the command that produces them, instead of dying on whichever file it opened first; a half-written _bookclose pair is its own readable error.
The F4 argument is that the three disputed months are indistinguishable from every other month, so it has to be backed by the distribution rather than by the three counts alone: 798 cells over all 59 months, 3-28 per month (median 12), 100% of them on the absolute arm, with the disputed 11/3/9 inside that spread. Also records that the two representative factors were re-run through the real harness, not only the offline classifier.
…eport The C5 first-run results existed only in artifacts/logs/, which is gitignored and lives on one machine — and `_make_logger` opens in truncate mode, so the phase-B rerun will overwrite every one of those files. This repo has already lost a full run's log once to a /tmp cleanup. The report is the durable carrier; the logs are not. Adds the per-factor x three-mode pre-fix table with each leg's counters, the unclassified count and the ok flag, and states the provenance of every row: eight transcribed from surviving log text, two from a verbatim capture taken before my own validation reruns overwrote them (cross-checked against an offline re-derivation on the same served panels under main's classifier), and one from catalogue section 7.6. The offline re-derivation reproduces the table cell for cell on the nine non-amp_marginal factors, so it stands as an evidence base without having to trust the logs. Corrects section 1.2's characterisation of the bounded warmup cell ceiling. It is (w-1) x |symbols|, a STRUCTURAL upper bound that cannot be exceeded unless the date-boundary predicate itself is broken — a backstop that only fires once another guard has already failed, not a magnitude or density gate. The measured 91.5% occupancy is not "8.5% of headroom"; it is the fraction of symbols with a differing warmup cell, which is structurally near one. And it does NOT close C4a review LOW-1: the class still has no magnitude bound at all, with a measured max rel of 1.996 (a sign flip) inside it. Registers three holes in the evidence base: the first C5 run executed on a classifier predating PR #109 so the ceiling never ran (impact settled — all three bounded factors clear it on current HEAD); the reports leg produced zero judgements for all eleven factors because every rc=1 was F5, making phase B its first observation and leaving the frozen baseline as the A/B/C decomposition's only reference object; and `_make_logger`'s truncate mode as a known operational risk, same failure family as D5a's "one run destroys the only frozen baseline" — not fixed here, that is a behaviour change.
…tors The window and whitelist were derived from TWO factors. The third, peak_ridge_amount_ratio_20, was failing at the time with unclassified=30 and had been recorded as passing in the handoff's results table — so it never entered the evidence base. This is not "widening the class to cover it": the old parameters were fitted to an incomplete sample, and these are derived from all three. How the omission happened is recorded in the catalogue: the reports leg was rc=1 for all eleven factors at once (F5 destroying its inputs), one whole column was uniformly dead, and attention went to which factors carried an F1-F4 label rather than back to the logs for that row. Membership is now stated as the MECHANISM it claims — the symbol's valid-day emission density on that factor is below that factor's own median — with the whitelist as that criterion's enumeration in this window. The claim was falsified rather than assumed: all 18 in-class (factor, symbol) pairs sit below their factor's median, 0 violate it, and no whitelisted name is dense on every affected factor. The sharpest confirmation is an asymmetry the mechanism predicts: 600906.SH is below its median on peak_ridge and above it on ridge, and appears in the former's cluster and not the latter's. Control group: the dense pooled factor volume_peak_count_20 has zero cells on those same names. Two committed tests re-run the check whenever the frozen panels are on disk. The catalogue records an ANTI-RATCHET constraint: this class must not be widened again. Keep relaxing and unclassified can always be driven to zero, at which point the class describes the residual instead of a mechanism and "an uncatalogued difference is a failure" has been repealed by degrees. A future factor needing it is evidence of exactly that -> stop and report. Boundaries: window [2021-11-01, 2021-11-15], nine names, cap unchanged at 20 (measured 13/13/19). Reverse tests re-cut to the new edge from both sides, and the whitelist test now reads an INDEPENDENTLY written expectation - deriving the parametrization from the constant under test could not detect a name being dropped (measured: removing one took the run from 9 passed to 8 passed, green both times).
…n anyone Until phase B this leg had produced ZERO judgements for ten of the eleven factors — every rc=1 was F5 destroying its inputs — so neither drift below had ever been seen. Same shape as the peak_ridge case: the registrations were fitted to a sample of one, not widened after the fact. A. The diagnostics-sink coverage disclosures arrive as an appended section. REGISTERED_EXTRA_SECTIONS held only vpq's, because vpq was the only factor whose reports leg had ever run. Re-verified per factor and per grid rather than assumed: every leaf under the appended index is old=None with no value rewrite; the verdict labels are unchanged; and there is NO INDEX DISPLACEMENT — sections 8 -> 9 with the first eight names pairwise identical and the disclosure appended last. That third check matters because leaves are compared position by position, so a section inserted mid-list would shift every later one and "the rest match" would be an illusion. It is detected rather than assumed, and a test pins the mechanism. B. spec.description value rewrites, registered as per-factor EXACT (old, new) pairs. This is a DIFFERENT class from the section 7 spec drift, which covers added keys; this is an existing key whose value changed. No predicate: a blanket "description rewrites are fine" rule would wave through the day a factor's stated meaning really changes, and spec.description is the field a reader uses to learn what the factor computes. Cause is the D2 taxonomy move, and D2 was measured bitwise identical (PR #89, 14/14 at max_rel_diff=0.0), so this is provenance, not semantics — no version bump, no correction carrier, by the same criterion as F1. Both carry anti-ratchet notes: a further member is evidence the registration describes residuals rather than a mechanism -> stop and report. Two defects found while doing it, both by observation rather than by a test: the displacement test passed for the WRONG REASON (a trailing extra index satisfied its weaker assertion; registering name changes left it green), now asserting the displaced sections[k].name directly; and the Markdown prefixes, derived from dataclasses.fields alone, missed the two COMPUTED properties to_section adds on top of asdict, leaving three rendered lines unregistered with the JSON side already green. That tuple is now author-once in qt.factor_eval_disclosures and a test compares the prefixes against the keys a real section payload actually carries. Reports leg: 11/11 rc=0, zero unregistered leaves. Scope of the rerun was established mechanically, not asserted: an AST diff against the phase-B commit shows the changed functions are exactly _classify_value_change, diff_report_json and run_reports_mode, with every panels/anchors entry point byte-identical.
…sults Sections 3/4/5 now carry the real numbers instead of TBD: per-factor results for all three legs (33 cells, every one rc=0, unclassified and failed both 0), the with_book decomposition, and the eleven-row verdict table. The decomposition is PER METRIC, as required — an aggregate "with_book moved because the book view changed" would let an engine regression hide behind an intended change. Split out, it says something stronger: A and B carry identical deltas on IC / ICIR / NW-t / N_eff (a book does not enter those), and C-B is non-zero on exactly 11 of 57 metric rows, every one of them the incremental ICIR — the only metric a book can reach. Verdicts and all three axis labels are unchanged for 11/11 factors in every grid. Also records jump_amount_corr_20's float tail sitting at 101/101 with ZERO headroom: the cap was calibrated on that factor's own observed count, so one more cell fails the leg, and the failure would read as "cap exceeded" rather than pointing at whatever moved. Header no longer claims to be a skeleton.
HIGH-1. The sparse-tail class was described as a MECHANISM whose whitelist is the mechanism's enumeration. Measured, the density predicate admits 420 + 404 + 426 = 1,250 (factor, symbol) pairs inside the window against the 18 the class covers — roughly 69x. So the predicate is necessary and nowhere near sufficient: it CONSTRAINS the member set without DETERMINING it, and the whitelist is the enumeration of OBSERVED cells. Relabelled as an enumeration class with a necessary-condition guard, in all three places that said otherwise. The label is operational, not cosmetic: "mechanism class" invites the next person to extend it by derivation, which is exactly what must not happen, and the anti-ratchet note now says so explicitly. The guard moves from the median to the 25th percentile. A median gate passes half the universe by construction, so it could hardly fail; all 18 in-class pairs sit below their factor's p25 (worst percentile rank 21.7%, 15 of 18 below 3%), so tightening costs nothing and turns an almost-unfailable check into one that can fail. Mutation evidence both ways: a dense-everywhere name is still caught under p25, and loosening back to the median still passes, which is what shows p25 is strictly tighter on this data. The 600906.SH asymmetry now carries its reach: it is a CONDITIONAL prediction whose NEGATIVE direction alone holds (dense => absent, corroborated by the dense control factor's zero cells), while sparse => present is false by that same 69x — and it is one data point whose dense side clears its median by a single day. HIGH-2. The with_book section claimed "if an engine regression hid in other metrics, C-B would be non-zero there". That inference is VACUOUS and the review demonstrated it: scaling ic_mean by 1.5 in all three books leaves C-B at exactly 0.0, because A/B/C hold identical values on those metrics and a regression moves all three and cancels. The "11 of 57 rows" framing was also a statement about the table's own row selection; at artifact level C-B is non-zero on 174 leaves. Replaced with what the data does support — those 174/174 land in sections[3] plus book_view plus verdict reasons, so the book view's blast radius is bounded — and the report now says plainly that engine regressions are caught by the strict no_book/bookclose gates against the frozen baseline, not by C-B. The refuted inference stays ON THE RECORD with its mutation, as the compare_postmerge empty-reconcile was recorded: the audit conclusion is unchanged, what was wrong is the attribution. LOW-1. The Markdown-prefix test could not fail for the tuple it named: both sides read DERIVED_PAYLOAD_PROPERTIES, so deleting an entry left it green (measured). Its docstring now states that reach, and a new test anchors the tuple to the union of @Property names declared on the coverage classes — which does go red on that exact edit. LOW-2. The report flagged only jump's 101/101. Added the sparse-tail window's right edge (drawn AT its last cell: zero days of headroom) and its cell cap at 19/20, alongside the looser bounds, so a reader can see which boundaries are one step from firing. NIT-1: the appended-section re-verification denominator is 12 grids (4 factors x 3), not 8. NIT-2: registered, not fixed — jump's reports leg and the decision-book grid do not gate VALUE changes (518 of 593 diffs ride the registered correction carrier; the decision grid is report-only), while structural additions and removals still gate. Scope: confirmed empirically rather than argued — perturbing this harness module leaves all five sampled factor code_hash keys unchanged, so the store stays valid and only the reports leg needed re-running (5 affected factors re-run, rc=0, zero unregistered).
The delta review found the old wording surviving in three places the previous pass missed — and the worst one asserted a MEDIAN gate fifteen lines above the code that reads p25, so a reader had two contradicting sentences on one screen. That is the exact defect this rework existed to remove, reappearing because the same claim was written in four places and only three were corrected. Rather than correct a fourth copy, the claim is now AUTHORED ONCE, on SPARSE_VALID_DAY_TAIL_SYMBOLS: the module docstring and the test banner point at it instead of restating it. The two Markdown documents still have to restate — no mechanism shares prose between a Python docstring and a .md file — so they carry an explicit pointer to where the authoritative wording lives. The catalogue's dangling back-reference (it pointed at a heading this rework had already renamed, and quoted the retired claim while doing so) now describes the old title instead of quoting it: copying it verbatim would keep any scan from reaching zero and would let a skimming reader mistake it for the live claim. The "p25 is strictly tighter" mutation record is replaced, because neither entry on file actually tested that claim: "loosen back to the median -> still passes" only shows both gates clear the REGISTERED members, and the injected dense name 000009.SZ fails under the median gate too. The witness that discriminates is a name from the 151-symbol band that sits below the median but at or above p25 — 000031.SZ, 38/38/38 against p25 36 and medians 41/41/40 — which FAILS under p25 and PASSES under the median. Both the new evidence and the old evidence's miss are recorded: this is the same defect as the rest of the round, a piece of evidence that did not support the claim it was filed under. The p25 guard's own headroom joins the near-edge table: its threshold is 36 emitting days and the tightest member has 35, so ONE day. It is flagged the way the other near-edge bounds are, with the note that a red here is stop-and-report and must not be answered by moving the threshold back to the median — the move anti-ratchet forbids, and the one that looks most attractive precisely when the guard fires. Section 6's numbering skipped 8.
The p25 guard's row was inserted without updating the two counts below it — the heading still said 'the first three' above four bullets, and the closing sentence said 'these three'. Same shape as the section-6 numbering skip: an insertion whose counts did not follow. In a PR whose subject is claims matching what is actually there, shipping a table that says three and lists four is the defect it exists to remove.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
D5 C5(四腿全量对账)收口。11 因子 × 3 模式 = 33 格全绿,审计报告填完,C5 通过 ⇒ 满足 C6 的前置条件之一。
结果
unclassified全 0failed全 0stk_mins_live_calls非零命中 0做了什么
vol > thr翻转。窗口/symbol 未动,按因子设相对界(kurtosis ≤5e-3 / relative_vwap ≤1e-5,实测 2.904e-3 / 1.530e-6),cap 25。frozen_finite_new_nan进 warmup 方向集(限 pooled + 早区);新类warmup_sparse_valid_day_tail。_in_warmup分支之前(按机制识别,不按位置)⇒ 月计数天然单调。实测对已绿因子无副作用,被否决的方案②未被启用。_bookclose,删掉事后os.replace。此前 close 模式先用共享 stem 写再移动,decision→close 顺序跑完会销毁 decision 的 with_book 三件套。unclassified=0并排会诱导"容差没牙"的误读)。过程中查出的三个证据基窟窿
这些比修复本身更重要——它们说明"跑绿"曾经只覆盖"我们看过的那部分"。
peak_ridge_amount_ratio_20的 panels 记成通过,而日志是unclassified=30 / ok=False。 它是 F3 同族的第三个因子。⇒ F2/F3/F4 的边界参数原本推自一个残缺证据基。据此对 11 因子 × 3 模式做了表 vs 日志全量对照(33 格,只此 1 格记错)。warmup_ceiling由35de346(07-28 02:08)引入,而幸存的 8 条 07-28 日志行没有一条带这个字段。⇒ bounded warmup 的 cell ceiling 一次都没上过场,而审计骨架把它写成生效中的闸门。本 PR 在当前 HEAD 上重跑,ceiling 真正上场并通过。1全部是 F5 的FileNotFoundError,不是判定结果。 ⇒ 该腿此前只被真正观测过一个因子。本 PR 是它对其余 10 个因子的首次观测——两类此前无人见过的漂移因此第一次露面(诊断 sink 的纯新增 section;D2 迁移导致的spec.descriptionprovenance 改写),两者都不是数值差异,均已登记。评审揪出的两处 overclaim(都已改准)
HIGH-1:「白名单是该密度谓词的枚举」是假的。 谓词在窗口内允许 1,250 对,类实际覆盖 18 对——69 倍差。
处置是改措辞、不改谓词(改谓词会把闸放宽约 69 倍)。该类现在如实标为**「枚举类 + 必要条件守卫」而非机制类**——这个区别是操作性的:读"机制类"的人会以为可以照机制推导着扩。证伪门槛从"低于中位"(按定义约一半的票满足,近乎恒真)收紧到低于 p25,仍 18/18 通过。
600906.SH的不对称证据射程也写准了:只有否定方向(稠密 ⇒ 不进簇)成立,且它在对照因子上只高于中位 1 天,是单点证据。HIGH-2:§四的核心推论是恒真。 「若引擎回归藏在别的指标里,C−B 会在那些行非零」——评审用 mutation 直接证否:把
ic_mean在 A/B/C 三本书里同时 ×1.5,C−B 仍精确为 0.0,因为那四个指标对书构造性免疫,引擎回归会让三者一起动、在差里对消。已替换为真有信息量的版本:artifact 层 C−B 非零共 174 个叶子,全部落在
sections[3]+book_view+ verdict reasons ⇒ 书视图的爆炸半径被限制住,174/174 通过;并明写真正检出引擎回归的是 no_book / bookclose 两格 strict 对冻结基线的逐叶子闸(存在且通过)。这条被证否的推理连同其 mutation 证据留在报告里,没有悄悄换掉——与compare_postmerge.py那次空对账的处理一致。贴边的边界(新增 §三之四)
反棘轮把边界钉死了,读者得知道有多紧:
jumpfloat_tail 101/101(0 格)、sparse-tail 窗口右端 2021-11-15(0 天)、cap 19/20(1 格)、p25 密度守卫 35 天 vs 门槛 36(1 天)。其中 p25 那道门红了就是 STOP-AND-REPORT,不许把门槛放回中位——反棘轮明令禁止,而它红的时候恰恰最诱人(中位门会放行,于是"改一个数就绿了")。
方法论:本轮出现 5 次"证据/清单打不中它要支持的那个主张"
全部如实留档,没有一次是被测试抓住的:
.pyc按 (mtime, size) 校验 ⇒ 等长改写在同一秒内会让"还原"后的源码被变异后的字节码顶替。据此把先前 13 条 mutation 全部重跑——只修 harness 不重跑等于把已污染的证据留在案上。to_section加的两个计算属性;且其覆盖测试两侧同源故对该元组无牙(现已钉到四个 coverage 类@property名字的并集)。000031.SZ:低于中位、高于 p25,p25 下红、中位下放行;带内共 151 只票)。同一主张写四遍、改准三遍漏一遍的情况出现两次;已把权威处收敛到参数所在的 docstring,其余改为指针(两份 Markdown 与 Python docstring 无共享 prose 机制,这个限制如实写明)。
重跑射程(实测,非论证)
扰动
qt/factor_eval_reconcile.py后重算 5 个因子的code_hash→ 5/5 逐字不变;负对照扰动factors/compute/minute/primitives.py(在 shared set 内)→ 5/5 全变 ⇒ 探针非空。harness 在 code_hash 射程之外、store 不失效。Gates(lead 独立重跑,未采信 subagent 自报)
pytest -p no:warnings2606 passed ·ruffclean · phase0 锚 ic 0.9600 / annual 0.8408 ·validate-config32/32 ·exec_baseline_freeze --verify77/77 @ 45c14aa · secret scan 0 · 陈旧措辞 grep 0 · 审计报告TBD0 · 编目对 main 纯追加(0 删除行) · 冻结基线 今日 0 个文件被写已登记、本 PR 不改
_make_logger是 truncate 的(重跑会静默销毁同名日志的唯一副本);jump 的 reports 腿对值改动实际不设闸(593 个 diff 里 518 个走既有更正承载登记 #11,with_book(decision)是 report-only),值级依据应看no_book/bookclose两个 strict 格与 panels 腿。