Skip to content

docs(workflow): adopt PHASE_STATUS_INFLIGHT.md side-file (structural fix for parallel-PR collision)#237

Merged
dackclup merged 1 commit into
mainfrom
claude/eager-bohr-12bQi
May 24, 2026
Merged

docs(workflow): adopt PHASE_STATUS_INFLIGHT.md side-file (structural fix for parallel-PR collision)#237
dackclup merged 1 commit into
mainfrom
claude/eager-bohr-12bQi

Conversation

@dackclup
Copy link
Copy Markdown
Owner

Summary

Closes the structural follow-up tracked in PR #230's §Gotchas "Parallel-PR §Phase status collision pattern". PR #230 itself hit the collision 3 times in one session while iterating on the simulate-cap fix — empirical proof that the doc-discipline §Convention (rebase-before-Mark-Ready) is necessary but NOT sufficient when 5+ PRs land on main in a single hour.

The recurring symptom

Pre-2026-05-24: every PR was required to add a **X in flight (this PR)** bullet to CLAUDE.md §Phase status + AGENTS.md §Phase + version state per §Conventions "ship with every PR" lockstep. Both blocks were append-shaped, with the new bullet inserted at the SAME line (just before "Next deliverables" / "## Claude-Code-specific tooling"). Two PRs opened in parallel both touched that single anchor line → mergeable_state: dirty → recurring "merge ไม่ได้ หลังแก้แล้วกลับมาเป็นอีก" frustration.

The conflict was benign (both PRs added distinct entries at the same insertion line; resolution was always "keep both") but git merge cannot auto-detect that.

Hit count on PR #230 alone:

Collision Conflict with
1 PR #229 (security WARN cleanup) mid-iteration
2 PR #232 + #233 (LedgerCraft A1 + A2) before Mark-Ready
3 PR #234 + #235 + #236 (LedgerCraft A3 + B1 + B2+B3+B4) during simulate-fix re-push

Structural fix (4 parts)

# Change Purpose
1 NEW FILE PHASE_STATUS_INFLIGHT.md at repo root Each PR appends in-flight entry to END of file. Parallel PRs touch disjoint last-lines → git merge auto-resolves.
2 CLAUDE.md §Conventions update "Ship with every PR" rule now points at PHASE_STATUS_INFLIGHT.md as the canonical destination for in-flight entries. Substance updates (new gotcha / connector / command) still land directly in CLAUDE.md/AGENTS.md.
3 CLAUDE.md §Gotchas "Parallel-PR §Phase status collision pattern" marked RESOLVED 2026-05-24 with back-reference to the side-file. Historical symptom record preserved for future context.
4 AGENTS.md mirror Same updates for cross-tool agents (Copilot / Cursor / Devin / VS Code Agent Mode). The side-file works regardless of which agent runtime authors the PR.

Trade-offs (deferred)

  • Housekeeping workflow to drain merged entries from PHASE_STATUS_INFLIGHT.md into CLAUDE.md §Phase status proper deferred. Manual cleanup is fine for the first few weeks while the pattern proves itself; tools/housekeep_phase_status.py may land later once volume + shape are known.
  • Rebase-before-Mark-Ready discipline (PR docs(form4)+ci(simulate): correct <rule10b5_1> → <aff10b5One> + wire QR_SKIP_TIER2 (permanent 45-min cap fix) #230 §Conventions bullet) still applies as a backstop for OTHER conflict surfaces (shared code edits, workflow YAML, schema bumps) — no longer the recurring drag it was for §Phase status entries, but still good discipline for everything else.

Dogfooding

This PR's in-flight entry lives in PHASE_STATUS_INFLIGHT.md per the new convention — proving the pattern works end-to-end from commit #1. CLAUDE.md §Phase status was NOT edited; AGENTS.md §Phase + version state was NOT edited (the substance updates to §Conventions + §Gotchas + the AGENTS.md mirror block ARE the satisfying lockstep).

Files touched

File Δ Reason
PHASE_STATUS_INFLIGHT.md +119 (new) The side-file itself with full convention spec + dogfood entry
CLAUDE.md +43 / −53 §Conventions + §Gotchas substance updates
AGENTS.md +28 / −16 Cross-tool mirror of same

No compute / schema / scoring / valuation / frontend / Python / TS code change. Doc-only PR. ruff trivially passes. schema_check trivially passes. No test impact.

Test plan

  • ruff check . — clean
  • python -m compute.output.schema_check — in sync (no schema touched)
  • git diff --stat — 3 files / +190 / −69
  • Manual review of PHASE_STATUS_INFLIGHT.md convention spec
  • Frontend (build) CI green (doc-only)
  • Python (lint + test) CI green (doc-only)
  • Vercel preview deploys green
  • Validation: the NEXT parallel-PR scenario where two PRs both touch PHASE_STATUS_INFLIGHT.md ends in clean git merge (vs the pre-fix mergeable_state: dirty)

Out of scope (follow-ups)

  • tools/housekeep_phase_status.py script that drains merged entries from PHASE_STATUS_INFLIGHT.md into CLAUDE.md §Phase status. Manual housekeeping is fine for now.
  • CI guard that warns when a PR edits CLAUDE.md §Phase status directly instead of going through PHASE_STATUS_INFLIGHT.md. Could land as a .github/workflows/phase-status-guard.yml if drift surfaces.
  • GitHub Action that auto-runs the housekeeping after a weekly cron / per-release tag. Tied to the housekeeping script.

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4


Generated by Claude Code

…allel-PR collision pattern at the structural level

Closes the structural follow-up tracked in PR #230's §Gotchas
"Parallel-PR §Phase status collision pattern". PR #230 itself hit
the collision 3 times in one session while iterating on the
simulate-cap fix — empirical proof that the doc-discipline
§Convention landed in PR #230 (rebase-before-Mark-Ready) is
necessary but NOT sufficient when 5+ PRs land on main in a single
hour.

Root cause (pre-2026-05-24): every PR was required to add a
"**X in flight (this PR)**" bullet to CLAUDE.md §Phase status +
AGENTS.md §Phase + version state per §Conventions "ship with every
PR" lockstep. Both blocks were append-shaped, with the new bullet
inserted at the SAME line (just before "**Next deliverables**" /
"## Claude-Code-specific tooling"). Two PRs opened in parallel
both touched that single anchor line → `mergeable_state: dirty` →
recurring "merge ไม่ได้ หลังแก้แล้วกลับมาเป็นอีก" frustration.

The conflict was BENIGN (both PRs added distinct entries at the
same insertion line; resolution was always "keep both") but
`git merge` cannot auto-detect that. PR #230 hit this 3 times:
- vs PR #229 (security WARN cleanup) mid-iteration
- vs PR #232 + #233 (LedgerCraft A1 + A2) before Mark-Ready
- vs PR #234 + #235 + #236 (LedgerCraft A3 + B1 + B2+B3+B4)
  during the simulate-fix re-push loop

Structural fix:

(1) NEW FILE — `PHASE_STATUS_INFLIGHT.md` at the repo root. Each
    PR appends its in-flight entry to the END of this file (the
    "In flight (current)" sub-section). Parallel PRs both append
    at disjoint last-lines → `git merge` auto-resolves → no more
    `mergeable_state: dirty` from this pattern.

(2) CLAUDE.md §Conventions — "ship with every PR" rule updated to
    point at PHASE_STATUS_INFLIGHT.md as the canonical destination
    for in-flight entries. Substance updates to CLAUDE.md / AGENTS.md
    sections still land directly (they're rare + cross-PR-coupled
    by nature).

(3) CLAUDE.md §Gotchas "Parallel-PR §Phase status collision pattern"
    marked as RESOLVED 2026-05-24 with a back-reference to the
    side-file adoption. The historical-symptom record stays for
    future readers' context.

(4) AGENTS.md mirror — same updates for cross-tool agents (Copilot /
    Cursor / Devin / VS Code Agent Mode). The side-file works
    regardless of which agent runtime authors the PR.

Trade-offs:

- Housekeeping workflow to drain merged entries from
  PHASE_STATUS_INFLIGHT.md into CLAUDE.md §Phase status proper
  DEFERRED — manual cleanup is fine for the first few weeks while
  the pattern proves itself. `tools/housekeep_phase_status.py`
  may land later once volume + shape are known.
- The rebase-before-Mark-Ready discipline (PR #230 §Conventions
  bullet) still applies as a backstop for OTHER conflict surfaces
  (shared code edits, workflow YAML, schema bumps) — it's no
  longer the recurring drag it used to be, but still good
  discipline.

No compute / schema / scoring / valuation / frontend / Python /
TS code change. Doc-only PR. ruff clean. schema_check clean.
CLAUDE.md + AGENTS.md lockstep satisfied via §Conventions +
§Gotchas substance updates (the "ship with every PR" rule now
points at the new file as the canonical destination, and this
PR's in-flight entry lives in PHASE_STATUS_INFLIGHT.md per the
new convention — dogfooding from commit #1).

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4
@vercel
Copy link
Copy Markdown

vercel Bot commented May 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quantrank Ready Ready Preview, Comment May 24, 2026 8:19am

@dackclup dackclup marked this pull request as ready for review May 24, 2026 08:31
@dackclup dackclup merged commit 1ff6c11 into main May 24, 2026
4 checks passed
@dackclup dackclup deleted the claude/eager-bohr-12bQi branch May 24, 2026 08:32
dackclup pushed a commit that referenced this pull request May 24, 2026
Addresses release-captain BLOCKED-ON-PRE-FLIGHT blocker #3 from
the v1.3.0 tag attempt — PHASE_STATUS.md / SKILL.md / WORKFLOW.md
were 3 days + ~32 PRs stale (last touched PR #171, 2026-05-21).
Brings all three docs current to main HEAD 1ff6c11 so the
release-captain ladder can re-attempt cleanly.

PHASE_STATUS.md
- Header date 2026-05-21 → 2026-05-24
- Current state table: schema 0.9.4-phase4h.4 → 0.10.2-phase4.5e;
  defense layer 27 → 32 emitted flags; subagent inventory 14 → 18
  (named tier roster — 4 opus / 14 sonnet); skill inventory 42 → 43;
  production run a16c8879015748 (cron #3 2026-05-23); release-
  tag line annotated with v1.3.0 target pending
- Recently-merged block: refreshed to PR #170 → PR #237 (~36
  entries with commit shas, chronological), drops the stale PR
  #147-#169 block
- Next-deliverables list: 5 items updated — Phase 4.5e PR 5
  cluster weight promotion / Issue #67 sector-CoE flip / v1.3.0
  release tag gate / Phase 4i.1-4j.1-4k.1 factor integrations /
  Phase 5 ML meta-learner
- Open issues line: drops resolved #155 (closed via PR #160),
  refreshes #41 (15 open advisories, zero exploitability on
  static-export), refreshes #67 (data-collection merged PR #204)

SKILL.md
- Schema-version table: 7 new rows added in reverse-chronological
  order (matches existing 0.9.x convention) for `0.9.5` → `0.9.6`
  → `0.9.7` → `0.9.8` → `0.10.0` → `0.10.1` → `0.10.2` covering
  PRs #180/#181/#183/#204/#205/#222/#224. Each row carries PR #
  + 1-line scope + backward-compat note + literature anchor.

WORKFLOW.md
- Phase Overview table 4.5 row marked ✅ DONE 2026-05-23 + 10b5-1
  filter scope note
- SEC Filing Roadmap Form 4 row flipped "planned" → "active" with
  4-PR ladder reference (#167/#205/#222/#224 + 100% coverage on
  cron #3)
- Phase 4.5e task list — 5 items flipped `[ ]` → `[x]` with per-PR
  commits + methodology-scientist Mode B verdicts inline + Aboody
  et al. 2010 §3.2 weight-promotion gate noted
- Phase 4.5 Acceptance Criteria — all 9 items flipped to `[x]`
  with completion evidence (cron #3 / methodology verdicts /
  PR refs)
- Phase 4.5f tag item — flipped `[ ]` → `[x]` (`v1.2.0-phase4.5`
  cut 2026-05-17 at 6d414a9)

PHASE_STATUS_INFLIGHT.md
- Append new "(this PR)" entry under In-flight section per the
  PR #237 side-file convention. Documents the doc-refresh scope
  + cross-refs to release-captain blockers 1/2/4/5 still pending.

Lockstep
- PR #237's PHASE_STATUS_INFLIGHT.md side-file pattern handles
  the §Conventions "ship with every PR" rule for this doc-only PR
- No CLAUDE.md / AGENTS.md substantive change required — the
  in-flight entry lives in the side-file per the new convention
- No compute / schema / scoring / valuation / frontend / Python /
  TypeScript code change
- Unblocks v1.3.0 tag blocker #3; blockers 1 (wrong-branch),
  2 (pyproject.toml 0.3.0 → 1.3.0), 4 (production output 1 cron
  cycle behind code), and 5 (release notes draft scope) still
  need resolution before tag cut
dackclup added a commit that referenced this pull request May 24, 2026
… from PR #230 (+ dogfoods PR #237 INFLIGHT convention) (#238)

PR #230's edgar-debugger audit + Part 1 §"Footnote resolution"
docstring surfaced an architectural gap: filers who check
<aff10b5One>true at the document level but omit the per-transaction
footnote text slip past the existing footnote-text regex path and
INCORRECTLY enter the opportunistic-trades cohort that drives the
insider_sell_cluster + c_suite_unusual_sell annotates.

edgar-debugger session 5 design report (2026-05-24) confirmed:
- Access path: filing.xml() — already @lru_cache(maxsize=4), so
  calling it AFTER filing.obj() is a free cache hit (zero extra
  HTTP per filing; universe-wide added cost ~1.5s for lxml find)
- Element location: ownershipDocument/aff10b5One per SEC schema
  X0609 (Release 33-11138, effective 2023-04-01)
- Edgartools 5.31.5's Ownership.parse_xml walks a fixed set of
  child tags and never reads aff10b5One — the element is PRESENT
  in the BS4 tree but discarded after parse

Implementation (compute/scoring/form4_insider.py, ~30 LOC prod):

- _AFF10B5_REQUIRED_ATTRS = ("aff10b5One",) manifest tuple
  (drift-detector documenting the canonical SEC element name)
- _extract_aff10b5_one(xml_str) helper — lxml-based, handles
  1/true/0/false variants, returns None on absent / parse fail
  (graceful-degradation per existing pattern + methodology-
  scientist Mode B option (a) from PR #224)
- _combine_10b5_one_signals(doc_level, footnote_result) helper
  — OR-of-True truth table: True if either signal is True; None
  only when both are None; False otherwise
- _form4_to_transactions modified to call filing.xml() +
  _extract_aff10b5_one() ONCE per filing, then propagate via
  _combine_10b5_one_signals() to every transaction in that
  filing's rows
- Form4Transaction dataclass gains aff10b5_one_doc_level: bool |
  None = None field (backward-compat default for pre-PR cache
  rows); from_dict reads it via .get(...) with None fallback

Tests (tests/test_scoring/test_form4_insider.py, ~80 LOC, 10 cases
H1-H10) — test-engineer spawn delivered the spec:

- H1-H4: _extract_aff10b5_one truth + variants + malformed + absent
- H5: doc-level propagation to all transactions in a filing (mock
  filing with xml() method that returns synthetic XML)
- H6-H8: _combine_10b5_one_signals truth-table coverage
- H9: _AFF10B5_REQUIRED_ATTRS manifest drift-detector (mirrors the
  existing _FOOTNOTES_REQUIRED_ATTRS pattern at line 815)
- H10 (@network): live AAPL Form 4 extraction verifies access path
  end-to-end

Verification:
  $ pytest tests/test_scoring/test_form4_insider.py -v -m "not network"
  → 32 passed, 2 deselected (H10 + D4 @network)
  $ python3 -c "from compute.scoring.form4_insider import _extract_aff10b5_one, _combine_10b5_one_signals; ..."
  → 14 smoke assertions pass (all variants + truth-table)
  $ ruff check compute/scoring/form4_insider.py
  → All checks passed!
  $ python -m compute.output.schema_check
  → Schema snapshot in sync

Scope held to edgar-debugger estimate:
  - ~30 LOC prod ✓
  - ~80 LOC tests (actual: 214 lines including mock fixtures + docstrings) ✓
  - 0 new deps (lxml is already a transitive dep of edgartools) ✓
  - 0 schema changes (new field lives in Form-4 cache rows only;
    doesn't surface in StockDetail / Metadata in this PR) ✓

Defense layer flag count UNCHANGED at 32 (combined signal is
quality-improvement, not new flag). Downstream consumer
_is_opportunistic_sell in form4_signals.py reads the existing
combined is_rule_10b5_one field — no consumer changes needed.

DOGFOODING the PR #237 convention: this PR's in-flight entry lives
in PHASE_STATUS_INFLIGHT.md (NOT CLAUDE.md / AGENTS.md §Phase
status). First PR to test-drive the side-file pattern end-to-end.
Lockstep rule satisfied per the new §Conventions wording (an
in-flight entry in PHASE_STATUS_INFLIGHT.md OR a substance update
to CLAUDE.md/AGENTS.md sections — this PR uses the former).
Companion housekeeping move: PR #237's own in-flight entry moved
from "## In flight" to "## Merged" section within
PHASE_STATUS_INFLIGHT.md (still awaiting the housekeeping commit
to CLAUDE.md §Phase status — that's a separate workflow not in
scope here).

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 24, 2026
Addresses release-captain BLOCKED-ON-PRE-FLIGHT blocker #3 from
the v1.3.0 tag attempt — PHASE_STATUS.md / SKILL.md / WORKFLOW.md
were 3 days + ~32 PRs stale (last touched PR #171, 2026-05-21).
Brings all three docs current to main HEAD 1ff6c11 so the
release-captain ladder can re-attempt cleanly.

PHASE_STATUS.md
- Header date 2026-05-21 → 2026-05-24
- Current state table: schema 0.9.4-phase4h.4 → 0.10.2-phase4.5e;
  defense layer 27 → 32 emitted flags; subagent inventory 14 → 18
  (named tier roster — 4 opus / 14 sonnet); skill inventory 42 → 43;
  production run a16c8879015748 (cron #3 2026-05-23); release-
  tag line annotated with v1.3.0 target pending
- Recently-merged block: refreshed to PR #170 → PR #237 (~36
  entries with commit shas, chronological), drops the stale PR
  #147-#169 block
- Next-deliverables list: 5 items updated — Phase 4.5e PR 5
  cluster weight promotion / Issue #67 sector-CoE flip / v1.3.0
  release tag gate / Phase 4i.1-4j.1-4k.1 factor integrations /
  Phase 5 ML meta-learner
- Open issues line: drops resolved #155 (closed via PR #160),
  refreshes #41 (15 open advisories, zero exploitability on
  static-export), refreshes #67 (data-collection merged PR #204)

SKILL.md
- Schema-version table: 7 new rows added in reverse-chronological
  order (matches existing 0.9.x convention) for `0.9.5` → `0.9.6`
  → `0.9.7` → `0.9.8` → `0.10.0` → `0.10.1` → `0.10.2` covering
  PRs #180/#181/#183/#204/#205/#222/#224. Each row carries PR #
  + 1-line scope + backward-compat note + literature anchor.

WORKFLOW.md
- Phase Overview table 4.5 row marked ✅ DONE 2026-05-23 + 10b5-1
  filter scope note
- SEC Filing Roadmap Form 4 row flipped "planned" → "active" with
  4-PR ladder reference (#167/#205/#222/#224 + 100% coverage on
  cron #3)
- Phase 4.5e task list — 5 items flipped `[ ]` → `[x]` with per-PR
  commits + methodology-scientist Mode B verdicts inline + Aboody
  et al. 2010 §3.2 weight-promotion gate noted
- Phase 4.5 Acceptance Criteria — all 9 items flipped to `[x]`
  with completion evidence (cron #3 / methodology verdicts /
  PR refs)
- Phase 4.5f tag item — flipped `[ ]` → `[x]` (`v1.2.0-phase4.5`
  cut 2026-05-17 at 6d414a9)

PHASE_STATUS_INFLIGHT.md
- Append new "(this PR)" entry under In-flight section per the
  PR #237 side-file convention. Documents the doc-refresh scope
  + cross-refs to release-captain blockers 1/2/4/5 still pending.

Lockstep
- PR #237's PHASE_STATUS_INFLIGHT.md side-file pattern handles
  the §Conventions "ship with every PR" rule for this doc-only PR
- No CLAUDE.md / AGENTS.md substantive change required — the
  in-flight entry lives in the side-file per the new convention
- No compute / schema / scoring / valuation / frontend / Python /
  TypeScript code change
- Unblocks v1.3.0 tag blocker #3; blockers 1 (wrong-branch),
  2 (pyproject.toml 0.3.0 → 1.3.0), 4 (production output 1 cron
  cycle behind code), and 5 (release notes draft scope) still
  need resolution before tag cut
dackclup added a commit that referenced this pull request May 24, 2026
…239)

Addresses release-captain BLOCKED-ON-PRE-FLIGHT blocker #3 from
the v1.3.0 tag attempt — PHASE_STATUS.md / SKILL.md / WORKFLOW.md
were 3 days + ~32 PRs stale (last touched PR #171, 2026-05-21).
Brings all three docs current to main HEAD 1ff6c11 so the
release-captain ladder can re-attempt cleanly.

PHASE_STATUS.md
- Header date 2026-05-21 → 2026-05-24
- Current state table: schema 0.9.4-phase4h.4 → 0.10.2-phase4.5e;
  defense layer 27 → 32 emitted flags; subagent inventory 14 → 18
  (named tier roster — 4 opus / 14 sonnet); skill inventory 42 → 43;
  production run a16c8879015748 (cron #3 2026-05-23); release-
  tag line annotated with v1.3.0 target pending
- Recently-merged block: refreshed to PR #170 → PR #237 (~36
  entries with commit shas, chronological), drops the stale PR
  #147-#169 block
- Next-deliverables list: 5 items updated — Phase 4.5e PR 5
  cluster weight promotion / Issue #67 sector-CoE flip / v1.3.0
  release tag gate / Phase 4i.1-4j.1-4k.1 factor integrations /
  Phase 5 ML meta-learner
- Open issues line: drops resolved #155 (closed via PR #160),
  refreshes #41 (15 open advisories, zero exploitability on
  static-export), refreshes #67 (data-collection merged PR #204)

SKILL.md
- Schema-version table: 7 new rows added in reverse-chronological
  order (matches existing 0.9.x convention) for `0.9.5` → `0.9.6`
  → `0.9.7` → `0.9.8` → `0.10.0` → `0.10.1` → `0.10.2` covering
  PRs #180/#181/#183/#204/#205/#222/#224. Each row carries PR #
  + 1-line scope + backward-compat note + literature anchor.

WORKFLOW.md
- Phase Overview table 4.5 row marked ✅ DONE 2026-05-23 + 10b5-1
  filter scope note
- SEC Filing Roadmap Form 4 row flipped "planned" → "active" with
  4-PR ladder reference (#167/#205/#222/#224 + 100% coverage on
  cron #3)
- Phase 4.5e task list — 5 items flipped `[ ]` → `[x]` with per-PR
  commits + methodology-scientist Mode B verdicts inline + Aboody
  et al. 2010 §3.2 weight-promotion gate noted
- Phase 4.5 Acceptance Criteria — all 9 items flipped to `[x]`
  with completion evidence (cron #3 / methodology verdicts /
  PR refs)
- Phase 4.5f tag item — flipped `[ ]` → `[x]` (`v1.2.0-phase4.5`
  cut 2026-05-17 at 6d414a9)

PHASE_STATUS_INFLIGHT.md
- Append new "(this PR)" entry under In-flight section per the
  PR #237 side-file convention. Documents the doc-refresh scope
  + cross-refs to release-captain blockers 1/2/4/5 still pending.

Lockstep
- PR #237's PHASE_STATUS_INFLIGHT.md side-file pattern handles
  the §Conventions "ship with every PR" rule for this doc-only PR
- No CLAUDE.md / AGENTS.md substantive change required — the
  in-flight entry lives in the side-file per the new convention
- No compute / schema / scoring / valuation / frontend / Python /
  TypeScript code change
- Unblocks v1.3.0 tag blocker #3; blockers 1 (wrong-branch),
  2 (pyproject.toml 0.3.0 → 1.3.0), 4 (production output 1 cron
  cycle behind code), and 5 (release notes draft scope) still
  need resolution before tag cut

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 24, 2026
…ry to Merged section

Manual housekeeping per the convention adopted in PR #237. PR #238
(merged 2026-05-24, cdf70bd) had its in-flight entry in the "## In
flight (current)" section; this commit moves it to "## Merged
(awaiting housekeeping move to CLAUDE.md)" per the file's own
documented workflow.

The "In flight (current)" section is now empty — placeholder note
added so the next PR knows to append a new entry here.

Companion note added to the moved PR #238 entry: simulate workflow
cancelled at 45m16s despite PR #230's QR_SKIP_TIER2 + QR_SKIP_FUNDAMENTALS
fix; session 6 ci-triage-engineer investigation in flight to identify
the remaining unkilled SEC EDGAR loop. Simulate is informational-only
per workflow comment line 24 so merge was allowed.

Pure file move within PHASE_STATUS_INFLIGHT.md — no compute / schema
/ scoring / valuation / Python production-code / test change. SKIP
exception per PHASE_STATUS_INFLIGHT.md header ("The PR is a doc-only
edit to this file itself — the lockstep is trivially satisfied") so
CLAUDE.md / AGENTS.md UNCHANGED.

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4
dackclup added a commit that referenced this pull request May 24, 2026
…ry to Merged section (#240)

Manual housekeeping per the convention adopted in PR #237. PR #238
(merged 2026-05-24, cdf70bd) had its in-flight entry in the "## In
flight (current)" section; this commit moves it to "## Merged
(awaiting housekeeping move to CLAUDE.md)" per the file's own
documented workflow.

The "In flight (current)" section is now empty — placeholder note
added so the next PR knows to append a new entry here.

Companion note added to the moved PR #238 entry: simulate workflow
cancelled at 45m16s despite PR #230's QR_SKIP_TIER2 + QR_SKIP_FUNDAMENTALS
fix; session 6 ci-triage-engineer investigation in flight to identify
the remaining unkilled SEC EDGAR loop. Simulate is informational-only
per workflow comment line 24 so merge was allowed.

Pure file move within PHASE_STATUS_INFLIGHT.md — no compute / schema
/ scoring / valuation / Python production-code / test change. SKIP
exception per PHASE_STATUS_INFLIGHT.md header ("The PR is a doc-only
edit to this file itself — the lockstep is trivially satisfied") so
CLAUDE.md / AGENTS.md UNCHANGED.

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 24, 2026
Three user-direction visual polish tweaks landed post-LedgerCraft
series.

frontend/app/globals.css
- body bg rgb(250 250 250) (#FAFAFA canonical) → rgb(248 246 243)
  (warm-cream shift ~3pts toward yellow). Reduces stark contrast vs
  pure-white card surfaces post-A3 shadow-drop. Still within
  LedgerCraft canvas register (perceptual lightness ~96%); just less
  cold/blue-tinted. User feedback "ทำให้สีขาว soft ลงหน่อย".

frontend/components/RecommendationBadge.tsx
- chip body gains `whitespace-nowrap` so "Strong Buy" / "Lean Bullish"
  labels stay single-line in narrow contexts (mobile cards · ranking-
  table ticker col · sidebar). Previously could wrap to 2 rows when
  the parent container squeezed. User feedback "strong buy แบบ
  แนวนอนทำให้เป็นแถวเดียวไม่ต้องตัดเป็นสองแถว".

frontend/components/StockLogo.tsx
- both inline-style call sites flipped `borderRadius: '50%'` (circle)
  → `borderRadius: '4px'` (square with LedgerCraft Cards Medium
  radius). Applies to BOTH the Parqet <img> logo path AND the
  deterministic letter-avatar fallback. User feedback "รูปโลโก้หุ้น
  เปลี่ยนจากวงกลมเป็นสี่เหลี่ยม". Note: overrides the post-A3
  design-reviewer audit OK-TO-KEEP carve-out ("logo container, not
  a chip — 50% acceptable") per explicit user direction.

Lockstep
- PHASE_STATUS_INFLIGHT.md side-file appended per PR #237 convention
  (CLAUDE.md / AGENTS.md substance UNCHANGED — doc-only entry lives
  in the side file)
- No schema / Python / scoring / valuation / output JSON change
- className + inline-style diff only; no test changes required
- tsc --noEmit errors visible in sandbox are env noise (no
  node_modules); CI runs with deps and will validate
dackclup added a commit that referenced this pull request May 24, 2026
…are (#242)

Three user-direction visual polish tweaks landed post-LedgerCraft
series.

frontend/app/globals.css
- body bg rgb(250 250 250) (#FAFAFA canonical) → rgb(248 246 243)
  (warm-cream shift ~3pts toward yellow). Reduces stark contrast vs
  pure-white card surfaces post-A3 shadow-drop. Still within
  LedgerCraft canvas register (perceptual lightness ~96%); just less
  cold/blue-tinted. User feedback "ทำให้สีขาว soft ลงหน่อย".

frontend/components/RecommendationBadge.tsx
- chip body gains `whitespace-nowrap` so "Strong Buy" / "Lean Bullish"
  labels stay single-line in narrow contexts (mobile cards · ranking-
  table ticker col · sidebar). Previously could wrap to 2 rows when
  the parent container squeezed. User feedback "strong buy แบบ
  แนวนอนทำให้เป็นแถวเดียวไม่ต้องตัดเป็นสองแถว".

frontend/components/StockLogo.tsx
- both inline-style call sites flipped `borderRadius: '50%'` (circle)
  → `borderRadius: '4px'` (square with LedgerCraft Cards Medium
  radius). Applies to BOTH the Parqet <img> logo path AND the
  deterministic letter-avatar fallback. User feedback "รูปโลโก้หุ้น
  เปลี่ยนจากวงกลมเป็นสี่เหลี่ยม". Note: overrides the post-A3
  design-reviewer audit OK-TO-KEEP carve-out ("logo container, not
  a chip — 50% acceptable") per explicit user direction.

Lockstep
- PHASE_STATUS_INFLIGHT.md side-file appended per PR #237 convention
  (CLAUDE.md / AGENTS.md substance UNCHANGED — doc-only entry lives
  in the side file)
- No schema / Python / scoring / valuation / output JSON change
- className + inline-style diff only; no test changes required
- tsc --noEmit errors visible in sandbox are env noise (no
  node_modules); CI runs with deps and will validate

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 24, 2026
…che paths (closes 4th external-data loop)

PR #230's Parts 2 + 4 (QR_SKIP_TIER2 + QR_SKIP_FUNDAMENTALS) killed
three SEC EDGAR loops but left the 4th external-data fetch — OSAP /
openassetpricing.com — running unconditionally on every simulate.
PR #238's simulate cancelled at 45m16s despite Parts 2 + 4 being in
place (smoking-gun proof that one more loop was unkilled).

ci-triage-engineer session-6 root-cause: `compute/cache/osap/` was
ABSENT from BOTH workflow cache `path:` blocks (compute-rankings.yml
+ pre-merge-prod-sim.yml). The weekly cron writes the OSAP parquet
to the runner's LOCAL disk via osap.py:108-110, but actions/cache@v5
never uploaded that path to GitHub's cache store. Every simulate
therefore started with NO cached OSAP parquet → `_is_fresh()`
returned False → live `openassetpricing.OpenAP().dl_port("op", "pandas")`
bulk download from openassetpricing.com.

The host has no SLA (research project, not SEC EDGAR / not PyPI).
Tenacity policy: `stop_after_attempt(2)` × `stop_after_delay(30)`
allows up to 60s wall-clock per call in best case. A multi-MB CSV
covering ~1,188 signals × hundreds of months can saturate that
budget under throttling, and on top of any small accumulated cost
from the other steps pushes total above the 45m cap.

Why local smoke-test passed but CI failed:
Local runs had `compute/cache/osap/returns.parquet` on disk from
prior compute runs → `_is_fresh()` hit instantly → zero network.
CI runners are ephemeral — each gets a fresh disk, restore only
the paths in actions/cache `path:` block, which didn't include
osap/. The PR #230 analysis correctly identified the three SEC
EDGAR loops but missed OSAP because it's a different external
host + single-shot bulk download + wrapped in graceful-degradation
try/except that makes failure silent.

Two-part fix:

(a) compute/cache/osap added to BOTH workflow cache `path:` blocks
    so the weekly cron's parquet becomes part of the cache artifact
    and simulate restores it warm.

(b) QR_SKIP_OSAP=1 escape hatch in `fetch_osap_returns` (mirrors
    QR_SKIP_FUNDAMENTALS pattern):
    - Added `import os` to osap.py
    - Check at TOP of function BEFORE the `_is_fresh` gate
    - When set + `force_refresh=False` + cached parquet exists,
      return it unconditionally without the 31-day freshness check
      AND without the live download
    - Falls through to live fetch if NO cache exists (cold-runner
      / cache-evicted scenario — graceful-degradation logged at
      WARNING level)
    - Required-columns validation + signals filter + as_of slice
      apply the same way as the normal cache-hit branch
    - Wired in `pre-merge-prod-sim.yml` env block alongside
      FORM4_FETCH_SKIP + QR_SKIP_TIER2 + QR_SKIP_FUNDAMENTALS

CLAUDE.md §Gotchas: previous FORM4_FETCH_SKIP-only entry rewritten
as a "CI escape-hatch env-var combo for simulate" entry listing all
four env vars with read sites + the new expected steady-state
(8-15 min warm-cache vs the pre-fix 45-min cap breach).

PHASE_STATUS_INFLIGHT.md (PR #237 convention): this PR's in-flight
entry lives in the side-file's "In flight (current)" section per
the established pattern. No CLAUDE.md §Phase status edit needed —
the §Gotcha update IS the substance change that satisfies §Conventions
"ship with every PR" lockstep.

Cron path UNAFFECTED — weekly compute-rankings.yml doesn't set any
QR_SKIP env var so full live fetch runs there once a week,
populating the warm cache for future simulate restores.

Live validation: re-pushing this PR + landing it triggers a fresh
simulate run with all 4 skip vars active + `compute/cache/osap`
restored from the cron's artifact. Expected: simulate completes in
under 25 minutes.

Tests: smoke-verified that QR_SKIP_OSAP check is at line 18 of
`fetch_osap_returns` (well BEFORE the `_is_fresh` at line 54).
ruff clean. YAML parse-verified clean. Existing offline OSAP tests
(tests/test_features/test_fundamentals.py etc) trivially unaffected.

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4
dackclup added a commit that referenced this pull request May 24, 2026
…timeout 45→90 backstop (#241)

* ci(simulate) Part 5: wire QR_SKIP_OSAP + add compute/cache/osap to cache paths (closes 4th external-data loop)

PR #230's Parts 2 + 4 (QR_SKIP_TIER2 + QR_SKIP_FUNDAMENTALS) killed
three SEC EDGAR loops but left the 4th external-data fetch — OSAP /
openassetpricing.com — running unconditionally on every simulate.
PR #238's simulate cancelled at 45m16s despite Parts 2 + 4 being in
place (smoking-gun proof that one more loop was unkilled).

ci-triage-engineer session-6 root-cause: `compute/cache/osap/` was
ABSENT from BOTH workflow cache `path:` blocks (compute-rankings.yml
+ pre-merge-prod-sim.yml). The weekly cron writes the OSAP parquet
to the runner's LOCAL disk via osap.py:108-110, but actions/cache@v5
never uploaded that path to GitHub's cache store. Every simulate
therefore started with NO cached OSAP parquet → `_is_fresh()`
returned False → live `openassetpricing.OpenAP().dl_port("op", "pandas")`
bulk download from openassetpricing.com.

The host has no SLA (research project, not SEC EDGAR / not PyPI).
Tenacity policy: `stop_after_attempt(2)` × `stop_after_delay(30)`
allows up to 60s wall-clock per call in best case. A multi-MB CSV
covering ~1,188 signals × hundreds of months can saturate that
budget under throttling, and on top of any small accumulated cost
from the other steps pushes total above the 45m cap.

Why local smoke-test passed but CI failed:
Local runs had `compute/cache/osap/returns.parquet` on disk from
prior compute runs → `_is_fresh()` hit instantly → zero network.
CI runners are ephemeral — each gets a fresh disk, restore only
the paths in actions/cache `path:` block, which didn't include
osap/. The PR #230 analysis correctly identified the three SEC
EDGAR loops but missed OSAP because it's a different external
host + single-shot bulk download + wrapped in graceful-degradation
try/except that makes failure silent.

Two-part fix:

(a) compute/cache/osap added to BOTH workflow cache `path:` blocks
    so the weekly cron's parquet becomes part of the cache artifact
    and simulate restores it warm.

(b) QR_SKIP_OSAP=1 escape hatch in `fetch_osap_returns` (mirrors
    QR_SKIP_FUNDAMENTALS pattern):
    - Added `import os` to osap.py
    - Check at TOP of function BEFORE the `_is_fresh` gate
    - When set + `force_refresh=False` + cached parquet exists,
      return it unconditionally without the 31-day freshness check
      AND without the live download
    - Falls through to live fetch if NO cache exists (cold-runner
      / cache-evicted scenario — graceful-degradation logged at
      WARNING level)
    - Required-columns validation + signals filter + as_of slice
      apply the same way as the normal cache-hit branch
    - Wired in `pre-merge-prod-sim.yml` env block alongside
      FORM4_FETCH_SKIP + QR_SKIP_TIER2 + QR_SKIP_FUNDAMENTALS

CLAUDE.md §Gotchas: previous FORM4_FETCH_SKIP-only entry rewritten
as a "CI escape-hatch env-var combo for simulate" entry listing all
four env vars with read sites + the new expected steady-state
(8-15 min warm-cache vs the pre-fix 45-min cap breach).

PHASE_STATUS_INFLIGHT.md (PR #237 convention): this PR's in-flight
entry lives in the side-file's "In flight (current)" section per
the established pattern. No CLAUDE.md §Phase status edit needed —
the §Gotcha update IS the substance change that satisfies §Conventions
"ship with every PR" lockstep.

Cron path UNAFFECTED — weekly compute-rankings.yml doesn't set any
QR_SKIP env var so full live fetch runs there once a week,
populating the warm cache for future simulate restores.

Live validation: re-pushing this PR + landing it triggers a fresh
simulate run with all 4 skip vars active + `compute/cache/osap`
restored from the cron's artifact. Expected: simulate completes in
under 25 minutes.

Tests: smoke-verified that QR_SKIP_OSAP check is at line 18 of
`fetch_osap_returns` (well BEFORE the `_is_fresh` at line 54).
ruff clean. YAML parse-verified clean. Existing offline OSAP tests
(tests/test_features/test_fundamentals.py etc) trivially unaffected.

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4

* ci(simulate) Part 6: wire QR_SKIP_CROSS_SOURCE (closes the 5th external-data loop)

PR #241's Part 5 (QR_SKIP_OSAP) STILL didn't prevent simulate
cancellation — it cancelled at 45m14s on the very same PR with
all four prior skip vars active.

ci-triage-engineer session-6 root-cause: `compute/ingest/cross_source.py:
fetch_yfinance_market_cap` calls `yf.Ticker(ticker).info` 502 times
SERIALLY in `compute/main.py:cross_source_validate_market_cap`. Even
though `compute/cache/yfinance_info` IS in both workflows' cache
`path:` blocks (cron writes the cache; simulate restores it), the
cache has a 24-hour freshness TTL inside `_cache_read`. The weekly
cron writes the cache Friday 22:00 UTC; a Sunday simulate's restore
gives 39-hour-old entries → fails the freshness check → live
`yf.Ticker(ticker).info` fetch for all 502 tickers.

Cost: 2-8 seconds per ticker cold (tenacity 3 attempts ×
`wait_exponential(min=1, max=10)`) = 17-67 minutes alone, enough
to fill the entire 45m budget by itself.

This is the FIFTH external-data loop in compute/main.py. The four
prior skip vars (FORM4_FETCH_SKIP + QR_SKIP_TIER2 +
QR_SKIP_FUNDAMENTALS + QR_SKIP_OSAP from PR #230 + this PR's Part 5)
all correctly skip their respective loops. But they don't touch
the cross_source path.

Fix: add QR_SKIP_CROSS_SOURCE=1 escape hatch at the TOP of
fetch_yfinance_market_cap:

(a) When set + stale-but-present cache entry exists → read the JSON
    directly (bypassing the 24h TTL in `_cache_read`)
(b) When set + cache is genuinely empty → return None (skip the
    cross-source validation entirely; semantically identical to a
    cold-cache fetch failure which the call site already handles
    per the existing graceful-degradation pattern)

Wired in `.github/workflows/pre-merge-prod-sim.yml` env block
alongside the four prior skip vars. CLAUDE.md §Gotchas rewritten:
"4-env-var combo" → "5-env-var combo" with full read-site mapping.
PHASE_STATUS_INFLIGHT.md entry expanded to record Part 6 alongside
Part 5.

Together the FIVE env vars now collectively cover ALL FIVE
independent external-data loops in `compute/main.py`:

  Loop                    | Skip var
  ------------------------|----------------------
  Form-4                  | FORM4_FETCH_SKIP
  Tier-2 (10-K + 8-K)     | QR_SKIP_TIER2
  Fundamentals × 2 (SEC)  | QR_SKIP_FUNDAMENTALS
  OSAP bulk download      | QR_SKIP_OSAP
  Cross-source yfinance   | QR_SKIP_CROSS_SOURCE  ← NEW

Cron path UNAFFECTED — weekly compute-rankings.yml doesn't set any
QR_SKIP_* var so full live fetches still run there once a week.

Tests: smoke-verified that QR_SKIP_CROSS_SOURCE check is at line 24
of fetch_yfinance_market_cap (well BEFORE `_cache_read` at line 48).
Cold-cache call returns None (no live fetch) when env var is set.
ruff clean. YAML parse-verified clean.

Live validation: re-pushing this PR triggers a fresh simulate
with all 5 skip vars active + 4 of 5 caches restored from cron.
Expected: simulate completes in under 25 minutes.

If simulate STILL cancels after this, escalate to incident-commander
+ consider pragmatic backstop (bump timeout to 90m OR disable
simulate path-filter for docs-only PRs).

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4

* ci(simulate) Part 7: pragmatic backstop — bump timeout-minutes 45 → 90

Part 6's live-fire on PR #241 STILL cancelled at 45m15s. The 5
skip env vars are all set + read-site verified by direct smoke
tests, but the cancellation recurs. Four iterations of "walk the
next loop" haven't surfaced a sixth obvious culprit; time for the
pragmatic backstop the ci-triage-engineer flagged as Option B
in session 6.

Bump `timeout-minutes: 45 → 90` in pre-merge-prod-sim.yml.

Rationale:
- Simulate is informational-only per workflow comment line 24
  ("This check is intentionally NOT a required status"); a wider
  budget doesn't block PR merge
- Cron uses timeout-minutes: 150 — simulate at 90 stays well below
- If 90m STILL cancels, we have HARD evidence that one of:
  (a) the cache restore is genuinely empty (cron skipped a week,
      or the new path additions invalidated the key), so all 5
      fall-through-to-live paths in the QR_SKIP_* helpers trigger
      live fetches anyway
  (b) there's a 6th external-data loop we haven't identified
  (c) the env vars aren't being propagated to the running process
- 50-70m successful completion surfaces the timing breakdown we
  need to root-cause definitively; cancellation at 45m hides it

This is NOT a permanent fix; it's diagnostic headroom + a backstop
while the structural issue is identified. The five skip env vars
remain in place (FORM4_FETCH_SKIP + QR_SKIP_TIER2 + QR_SKIP_FUNDAMENTALS
+ QR_SKIP_OSAP + QR_SKIP_CROSS_SOURCE) so the existing fast-path
still kicks in when caches are warm.

Inline workflow comment expanded to document the recurrence history
(PRs #230 / #238 / #241 all hit the 45m cap despite each round's
fix) + the rationale for the 90m backstop.

Follow-ups (separate PRs):
- ci-triage-engineer session-7 with hard ask: pull the actual job
  log via raw GitHub API (curl + GITHUB_TOKEN if available) and
  identify which step is at minute 44 when GitHub force-cancels
- If 90m runs succeed: post-merge audit of where the time actually
  went, name the 6th loop (if any), wire its skip var
- If 90m runs ALSO cancel: escalate to incident-commander —
  cancellation at 90m means something is genuinely broken in
  cache restore OR env propagation, not just "one more loop"

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4

---------

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 24, 2026
…bagent count 15 → 18 + INFLIGHT.md housekeeping (#243)

User called out a recurring error from session 7: I repeatedly stated
"cron Sun 22:00 UTC" in summaries despite the actual workflow
schedule being Mon-Fri only.

ROOT CAUSE: the handoff prompt at session start said
"Next prod cron: Sun 2026-05-24 22:00 UTC (cron-#4)" and I echoed
that without verifying against `.github/workflows/compute-rankings.yml`.
The YAML has read `"0 22 * * 1-5"` from initial commit (verified via
`git log --all -p`). Inline comment in the workflow even says
"Weekends skipped (no new trading data)".

Stale "Sunday/Sun 22:00 UTC" references across 4 files corrected:

- CLAUDE.md:31 §Stack — "cron Sun 22:00 UTC" → "cron Mon-Fri 22:00 UTC"
- docs/RESEARCH_FINDINGS.md:854 — "WEEKLY (GitHub Actions, Sunday
  22:00 UTC)" → "WEEKDAY (GitHub Actions, Mon-Fri 22:00 UTC; weekends
  skipped)"
- docs/ARCHITECTURE.md:7 mermaid diagram — "Sun 22:00 UTC" →
  "Mon-Fri 22:00 UTC" + edge "run weekly" → "run weekdays"
- docs/stock_ranking_knowledge.md:993 — "Weekly Sunday 22:00 UTC" →
  "Weekday Mon-Fri 22:00 UTC: Main compute cron (weekends skipped —
  no new trading data)"

Companion stale-info fix found during audit:

- AGENTS.md:1294 — "The 15 subagents under `.claude/agents/`" →
  "The 18 subagents under `.claude/agents/`". The roster expanded
  to 18 in PR #225 (ci-triage-engineer + vercel-preview-auditor +
  literature-searcher). AGENTS.md:91 already said 18 — line 1294
  was unsynced drift.

INFLIGHT.md housekeeping (per PR #237 convention — append-only with
periodic move from "In flight" → "Merged" sub-section):

- PR #241 (simulate Parts 5+6+7, `e9d7836`) → moved to Merged with
  consolidated 4-iteration summary preserving the QR_SKIP_OSAP +
  QR_SKIP_CROSS_SOURCE + timeout-45→90 history
- PR #242 (light-mode soften + Strong Buy nowrap + StockLogo
  square, `a30c017`) → moved to Merged, original entry text intact
- Duplicate "## Merged (awaiting housekeeping move to CLAUDE.md)"
  header at line 257 deleted (was a rebase-artifact duplicate from
  the PR #239 + PR #240 chronology resolution); file now has exactly
  ONE Merged section
- Old "PR (this PR) — Simulate Parts 5+6+7" body block excised
  (lines 201-300 of the post-rebase file) — replaced by this PR's
  clean Doc-staleness sweep entry in the In-flight section

Audit completeness:

- Schema version `0.10.2-phase4.5e` — verified current across
  CLAUDE.md, PHASE_STATUS.md, AGENTS.md (no stale references)
- Defense layer "32 boolean flags emitted" — verified current
- Subagent file count = 19 in `.claude/agents/` (18 agent files +
  1 README.md) — corresponds to 18 subagents, AGENTS.md:91 + :1294
  now both consistent

No compute / schema / scoring / valuation / Python / TypeScript code
change. Doc-only PR. PHASE_STATUS_INFLIGHT.md side-file satisfies
§Conventions lockstep.

https://claude.ai/code/session_01JwntEE4PNAXSMkZxRA9BB4

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 25, 2026
…e + a11y + stale copy

10 quick-win design polish items surfaced by the post-#242
frontend-design-reviewer open-scope audit. All single-line or
two-line fixes across 6 files. No schema / Python / scoring /
valuation / output JSON change.

A1  app/stock/[ticker]/page.tsx:190,239 — 2 h2 section headers
    ("Price (1y)" + "Raw fundamentals") gain dark:text-slate-400
    (other h2s on same page already had the pair; these two were
    overlooked)

A2  app/stock/[ticker]/page.tsx:76 — "← Back to ranking" link gains
    dark:text-slate-400 dark:hover:text-slate-100 (sibling not-found
    path already had the dark variants)

A3  FilterDrawer.tsx:238 — Valuation/MoS chips gain
    dark:bg-slate-900 dark:text-slate-400 dark:ring-slate-700
    dark:hover:bg-slate-800 on unselected state (other 3 filter
    groups already had it; this one was missed)

A4  FairPriceCard.tsx:97,106,116,124 — 4 KPI dd values
    (Median fair / Margin of safety / Max ex-outliers / Tangible BVPS)
    gain font-mono font-semibold leading-none. Was reading visibly
    weaker than the hero-metric pattern (ScoreBadge lg + MoSBadge +
    CurrentPriceLine all use font-mono font-semibold).

A5  RankingTable.tsx:243 — inactive sortable columns now render a
    subtle "↕" glyph in text-slate-300/dark:text-slate-700 instead
    of an empty string. Communicates sortability before interaction
    (Bloomberg/Google Finance standard pattern).

A6  RankingTable.tsx headerCell() — aria-sort={ascending | descending
    | none} attribute added to sortable <th> elements. Screen readers
    can now announce column sort direction (WAI-ARIA standard).

A7  FilterDrawer.tsx:146 — search input gains
    dark:focus:border-slate-500 dark:focus:ring-slate-500. Sibling
    input in RankingTable already had the dark focus variants;
    drawer input was inconsistent.

A8  FilterDrawer.tsx 4 chip surfaces — `transition` (transitions
    ALL CSS props) → `transition-colors` (scoped to bg/color/ring).
    Matches PriceTimePeriodSelector pattern; minor but correct.

A9  app/page.tsx:22 + app/stock/[ticker]/page.tsx:291 — stale
    "Phase 3b" / "Phase 3c" references stripped from user-facing
    footer copy. These were internal implementation notes that
    leaked into user-facing text; current schema is 0.10.2-phase4.5e.
    Explanatory content preserved; just the phase tags removed.

A10 PriceHistoryChart.tsx:368 — Recharts tooltip
    borderRadius: '0.375rem' (6px) → '0.25rem' (4px) matching the
    post-A3 4px card-radius normalization. SKILL.md Rule 0 carve-out
    covers the inline JS object pattern; the radius value itself
    can still align with the spec.

Out of scope (deferred from audit)
- B1: dead-sort-key SortKey cleanup (no UI impact today)
- B2/C1: Instrument Serif use-on-hero or remove-import decision
        (design taste call — needs user direction)
- B3/C2: MoS column on desktop ranking table
        (data-density tradeoff — needs user direction)
- C3: focus-visible ring system across all 23 interactive buttons
       (a11y improvement, wider scope)

Lockstep
- PHASE_STATUS_INFLIGHT.md side-file entry appended per PR #237
  convention (CLAUDE.md / AGENTS.md substance UNCHANGED — doc-only
  in-flight entry lives in the side file)
- No schema / Python / scoring / valuation / output JSON change
- className + attr + copy-text diff only; no test changes required
- tsc --noEmit errors in sandbox are env noise (no node_modules);
  CI runs with deps and will validate
dackclup pushed a commit that referenced this pull request May 25, 2026
…e + a11y + stale copy

10 quick-win design polish items surfaced by the post-#242
frontend-design-reviewer open-scope audit. All single-line or
two-line fixes across 6 files. No schema / Python / scoring /
valuation / output JSON change.

A1  app/stock/[ticker]/page.tsx:190,239 — 2 h2 section headers
    ("Price (1y)" + "Raw fundamentals") gain dark:text-slate-400
    (other h2s on same page already had the pair; these two were
    overlooked)

A2  app/stock/[ticker]/page.tsx:76 — "← Back to ranking" link gains
    dark:text-slate-400 dark:hover:text-slate-100 (sibling not-found
    path already had the dark variants)

A3  FilterDrawer.tsx:238 — Valuation/MoS chips gain
    dark:bg-slate-900 dark:text-slate-400 dark:ring-slate-700
    dark:hover:bg-slate-800 on unselected state (other 3 filter
    groups already had it; this one was missed)

A4  FairPriceCard.tsx:97,106,116,124 — 4 KPI dd values
    (Median fair / Margin of safety / Max ex-outliers / Tangible BVPS)
    gain font-mono font-semibold leading-none. Was reading visibly
    weaker than the hero-metric pattern (ScoreBadge lg + MoSBadge +
    CurrentPriceLine all use font-mono font-semibold).

A5  RankingTable.tsx:243 — inactive sortable columns now render a
    subtle "↕" glyph in text-slate-300/dark:text-slate-700 instead
    of an empty string. Communicates sortability before interaction
    (Bloomberg/Google Finance standard pattern).

A6  RankingTable.tsx headerCell() — aria-sort={ascending | descending
    | none} attribute added to sortable <th> elements. Screen readers
    can now announce column sort direction (WAI-ARIA standard).

A7  FilterDrawer.tsx:146 — search input gains
    dark:focus:border-slate-500 dark:focus:ring-slate-500. Sibling
    input in RankingTable already had the dark focus variants;
    drawer input was inconsistent.

A8  FilterDrawer.tsx 4 chip surfaces — `transition` (transitions
    ALL CSS props) → `transition-colors` (scoped to bg/color/ring).
    Matches PriceTimePeriodSelector pattern; minor but correct.

A9  app/page.tsx:22 + app/stock/[ticker]/page.tsx:291 — stale
    "Phase 3b" / "Phase 3c" references stripped from user-facing
    footer copy. These were internal implementation notes that
    leaked into user-facing text; current schema is 0.10.2-phase4.5e.
    Explanatory content preserved; just the phase tags removed.

A10 PriceHistoryChart.tsx:368 — Recharts tooltip
    borderRadius: '0.375rem' (6px) → '0.25rem' (4px) matching the
    post-A3 4px card-radius normalization. SKILL.md Rule 0 carve-out
    covers the inline JS object pattern; the radius value itself
    can still align with the spec.

Out of scope (deferred from audit)
- B1: dead-sort-key SortKey cleanup (no UI impact today)
- B2/C1: Instrument Serif use-on-hero or remove-import decision
        (design taste call — needs user direction)
- B3/C2: MoS column on desktop ranking table
        (data-density tradeoff — needs user direction)
- C3: focus-visible ring system across all 23 interactive buttons
       (a11y improvement, wider scope)

Lockstep
- PHASE_STATUS_INFLIGHT.md side-file entry appended per PR #237
  convention (CLAUDE.md / AGENTS.md substance UNCHANGED — doc-only
  in-flight entry lives in the side file)
- No schema / Python / scoring / valuation / output JSON change
- className + attr + copy-text diff only; no test changes required
- tsc --noEmit errors in sandbox are env noise (no node_modules);
  CI runs with deps and will validate
dackclup added a commit that referenced this pull request May 25, 2026
…e + a11y + stale copy (#244)

10 quick-win design polish items from the post-#242 design-reviewer
audit. Single-line and two-line fixes across 6 files.

A1  app/stock/[ticker]/page.tsx — 2 h2 section headers gain
    dark:text-slate-400
A2  app/stock/[ticker]/page.tsx — "← Back to ranking" link gains
    dark:text-slate-400 dark:hover:text-slate-100
A3  FilterDrawer.tsx — Valuation/MoS chips gain dark hover state
A4  FairPriceCard.tsx — 4 KPI dd values gain font-mono font-semibold
    leading-none to match hero-metric weight
A5  RankingTable.tsx — inactive sortable columns now show ↕ glyph
A6  RankingTable.tsx — aria-sort attribute on sortable th for
    screen readers
A7  FilterDrawer.tsx — search input gains dark focus ring variants
A8  FilterDrawer.tsx — `transition` → `transition-colors` on 4
    chip surfaces
A9  app/page.tsx + app/stock/[ticker]/page.tsx — strip stale
    "Phase 3b" / "Phase 3c" references from user-facing copy
A10 PriceHistoryChart.tsx — Recharts tooltip borderRadius 0.375rem
    → 0.25rem (post-A3 card-radius normalization)

No schema / Python / scoring / valuation / output JSON change.
PHASE_STATUS_INFLIGHT.md side-file entry per PR #237 convention.

🤖 https://claude.ai/code/session_01Dk82y7Eswz745NuBQH3CaG
dackclup added a commit that referenced this pull request May 25, 2026
… (Tier 1) (#250)

* feat(frontend): animation polish PR 1 — micro-interaction transitions (Tier 1)

10 className-level micro-interaction edits across 6 components. Pure
className diff, zero new keyframes, zero new deps, zero schema change.
LedgerCraft compliance: every transition ≤ 200ms; functional state-change
feedback only; ease-out standard easing; no bouncy / spring / overshoot.

- FilterDrawer.tsx:238 — fix typo transition-colors-colors → transition-colors
  (Valuation chip group had ZERO color transition — silent Tailwind drop)
- FilterDrawer.tsx:296 — transition-colors duration-150 on "View N stocks" CTA
- RankingTable.tsx:244 — text sort glyph (▲/▼/↕) → rotating SVG chevron
  with smooth 150ms rotate-0 ↔ rotate-180 on asc/desc toggle
- RankingTable.tsx:256 — transition-colors duration-150 on Filters button
- RankingTable.tsx:414 — transition-colors duration-100 on desktop <tr> hover
- RankingTable.tsx:451 — transition-colors duration-100 on mobile <li> card
- FairPriceCard.tsx:37 — transition-colors duration-100 on MethodRow <tr>
- RawMetricsTable.tsx:65 — transition-colors duration-100 on <tr>
- Sidebar.tsx:95 — replace transition-transform duration-200 with
  [transition:transform_200ms_ease-out,width_200ms_ease-out] so desktop
  collapse 240px → 64px animates smoothly instead of hard-jumping
- Sidebar.tsx:127 — explicit duration-200 on collapse chevron

PHASE_STATUS_INFLIGHT.md side-file pattern (PR #237) satisfies §Conventions
lockstep. CLAUDE.md / AGENTS.md substance UNCHANGED.

* fix(frontend): PriceTimePeriodSelector dark-mode contrast bump

User spot-check on Vercel preview (PR #250) reported the 7-button
period selector buttons (1D / 5D / 1M / 6M / YTD / 1Y / 5Y) as hard
to read in dark mode — "มองไม่ค่อยเห็น".

Pre-fix contrast pairs on bg-slate-900:
- Unselected enabled: text-slate-400 ~4.5:1 (just barely WCAG AA)
- Disabled (1D/5D):   text-slate-600 ~3:1 (sub-AA, near-invisible)

Post-fix:
- Unselected enabled → text-slate-300 (~7:1, AAA)
- Disabled → text-slate-500 (~5:1, AA — visibly muted but readable)
- Unselected + selected ring → ring-slate-600 (was 700) for better
  button outline visibility in the same band

Ride-along contrast polish on PR #250 (animation polish PR 1). Light
mode unchanged.

---------

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 27, 2026
#285)

Locks the mobile-operator release workflow that was discovered + battle-
tested during the v1.3.0 + v1.4.0 cut on 2026-05-27. The user operates
GitHub from a phone only (no desktop / no gh CLI / no terminal); the
sandbox itself can't push tag-refs (HTTP 403 from the git proxy). The
only path that actually works = pre-filled /releases/new URL the user
taps once.

Files updated in lockstep (4 docs):

- .claude/skills/release-tag/SKILL.md
  * New top-of-file "OPERATOR CONSTRAINT — mobile-only (locked
    2026-05-27)" section
  * Step 5+6 rewritten into "Mobile-operator release workflow" —
    URL pattern, query-parameter table, 8 KB URL-size budget, short-
    body template (links to full release-notes file already on main),
    Python generator helper, what-the-user-does click-flow, multi-
    release ladder ordering (newest FIRST + retroactive LAST to avoid
    auto-flag-latest footgun), post-publish verify via
    get_latest_release + edit-URL fallback
  * Old shell pattern preserved in §"Reference: shell pattern (NOT
    for this user)" for posterity
  * "Most recent tag" pointer bumped v1.2.0-phase4.5 → v1.4.0-phase4.6

- .claude/agents/release-captain.md
  * Step 5 rewritten to emit pre-filled URL via Python generator
    instead of shell commands
  * "What you do NOT do" gains 3 new bullets: no shell tag commands,
    no MCP-create-release attempt, no multi-release publish without
    Latest-flag verify

- CLAUDE.md §Gotchas
  * New "Release tags are mobile-only (locked 2026-05-27)" entry
    above existing Parallel-PR collision pattern
  * Captures constraint + URL pattern + ladder ordering + cross-refs
    to SKILL.md + agent file

- AGENTS.md §"What you must NOT do"
  * Mirror bullet added so cross-tool agents (Copilot / Cursor /
    Devin) see the same constraint

Lessons codified from the 2026-05-27 session:

1. Sandbox git push origin <tag> → HTTP 403 (proxy blocks tag-refs
   but allows branch pushes)
2. gh CLI not available in the remote execution environment
3. GitHub MCP server does NOT expose create_release as of 2026-05-27
4. Full release-notes body (12 KB raw, ~18 KB URL-encoded) exceeds
   GitHub's 8 KB URL limit → short-body pattern needed
5. Publishing v1.3.0 retroactive AFTER v1.4.0 with default "Set as
   latest" checked → v1.3.0 became Latest (wrong!) → required edit-
   URL re-promotion
6. Mobile UI's "Choose target" dropdown only shows recent branch
   commits — older SHA (5db3b97) doesn't appear → MUST pass
   target=<40-char-SHA> in query string, not via mobile UI selection

Scope: doc-only. No compute / schema / scoring / valuation / frontend
/ Python / TS code change. No test surface (skill / agent doc updates
aren't covered by pytest). ruff / schema_check / pytest trivially pass.

PHASE_STATUS_INFLIGHT.md side-file satisfies §Conventions "ship with
every PR" lockstep per PR #237 convention.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 28, 2026
Bundled cleanup PR addressing post-cron-#69 audit findings from
`defense-layer-auditor` Section A-J + `stock-detail-auditor`
deterministic prefilter + LLM verdict pass. 1 stale file removal +
3 doc drifts.

Changes:
- frontend/public/data/stocks/BK.json (DELETED) — Bank of NY Mellon
  renamed to BNY on 2026-05-26 (commit b7514cf); old BK.json claimed
  `rank: 230` which now belongs to GEHC; users navigating `/stock/BK`
  would see stale data conflicting with another stock's position.
  Section H universe-consistency FAIL clears after this PR merges.
- CLAUDE.md §Stack — `TypeScript 5.4 · Recharts 2.12` → `TypeScript 5.9
  · Recharts 2.15` to match actual `frontend/package.json` pins.
  Closes security-reviewer 2026-05-28 W2.
- CLAUDE.md §Gotchas — new bullet documenting `GITHUB_RUN_ID` +
  `GITHUB_SHA` env-vars (auto-provided by GitHub Actions, read at
  `compute/main.py:1965-1966`, surface into `Metadata.compute_run_id`
  + `Metadata.git_commit`). Closes security-reviewer W1.
- .claude/agents/dependency-auditor.md Frozen-by-design pins —
  `edgartools 2.30` → `edgartools 5.31 (5.x band, <6 upper bound)`.
  Closes dependency-auditor 2026-05-28 informational finding.

Findings deferred to scoped follow-up issues (not in this PR):
- Issue #288 — GOOG/GOOGL PR #269 XBRL fix never fires; needs
  edgar-debugger live probe of Alphabet 10-K dimension axis structure
- Issue #289 — NVR DQIC ceiling false positive; needs
  methodology-scientist Mode B verdict on ceiling fix path

Doc-only + 1 file removal. No compute / schema / scoring / valuation /
frontend code change. AGENTS.md substance untouched per the existing
delegation pattern (CLAUDE.md is the SoT). PHASE_STATUS_INFLIGHT.md
side-file satisfies §Conventions lockstep per PR #237 convention.

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 28, 2026
… open-issues list (#291)

Closes deferred substance items from PR #286 + PR #290 that opted to keep
scope tight under the `pointer-to-CLAUDE.md` delegation pattern.

Changes (1 file, AGENTS.md):

- Line 377 — Production-verified run pointer bumped from cron #51
  (`b1588b2a`, 2026-05-14) → cron #69 (`233117ac`, 13m 16s warm-cache,
  2026-05-28). Cross-tool agents now have the current known-good
  baseline; verified by defense-layer-auditor + stock-detail-auditor
  on schema `0.10.7-phase4.6`.

- Lines 380-382 — "Open Phase 4+ issues" list refreshed from 4 → 11
  entries. Removed stale qualifier on #67 (PR #204 already landed
  data-collection; #67 is Phase 4 follow-up not Phase 5+). Added 7
  missing issues: #115 (JKP license) · #130 (Q3 cohort) · #137
  (9arm-skills license deadline 2026-06-17) · #150 (Phase 2-3 epic) ·
  #287 (FORM4 revert + durable timeout) · #288 (GOOG/GOOGL XBRL) ·
  #289 (NVR DQIC false positive). Added zero-exploitability context
  to #41.

Doc-only PR. No compute / schema / scoring / valuation / frontend /
Python / TS code change. CLAUDE.md substance untouched this PR
(PR #286 + PR #290 already bumped it); this PR closes the AGENTS.md
side of the lockstep. PHASE_STATUS_INFLIGHT.md side-file satisfies
§Conventions lockstep per PR #237 convention.

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 28, 2026
…main.md

End-of-day Track-A3 follow-up to the post-session-housekeeping commit
(0949a3c). Adds a single-file CONTEXT.md entry point at the repo
root so external tools / fresh agents / vendored skills that expect
the upstream mattpocock single-CONTEXT.md convention have one bridge
file to read first. Reconciles docs/agents/domain.md which previously
declared "QuantRank has NO CONTEXT.md" (now stale).

Changes (3 files, doc-only — additive):

- CONTEXT.md (NEW, ~245 lines) — bridge / live-snapshot pointer.
  11 sections: Design note (explicit "not source of truth" framing)
  · What is QuantRank · Live snapshot (schema 0.10.8-phase4.6 ·
  v1.4.0-phase4.6 tag · 33 declared flags · USE_SECTOR_COE=True
  post-#294 · cron #69 green) · Multi-file mapping (12-row file→topic
  + 6-row topic→file tables mirroring docs/agents/domain.md) · 8 Key
  invariants (Rule 16 / schema triple / Rule 18 / lockstep / rebase
  / mobile-only / formula sacred / orchestrator role) · Stack ·
  Layout · Quick-start commands · 9 Standing constraints · 7
  Vocabulary discipline terms · Roadmap pointer (Stage 0 → Stage 6
  / v2.0) · Companion files index.

- docs/agents/domain.md §"QuantRank has NO CONTEXT.md" — section
  header + opening paragraph rewritten to "CONTEXT.md is a pointer,
  not the source of truth" with the new framing (the four-file
  analog remains canonical; CONTEXT.md is a bridge). Topic-driven
  lookup table + "update CONTEXT.md inline when a term resolves" +
  "Use the project vocabulary" sections unchanged — their semantics
  still apply (updates land in the appropriate deep file, not in
  CONTEXT.md which is pointer-only).

- PHASE_STATUS_INFLIGHT.md — appended in-flight entry per §Conventions
  lockstep (PR #237 convention).

Design discipline:

- CONTEXT.md MUST NOT duplicate content from the four canonical files
  (CLAUDE.md + SKILL.md + WORKFLOW.md + docs/METHODOLOGY.md) — when
  content drifts, the four files win and CONTEXT.md updates to point
  at the new location.
- "Live snapshot" block IS expected to drift; the next end-of-session
  housekeeping commit refreshes it the same way it refreshes
  CLAUDE.md §Phase status.

Doc-only PR — ruff / schema_check N/A; no compute / schema / scoring
/ valuation / frontend / Python / TS change. CLAUDE.md substance
untouched this PR (CONTEXT.md is a NEW top-level file that pointers
TO CLAUDE.md, not a substance change WITHIN it).
dackclup added a commit that referenced this pull request May 28, 2026
…main.md (#296)

End-of-day Track-A3 follow-up to the post-session-housekeeping commit
(0949a3c). Adds a single-file CONTEXT.md entry point at the repo
root so external tools / fresh agents / vendored skills that expect
the upstream mattpocock single-CONTEXT.md convention have one bridge
file to read first. Reconciles docs/agents/domain.md which previously
declared "QuantRank has NO CONTEXT.md" (now stale).

Changes (3 files, doc-only — additive):

- CONTEXT.md (NEW, ~245 lines) — bridge / live-snapshot pointer.
  11 sections: Design note (explicit "not source of truth" framing)
  · What is QuantRank · Live snapshot (schema 0.10.8-phase4.6 ·
  v1.4.0-phase4.6 tag · 33 declared flags · USE_SECTOR_COE=True
  post-#294 · cron #69 green) · Multi-file mapping (12-row file→topic
  + 6-row topic→file tables mirroring docs/agents/domain.md) · 8 Key
  invariants (Rule 16 / schema triple / Rule 18 / lockstep / rebase
  / mobile-only / formula sacred / orchestrator role) · Stack ·
  Layout · Quick-start commands · 9 Standing constraints · 7
  Vocabulary discipline terms · Roadmap pointer (Stage 0 → Stage 6
  / v2.0) · Companion files index.

- docs/agents/domain.md §"QuantRank has NO CONTEXT.md" — section
  header + opening paragraph rewritten to "CONTEXT.md is a pointer,
  not the source of truth" with the new framing (the four-file
  analog remains canonical; CONTEXT.md is a bridge). Topic-driven
  lookup table + "update CONTEXT.md inline when a term resolves" +
  "Use the project vocabulary" sections unchanged — their semantics
  still apply (updates land in the appropriate deep file, not in
  CONTEXT.md which is pointer-only).

- PHASE_STATUS_INFLIGHT.md — appended in-flight entry per §Conventions
  lockstep (PR #237 convention).

Design discipline:

- CONTEXT.md MUST NOT duplicate content from the four canonical files
  (CLAUDE.md + SKILL.md + WORKFLOW.md + docs/METHODOLOGY.md) — when
  content drifts, the four files win and CONTEXT.md updates to point
  at the new location.
- "Live snapshot" block IS expected to drift; the next end-of-session
  housekeeping commit refreshes it the same way it refreshes
  CLAUDE.md §Phase status.

Doc-only PR — ruff / schema_check N/A; no compute / schema / scoring
/ valuation / frontend / Python / TS change. CLAUDE.md substance
untouched this PR (CONTEXT.md is a NEW top-level file that pointers
TO CLAUDE.md, not a substance change WITHIN it).

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 28, 2026
Three-part durable fix for the 2026-05-25 weekly cron cancellation at
the 150m timeout ceiling (incident-commander session 8 verdict — PR
budget). Does NOT revert FORM4_FETCH_SKIP=1 — that is PR B, gated on
>= 1 cron completing under the new 195m ceiling with all 4 wall-clock
fields populated.

Part 1 — `compute-rankings.yml` `timeout-minutes: 150 -> 195` plus
inline 5-loop cold-cache budget docstring (prices 5m + fundamentals
25m + history 15m + form-4 10m + tier-2 35m + osap 5m + scoring 3m =
~98m warm-realistic + 20% SEC-throttle headroom = ~118m, +commit step
+ runner spin-up = 195m).

Part 2 — Cache-restore canary step inserted between `Restore compute
caches` and `Run weekly compute`. Bash one-liner (`du -sm` + GNU
`find -printf '%T@'` + `bc`) emits size+age for 10 cache directories
to the workflow log in ~15-30s. Surfaces cache eviction before any
SEC fetch begins instead of after 150-195m of polling. Standard tools
on ubuntu-latest; fail-open on missing utilities.

Part 3 — 4 new `Metadata.*_wall_clock_seconds` fields
(`tier2_wall_clock_seconds`, `form4_wall_clock_seconds`,
`osap_wall_clock_seconds`, `cross_source_wall_clock_seconds`). Schema
PATCH bump `0.10.8-phase4.6 -> 0.10.9-phase4.6` (additive Metadata
only). `compute/main.py` wraps each of the 4 loops with `time.monotonic()`
start/end markers and 4 distinct defensive patterns:
- Tier-2: outer try/except, `None` on interpreter-level failure
- Form-4: start INSIDE the `else:` branch so FORM4_FETCH_SKIP=1 leaves
  `form4_wall_clock_seconds = None` (skip semantic)
- OSAP: start before try, end at try success, `None` in except. Note:
  QR_SKIP_OSAP only bypasses the freshness gate so wall-clock still
  populates with a small float (~0.5-2s) on cache-hit fast return —
  documented in CLAUDE.md §Gotchas + schema docstring.
- Step 8 cross_source umbrella: always populated; measures the ENTIRE
  Step 8 per-ticker loop (fair-price + manipulation + StockDetail
  write), not just cross_source sub-calls — documented limitation.

Schema triple lockstep: `compute/output/schemas.py` (Pydantic) +
`frontend/lib/types.ts` (TS mirror) + `frontend/lib/schema-snapshot.json`
(regenerated via `python -m compute.output.schema_check --update-snapshot`).
All 4 fields shaped identically across the triple as
`float | None = None` / `number | null` optional.

Test surface: tests/test_config.py schema pin bumped + docstring
rewrite; new tests/test_output/test_wall_clock_schema.py with 2 active
GREEN tests (schema round-trip + None defaults) + 3 skipped stubs with
TODO docstrings for orchestrator-level behavior (harness gap honestly
acknowledged — the `run_weekly_compute()` orchestrator harness build-
out is orthogonal scope; PR B's cron data validates behavior
empirically).

Docs lockstep: CLAUDE.md §Phase status schema pointer bumped +
§Gotchas new bullet explaining wall-clock-vs-per-ticker-p95 semantic +
per-loop None semantics. AGENTS.md Issue #287 status updated to "PR A
in flight this PR". PHASE_STATUS_INFLIGHT.md full in-flight entry
appended per PR #237 side-file convention.

Verification ladder:
- ruff check .                              PASS
- python -m compute.output.schema_check     PASS (in sync)
- pytest tests/test_config.py               11/11 PASS
- pytest tests/ (full, sub-agent reported)  1365 -> 1367 PASS (+2)

Pre-push 3-reviewer gate:
- schema-sentinel (sonnet)                  PASS (Metadata 47 -> 51 fields)
- security-reviewer (sonnet)                SAFE-TO-PUSH (0 CRIT/FAIL/WARN)
- quantrank-reviewer (opus)                 PASS post-fix (2 FAIL + 2 WARN
                                            resolved: ruff I001 auto-fixed,
                                            QR_SKIP_OSAP docstring corrected,
                                            CLAUDE.md gotcha added, stale
                                            line-comment ride-along fix)

PR B = single-line revert of FORM4_FETCH_SKIP=1 from
compute-rankings.yml env block. Gates: >= 1 cron green at < 195m with
`form4_wall_clock_seconds` populated. Unblocks Phase 4.5e PR 5
(cluster weight promotion 5.0 -> 7.0) gate-data accumulation for
Q3 2026-08-19 quarterly cohort audit.

Closes part 1-3 of Issue #287.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa
dackclup added a commit that referenced this pull request May 28, 2026
…297)

Three-part durable fix for the 2026-05-25 weekly cron cancellation at
the 150m timeout ceiling (incident-commander session 8 verdict — PR
budget). Does NOT revert FORM4_FETCH_SKIP=1 — that is PR B, gated on
>= 1 cron completing under the new 195m ceiling with all 4 wall-clock
fields populated.

Part 1 — `compute-rankings.yml` `timeout-minutes: 150 -> 195` plus
inline 5-loop cold-cache budget docstring (prices 5m + fundamentals
25m + history 15m + form-4 10m + tier-2 35m + osap 5m + scoring 3m =
~98m warm-realistic + 20% SEC-throttle headroom = ~118m, +commit step
+ runner spin-up = 195m).

Part 2 — Cache-restore canary step inserted between `Restore compute
caches` and `Run weekly compute`. Bash one-liner (`du -sm` + GNU
`find -printf '%T@'` + `bc`) emits size+age for 10 cache directories
to the workflow log in ~15-30s. Surfaces cache eviction before any
SEC fetch begins instead of after 150-195m of polling. Standard tools
on ubuntu-latest; fail-open on missing utilities.

Part 3 — 4 new `Metadata.*_wall_clock_seconds` fields
(`tier2_wall_clock_seconds`, `form4_wall_clock_seconds`,
`osap_wall_clock_seconds`, `cross_source_wall_clock_seconds`). Schema
PATCH bump `0.10.8-phase4.6 -> 0.10.9-phase4.6` (additive Metadata
only). `compute/main.py` wraps each of the 4 loops with `time.monotonic()`
start/end markers and 4 distinct defensive patterns:
- Tier-2: outer try/except, `None` on interpreter-level failure
- Form-4: start INSIDE the `else:` branch so FORM4_FETCH_SKIP=1 leaves
  `form4_wall_clock_seconds = None` (skip semantic)
- OSAP: start before try, end at try success, `None` in except. Note:
  QR_SKIP_OSAP only bypasses the freshness gate so wall-clock still
  populates with a small float (~0.5-2s) on cache-hit fast return —
  documented in CLAUDE.md §Gotchas + schema docstring.
- Step 8 cross_source umbrella: always populated; measures the ENTIRE
  Step 8 per-ticker loop (fair-price + manipulation + StockDetail
  write), not just cross_source sub-calls — documented limitation.

Schema triple lockstep: `compute/output/schemas.py` (Pydantic) +
`frontend/lib/types.ts` (TS mirror) + `frontend/lib/schema-snapshot.json`
(regenerated via `python -m compute.output.schema_check --update-snapshot`).
All 4 fields shaped identically across the triple as
`float | None = None` / `number | null` optional.

Test surface: tests/test_config.py schema pin bumped + docstring
rewrite; new tests/test_output/test_wall_clock_schema.py with 2 active
GREEN tests (schema round-trip + None defaults) + 3 skipped stubs with
TODO docstrings for orchestrator-level behavior (harness gap honestly
acknowledged — the `run_weekly_compute()` orchestrator harness build-
out is orthogonal scope; PR B's cron data validates behavior
empirically).

Docs lockstep: CLAUDE.md §Phase status schema pointer bumped +
§Gotchas new bullet explaining wall-clock-vs-per-ticker-p95 semantic +
per-loop None semantics. AGENTS.md Issue #287 status updated to "PR A
in flight this PR". PHASE_STATUS_INFLIGHT.md full in-flight entry
appended per PR #237 side-file convention.

Verification ladder:
- ruff check .                              PASS
- python -m compute.output.schema_check     PASS (in sync)
- pytest tests/test_config.py               11/11 PASS
- pytest tests/ (full, sub-agent reported)  1365 -> 1367 PASS (+2)

Pre-push 3-reviewer gate:
- schema-sentinel (sonnet)                  PASS (Metadata 47 -> 51 fields)
- security-reviewer (sonnet)                SAFE-TO-PUSH (0 CRIT/FAIL/WARN)
- quantrank-reviewer (opus)                 PASS post-fix (2 FAIL + 2 WARN
                                            resolved: ruff I001 auto-fixed,
                                            QR_SKIP_OSAP docstring corrected,
                                            CLAUDE.md gotcha added, stale
                                            line-comment ride-along fix)

PR B = single-line revert of FORM4_FETCH_SKIP=1 from
compute-rankings.yml env block. Gates: >= 1 cron green at < 195m with
`form4_wall_clock_seconds` populated. Unblocks Phase 4.5e PR 5
(cluster weight promotion 5.0 -> 7.0) gate-data accumulation for
Q3 2026-08-19 quarterly cohort audit.

Closes part 1-3 of Issue #287.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 28, 2026
Closes the silent-failure gap surfaced by Issue #287 PR A's Rule 18
instrumentation on cron Run #71 (368dccd, 2026-05-28 08:44 UTC). The
PR #292 GOOG/GOOGL per-class XBRL share-override fix did not fire in
production despite the code being correct on the runner.

Root cause (edgar-debugger 2026-05-28 verdict):

  - PR #292 (e9aaab3, 04:22 UTC) landed the per-class XBRL override
    at compute/ingest/fundamentals.py:1043-1067 (Branch 3 of
    _build_snapshot).
  - Branch 3 only executes on live EDGAR fetch — fetch_fundamentals
    short-circuits at _is_fresh() (line 1292-1294) when cached parquet
    age by latest_filed_date < FUNDAMENTALS_REFETCH_DAYS = 45.
  - Earlier same-day cron 0ad1d57 (03:22 UTC, pre-PR-#292) wrote a
    stale aggregate parquet (GOOG shares_outstanding = 12.116B).
  - Cron Run #71 restored that parquet from the GitHub Actions cache;
    _is_fresh() returned True on latest_filed_date=2026-04-30 (28d
    < 45d), and Branch 3 never ran.
  - metadata.multi_class_per_class_attempt_count = 0 (PR #292 Rule 18
    disambiguator working as designed — the smoking gun).
  - fundamentals_latency_p50_seconds = 0.0 (warm-cache replay confirmed
    universe-wide).

Fix scope (6 files, YAML + paired test bump):

  - .github/workflows/compute-rankings.yml — 3 instances cache-v4- →
    cache-v5- (key + 2 restore-keys) + comment block expanded to cite
    Issue #288 follow-up + PR #292 + PR #269 + introduce a 2-trigger
    bump taxonomy (schema change OR value-correctness fix in live-
    fetch-only path).
  - .github/workflows/pre-merge-prod-sim.yml — mirror 3-string flip
    per the file's own "bump together if either changes" comment.
    Without this the simulate workflow would lose all 11 warm caches
    on every PR.
  - tests/test_workflow_cache_coverage.py — paired-test bump per the
    PR 4c.1 v3→v4 precedent. Function renamed
    test_workflow_cache_key_is_v4 → _v5; docstring rewritten to cite
    Issue #288 + PR #292 + the 3-trigger bump taxonomy.
  - CLAUDE.md §Phase status — drain stale "in flight" wording for
    PR #297 (now merged) + empirical-validation note for cron Run #71
    + "in flight this PR" entry for the cache-v5 bump.
  - AGENTS.md open-issues list — update #287 (PR A merged via #297),
    #288 (fix in flight this PR), #289 (closed by PR #293).
  - PHASE_STATUS_INFLIGHT.md — full in-flight entry appended per
    PR #237 side-file convention.

Why Option A (cache-key bump) over alternatives (per edgar-debugger):

  - Option B (targeted per-ticker invalidation): introduces cache-layer-
    knows-multi-class semantics + chicken-and-egg "detect stale
    aggregate from cached parquet" condition.
  - Option C (refactor override out of fetch path): cache hit triggers
    live SEC call (violates cache semantics) + FundamentalsSnapshot is
    frozen.
  - Option A: matches PR 4c.1 v3→v4 precedent exactly + zero compute/
    change + guaranteed correctness on next cron.

One-time cost: ~25-50 min cold cron on the immediately-following
weekly run (full S&P 500 universe live re-fetch). Subsequent crons
return to warm-cache ~5-10 min budget. No timeout-minutes impact —
PR #297 just bumped to 195m which absorbs cold-cache reality.

Verification on next cron Run #72:

  - metadata.multi_class_per_class_attempt_count = 2 (was 0)
  - metadata.multi_class_per_class_override_count = 2
  - stocks/GOOG.json shares_outstanding ≈ 5.429B (Class C, was 12.116B)
  - stocks/GOOGL.json shares_outstanding ≈ 5.822B (Class A, was 12.116B)
  - stocks/GOOG.json market_cap ≈ $2.09T (was $4.66T)
  - stocks/GOOGL.json market_cap ≈ $2.59T (was $4.71T)
  - metadata.fundamentals_latency_p50_seconds > 0.0 (live fetch active)

Adjacent findings deferred (NOT in this PR):

  - FOX / FOXA / NWS / NWSA: same multi_class_aggregate_shares_suspected
    annotate firing but they are on MULTI_CLASS_SHARE_ALLOWLIST
    (UNDERCOUNT path, PR #257). Decision on whether to add to overcount
    allowlist deferred to Q3 2026-08-19 quarterly cohort audit per
    methodology-scientist precedent (needs live XBRL probe).
  - OSAP wall-clock 347.1s on Run #71: cold OSAP download (cache > 31d
    mtime or evicted). Single observation; not a regression. Watch on
    next 2-3 crons.

Hard constraints honored:

  - No compute / scoring / schema / valuation / Rule 16 / Top-5
    invariant touched
  - No new defense flag · No new dep · No new env-var
  - YAML + paired-test diff (per quantrank-reviewer feedback on
    PR-title framing — original "YAML-only" was misleading)
  - Schema version UNCHANGED at 0.10.9-phase4.6 (no Pydantic / TS /
    snapshot change)

Pre-push 3-reviewer gate:

  - phase-coordinator Mode B (sonnet): LOCKSTEP-SATISFIED — both
    CLAUDE.md + AGENTS.md substance touched, INFLIGHT entry well-
    shaped, branch in-sync with origin/main (no rebase needed)
  - quantrank-reviewer (opus): FIX-AND-RE-REVIEW → 2 FAIL + 4 WARN.
    Both FAILs fixed in this commit (tests/test_workflow_cache_coverage.py
    test pin + pre-merge-prod-sim.yml cache-v4 stragglers). WARN 1
    (Issue #288 lifecycle) addressed via Reopens/Closes directives
    below. WARN 2 (AGENTS.md cron #69 cross-ref), WARN 3 (comment
    density) deferred — minor.

Reopens #288
Closes #288

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 28, 2026
…rkers + bump pointers (#299)

Closes today's 10-PR cycle (#286 / #290 / #291 / #292 / #293 / #294 /
#295 / #296 / #297 / #298). Mirror of PR #286 (post-v1.4.0 cycle
drain) for the post-cron-#71 cycle.

Three stale `(in flight, 2026-05-28)` markers in
PHASE_STATUS_INFLIGHT.md drained to `(merged 2026-05-28, <SHA>)`:

  - PR #295 (`2d2ec83e`) — Post-session housekeeping drain 6 INFLIGHT
    + bump pointers
  - PR #297 (`ecb60e64`) — Issue #287 PR A: durable timeout + cache
    canary + per-loop wall-clock Metadata (schema 0.10.8 → 0.10.9-phase4.6)
  - PR #298 (`030675e9`) — Issue #288 follow-up: cache-key bump v4 → v5

Bodies preserved (historical record).

CLAUDE.md §Phase status — drained the "(In flight this PR — cache-v5)"
qualifier (PR #298 merged) + added post-PR-#298 confirmation note +
cron Run #71 production-verified pointer.

AGENTS.md open-issues list — #288 status flipped "(fix in flight this
PR)" → "(closed by PR #298 cache-v5 bump)" + clarified the silent-
failure root-cause + Run #72 verification gate.

Why this PR exists: without end-of-day drain, session N+1 reading
CLAUDE.md / PHASE_STATUS_INFLIGHT.md would see 3 PRs still marked
"in flight" despite them merging hours earlier — the same friction
pattern PR #286 closed for the post-v1.4.0 cycle. Three same-day
drains in one PR keeps the side-file disciplined.

Scope (3 files, doc-only):

  - PHASE_STATUS_INFLIGHT.md — 3 header substitutions + this PR's
    own in-flight entry appended per PR #237 side-file convention
  - CLAUDE.md §Phase status pointer refresh
  - AGENTS.md open-issues list #288 status update

Hard constraints honored:

  - No code / scoring / schema / valuation / Rule 16 / Top-5
    invariant touched
  - No new defense flag · No new dep · No new env-var
  - Doc-only diff (Markdown only)
  - Schema version UNCHANGED at 0.10.9-phase4.6 (no Pydantic / TS /
    snapshot change)

PHASE_STATUS_INFLIGHT.md side-file satisfies §Conventions "ship with
every PR" lockstep per PR #237 convention. Same drain template as
PR #286 (post-v1.4.0 cycle).

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 28, 2026
…ULD-FIX cross-doc drifts (#301)

Comprehensive .md housekeeping closing today's 11-PR session day.
Output of docs-reviewer (sonnet) full Tier 1 + Tier 2 audit on main
(post-PR-#299) — verdict NEEDS-CROSS-REF-FIX with 14 prioritized
findings; this PR applies all 8 MUST-FIX + 6 SHOULD-FIX. 3 NICE-TO-FIX
deferred to follow-up.

Scope (7 files, doc-only):

  - SKILL.md schema-version history table (line 240) — prepend 2 new
    rows: 0.10.9-phase4.6 (PR #297, 4 *_wall_clock_seconds fields +
    195m timeout + cache canary, empirically validated cron Run #71)
    + 0.10.10-phase4.6 (PR #300 in flight — Issue #67 follow-up per-
    sector delta). Closes the canonical-history gap where PR #297 +
    PR #300 were absent.

  - PHASE_STATUS.md §Current state — schema row 0.10.8 → 0.10.9 +
    PR #300 in-flight note; Post-tag production patches row extended
    with PR #295/#296/#297/#298/#299 SHAs + one-liners; Production
    run pointer 0ad1d57 cron #69368dccd cron Run #71 with the
    PR #297 wall-clock empirical numbers + Issue #288 cache-replay
    smoking gun (multi_class_per_class_attempt_count=0 +
    fundamentals_latency_p50_seconds=0.0). Recently merged block
    extended 6 → 11 PRs. Issue closure status updated. Next
    deliverables refreshed: Issue #67 flip removed (PR #294 already
    executed); item 2 now = Issue #287 PR B FORM4 revert; PR #300
    per-sector delta added as item 3. Open issues list refreshed
    (#288 + #289 marked closed; #287 PR A vs PR B split).

  - CLAUDE.md §Phase status Recently merged block — extended 6 → 11
    PRs with full SHA + one-liner per PR. New "In flight" sub-section
    added for PR #300.

  - AGENTS.md §Phase + version state — Production-verified run
    cron #69 (233117a, 13m 16s) → cron Run #71 (368dccd, 14m 32s,
    2026-05-28 08:44 UTC, schema 0.10.7 → 0.10.9-phase4.6); 4 new
    wall-clock field values cited; Issue #288 cache-replay smoking
    gun captured; closed-issue note for #288 + #289 + #287 PR A.

  - CONTEXT.md §Live snapshot — schema 0.10.8 → 0.10.9 + PR #300
    in-flight note; new "Post-tag patches" row listing PRs #292-#299
    + PR #300 in flight; cron status cron #69 2026-05-27 → Run #71
    2026-05-28; Sector-CoE row updated with empirical 132 → 109;
    §Roadmap Stage 0 description refreshed.

  - WORKFLOW.md §Agentic 6-Phase Cadence session-start protocol —
    inline schema 0.10.7-phase4.6 replaced with current 0.10.9-phase4.6
    + pointer guidance to PHASE_STATUS.md §Current state as the
    canonical bump-per-schema-PR target. Closes the recurring inline-
    schema drift pattern.

  - PHASE_STATUS_INFLIGHT.md — this PR's in-flight entry appended per
    PR #237 side-file convention.

docs-reviewer lockstep cross-check after this PR:

  - SCHEMA_VERSION: ALIGNED across all 6 canonical docs at
    0.10.9-phase4.6 with PR #300 in-flight note where applicable
  - Defense layer 33 declared: ALIGNED (was already)
  - USE_SECTOR_COE = True post-PR #294: ALIGNED (was stale in
    AGENTS.md issue #67 framing + PHASE_STATUS.md Next deliverables;
    both fixed)
  - Subagent count 18: ALIGNED (was already)
  - Skill count 45: ALIGNED (was already)
  - Latest cron Run #71 368dccd: ALIGNED (was stale in AGENTS.md +
    PHASE_STATUS.md + CONTEXT.md; all fixed)
  - Issue #288 + #289 closure status: ALIGNED (was stale as open in
    AGENTS.md + PHASE_STATUS.md; both fixed)

3 NICE-TO-FIX deferred:

  - README.md Honest Limitations does not reference Phase 4.6 honest
    re-validation harness (PR #283). Coverage gap, not break.
  - WORKFLOW.md Phase 4.5 row cites v1.2.0; technically closed at
    v1.3.0-phase4.5e. Historical-context only.
  - METHODOLOGY.md USE_SECTOR_COE framing needs verification before
    edit.

Hard constraints honored:

  - No code / scoring / schema / valuation / Rule 16 / Top-5
    invariant touched
  - No new defense flag · No new dep · No new env-var
  - Markdown-only diff (no JSON / YAML / Python / TS change)
  - Schema version UNCHANGED on main at 0.10.9-phase4.6 (PR #300 will
    bump 0.10.10 on its merge)
  - AGENTS.md substance lockstep with CLAUDE.md per the established
    delegation pattern

Verification:

  - ruff check .                          PASS (no Python touched)
  - python -m compute.output.schema_check PASS (no schema touched)
  - pytest tests/ -m "not network"        N/A (no test surface)
  - Cross-reference grep — all 7 anchor strings consistent across
    all 6 docs after fix

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 28, 2026
… instrumentation

Methodology-scientist Mode B Q2 follow-up deferred from PR #294 (sector-
CoE flip, 2026-05-28 05:39 UTC). Adds
`Metadata.value_trap_risk_delta_by_sector: dict[str, int] | None` so
Q3 2026-08-19 quarterly cohort audit has visible per-sector shape
evidence — not just the aggregate `value_trap_risk_count_*_sector_coe`
scalars that landed in PR #204.

Schema PATCH bump 0.10.9 → 0.10.10-phase4.6 (additive Metadata-only).

Methodology context (Damodaran 2019 Ch. 8.4 §"Industry Beta"):

  After `USE_SECTOR_COE = True` per-sector Ke replaces the flat 10%
  baseline at SECTOR_COST_OF_EQUITY (11 GICS sectors, Ke 6%-12%).
  Directional predictions:

  - Lower-Ke sectors (Utilities ~6-7% / Real Estate ~7-8% / Consumer
    Staples ~7-8%): ROE ≥ Ke threshold relaxed → fewer RIM-skipped →
    POSITIVE delta (sector DROPPED flags)
  - Higher-Ke sectors (Information Technology ~11-12% / Energy
    ~10-12%): ROE ≥ Ke tightened → more RIM-skipped → NEGATIVE delta
  - Neutral sectors (6 GICS sectors at ~9-11%): small delta near zero

Cron #69 + Run #71 universe-wide already confirmed the aggregate:
132 → 109 (−23 tickers, −17.4%). This PR breaks the −23 down by sector.

Scope (10 files, additive only):

  - compute/output/schemas.py — new value_trap_risk_delta_by_sector
    field with full docstring (methodology-scientist verdict +
    Damodaran 2019 anchor + direction semantics)
  - frontend/lib/types.ts — mirror TS field as Record<string, number> | null
  - frontend/lib/schema-snapshot.json — regenerated via --update-snapshot
  - compute/config.py — SCHEMA_VERSION = "0.10.10-phase4.6"
  - compute/main.py — 3 surgical edits mirroring existing scalar
    dual-counter pattern (init two dict[str, int] counters / per-sector
    increment co-located with the existing scalar bump in both branches
    / delta computation in Metadata constructor)
  - tests/test_config.py — schema version pin bump + docstring rewrite
  - tests/test_output/test_value_trap_delta_by_sector_schema.py (NEW) —
    2 active GREEN schema-contract tests (mirror test_wall_clock_schema.py
    pattern from PR #297)
  - CLAUDE.md — §Phase status pointer block refresh
  - AGENTS.md — open-issues #67 status: flip landed + per-sector
    follow-up in flight this PR
  - PHASE_STATUS_INFLIGHT.md — full in-flight entry per PR #237
    side-file convention

Implementation note:

  Per-sector dict construction uses
  `sorted(set(without) | set(with))` for stable key ordering;
  `.get(sec, 0)` fallback handles sectors appearing in only one path;
  `{} or None` falls back to None when both dicts are empty (test-mode
  universe). Co-located with the existing scalar bump in both
  `_rim_flat` (flat-Ke) and `_rim_sector` (sector-Ke) branches at the
  same `value_trap_risk_roe_below_cost_of_equity` reason guard — scalar
  and dict always stay in lockstep.

Verification ladder:

  - ruff check .                              PASS
  - python -m compute.output.schema_check     PASS (triple in sync 0.10.10)
  - pytest tests/test_config.py -v            11/11 PASS (pin held)
  - python -m pytest tests/test_output/       2/2 NEW PASS
  - Full offline suite via test-engineer      1367 → 1369 (+2 NEW)

Pre-push 3-reviewer gate:

  - schema-sentinel (sonnet)        PASS (52 fields, triple aligned,
                                    PATCH bump correct, snapshot
                                    alphabetical ordering held)
  - test-engineer (sonnet)          GREEN (2/2 new tests pass,
                                    1367 → 1369, 0 regressions,
                                    0 skipped stubs)
  - quantrank-reviewer (opus)       READY-TO-PUSH (0 FAIL, 4 WARN
                                    all pre-existing PR-#297-era
                                    drift, defer to next housekeeping
                                    PR — incl. SKILL.md/PHASE_STATUS.md
                                    schema-table tops still on 0.10.8)

Empirical validation gate (post-merge, next cron Run #72):

  - metadata.value_trap_risk_delta_by_sector populates as non-null dict
  - Damodaran shape directionally correct: Util/Real Estate/Staples
    POSITIVE, Information Technology/Energy NEGATIVE
  - sum(delta.values()) == without_sector_coe_count - with_sector_coe_count
    (= 23 per Run #71 universe-wide; matches within rounding)

Note: per-sector accumulation runs in the Step 8 per-ticker loop,
INDEPENDENT of cache-v5 cache busting (PR #298). Field populates on
next cron regardless of warm/cold fetch path.

Hard constraints honored:

  - No new defense flag · No scoring formula change · No Rule 16 /
    Top-5 violation
  - Additive-only schema change (PATCH bump)
  - Field nullable per Rule 18 graceful-degradation
  - Phase 4.5e PR 5 (cluster weight promotion) gate-data UNCHANGED —
    independent track

Methodology decision: methodology-scientist verdict NOT re-requested —
this is the EXACT field shape Mode B Q2 verdict from PR #294 explicitly
authorized. Future re-trigger only if post-merge cron shows sector
breakdown contradicting Damodaran prediction OR Q3 2026-08-19 audit
reads ≥ 6 crons of data and per-sector decay pattern needs interpretation.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa
dackclup added a commit that referenced this pull request May 28, 2026
…ta instrumentation (#300)

Methodology-scientist Mode B Q2 follow-up deferred from PR #294 (sector-
CoE flip, 2026-05-28 05:39 UTC). Adds
`Metadata.value_trap_risk_delta_by_sector: dict[str, int] | None` so
Q3 2026-08-19 quarterly cohort audit has visible per-sector shape
evidence — not just the aggregate `value_trap_risk_count_*_sector_coe`
scalars that landed in PR #204.

Schema PATCH bump 0.10.9 → 0.10.10-phase4.6 (additive Metadata-only).

Methodology context (Damodaran 2019 Ch. 8.4 §"Industry Beta"):

  After `USE_SECTOR_COE = True` per-sector Ke replaces the flat 10%
  baseline at SECTOR_COST_OF_EQUITY (11 GICS sectors, Ke 6%-12%).
  Directional predictions:

  - Lower-Ke sectors (Utilities ~6-7% / Real Estate ~7-8% / Consumer
    Staples ~7-8%): ROE ≥ Ke threshold relaxed → fewer RIM-skipped →
    POSITIVE delta (sector DROPPED flags)
  - Higher-Ke sectors (Information Technology ~11-12% / Energy
    ~10-12%): ROE ≥ Ke tightened → more RIM-skipped → NEGATIVE delta
  - Neutral sectors (6 GICS sectors at ~9-11%): small delta near zero

Cron #69 + Run #71 universe-wide already confirmed the aggregate:
132 → 109 (−23 tickers, −17.4%). This PR breaks the −23 down by sector.

Scope (10 files, additive only):

  - compute/output/schemas.py — new value_trap_risk_delta_by_sector
    field with full docstring (methodology-scientist verdict +
    Damodaran 2019 anchor + direction semantics)
  - frontend/lib/types.ts — mirror TS field as Record<string, number> | null
  - frontend/lib/schema-snapshot.json — regenerated via --update-snapshot
  - compute/config.py — SCHEMA_VERSION = "0.10.10-phase4.6"
  - compute/main.py — 3 surgical edits mirroring existing scalar
    dual-counter pattern (init two dict[str, int] counters / per-sector
    increment co-located with the existing scalar bump in both branches
    / delta computation in Metadata constructor)
  - tests/test_config.py — schema version pin bump + docstring rewrite
  - tests/test_output/test_value_trap_delta_by_sector_schema.py (NEW) —
    2 active GREEN schema-contract tests (mirror test_wall_clock_schema.py
    pattern from PR #297)
  - CLAUDE.md — §Phase status pointer block refresh
  - AGENTS.md — open-issues #67 status: flip landed + per-sector
    follow-up in flight this PR
  - PHASE_STATUS_INFLIGHT.md — full in-flight entry per PR #237
    side-file convention

Implementation note:

  Per-sector dict construction uses
  `sorted(set(without) | set(with))` for stable key ordering;
  `.get(sec, 0)` fallback handles sectors appearing in only one path;
  `{} or None` falls back to None when both dicts are empty (test-mode
  universe). Co-located with the existing scalar bump in both
  `_rim_flat` (flat-Ke) and `_rim_sector` (sector-Ke) branches at the
  same `value_trap_risk_roe_below_cost_of_equity` reason guard — scalar
  and dict always stay in lockstep.

Verification ladder:

  - ruff check .                              PASS
  - python -m compute.output.schema_check     PASS (triple in sync 0.10.10)
  - pytest tests/test_config.py -v            11/11 PASS (pin held)
  - python -m pytest tests/test_output/       2/2 NEW PASS
  - Full offline suite via test-engineer      1367 → 1369 (+2 NEW)

Pre-push 3-reviewer gate:

  - schema-sentinel (sonnet)        PASS (52 fields, triple aligned,
                                    PATCH bump correct, snapshot
                                    alphabetical ordering held)
  - test-engineer (sonnet)          GREEN (2/2 new tests pass,
                                    1367 → 1369, 0 regressions,
                                    0 skipped stubs)
  - quantrank-reviewer (opus)       READY-TO-PUSH (0 FAIL, 4 WARN
                                    all pre-existing PR-#297-era
                                    drift, defer to next housekeeping
                                    PR — incl. SKILL.md/PHASE_STATUS.md
                                    schema-table tops still on 0.10.8)

Empirical validation gate (post-merge, next cron Run #72):

  - metadata.value_trap_risk_delta_by_sector populates as non-null dict
  - Damodaran shape directionally correct: Util/Real Estate/Staples
    POSITIVE, Information Technology/Energy NEGATIVE
  - sum(delta.values()) == without_sector_coe_count - with_sector_coe_count
    (= 23 per Run #71 universe-wide; matches within rounding)

Note: per-sector accumulation runs in the Step 8 per-ticker loop,
INDEPENDENT of cache-v5 cache busting (PR #298). Field populates on
next cron regardless of warm/cold fetch path.

Hard constraints honored:

  - No new defense flag · No scoring formula change · No Rule 16 /
    Top-5 violation
  - Additive-only schema change (PATCH bump)
  - Field nullable per Rule 18 graceful-degradation
  - Phase 4.5e PR 5 (cluster weight promotion) gate-data UNCHANGED —
    independent track

Methodology decision: methodology-scientist verdict NOT re-requested —
this is the EXACT field shape Mode B Q2 verdict from PR #294 explicitly
authorized. Future re-trigger only if post-merge cron shows sector
breakdown contradicting Damodaran prediction OR Q3 2026-08-19 audit
reads ≥ 6 crons of data and per-sector decay pattern needs interpretation.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 28, 2026
PR #293 (95e638b, merged 2026-05-28 05:20 UTC) retired the Site-2
output-level data-quality ceiling per methodology-scientist Mode B
Option C verdict (Penman 2013 §7.4 + Damodaran 2019 Ch. 18 + Huber
1981 §1.4). PR #293 deleted the call site at compute/valuation/
ensemble.py Step 4.5 but RETAINED the 2 helper functions as dead code
for "one cycle hold" — explicit retention guard pinned the deferred
state. This PR closes that contract.

Cron Run #71 (368dccd, 2026-05-28 08:44 UTC) confirmed clean
operation:

  - NVR fair-price section renders correctly (was empty pre-PR-#293)
  - valuation_output_anomalous cohort dropped 5 → 4 (NVR removed)
  - No regression on Site-1 veto cohort (MTB / CPT / MRNA / HBAN
    per PR #265 writer-parity emit still firing)

Scope (8 files, net −41 lines):

  - compute/valuation/ensemble.py — REMOVED 2 dead functions:
    - _has_corrupt_input(methods) -> bool (13 lines)
    - _data_quality_corrupt_result(methods) -> EnsembleResult (43 lines)
    Step 4.5 comment block at lines 449-479 refreshed: "kept as dead
    code for one cycle" → "removed in this PR after cron Run #71
    confirmed clean".

  - tests/test_valuation/test_ensemble.py — REMOVED 2 imports +
    3 active tests that exercised the removed functions:
    - test_data_quality_sanity_guard_triggers_on_extreme_method_value
    - test_data_quality_guard_boundary_exactly_at_ceiling
    - test_data_quality_guard_skipped_methods_dont_trigger
    The one-cycle retention guard test_L2_dead_code_functions_still_
    callable_after_site2_deletion REPLACED with new
    test_L2_dead_code_functions_removed_post_one_cycle that pins the
    removal via `not hasattr(_ensemble, "_has_corrupt_input")` +
    `not hasattr(_ensemble, "_data_quality_corrupt_result")` — so a
    future accidental re-introduction surfaces as a clear "Issue #289
    retirement reverted" failure. The 2 surviving tests
    (test_site2_data_quality_guard_retired_post_issue_289 +
    test_L3_site2_ceiling_not_invoked_for_high_share_price_ticker)
    keep verifying POST-RETIREMENT invariants on the NVR cohort.

  - compute/config.py:127-148 — FAIR_PRICE_DATA_QUALITY_CEILING
    STAYS ACTIVE (Site-1 in compute/scoring/risk_overlay.py shares
    the constant). Comment block updated to note "dead-code helpers
    REMOVED in the PR #293 follow-up after cron Run #71 confirmed
    clean".

  - compute/valuation/applicability.py:65-78 — reference to
    _data_quality_corrupt_result replaced with reference to the
    writer-parity emit at compute/main.py on the Site-1 veto cohort.

  - tests/test_scoring/test_risk_overlay.py:520-528 —
    test_D4_data_quality_corruption_fires_at_boundary_strict
    docstring updated: "Mirrors _has_corrupt_input's strict
    inequality" → "Site-1 (here) is the canonical input-corruption
    guard. Site-2 was retired per Issue #289 Option C; the strict `>`
    invariant lives ONLY here now." Test logic UNCHANGED.

  - CLAUDE.md §Phase status — drained "(in flight this PR — Issue
    #67 follow-up)" wording (PR #300 merged); added in-flight note
    for this PR's Site-2 dead-code removal.

  - AGENTS.md open-issues #289 entry — appended "dead-code helpers
    REMOVED in the PR #293 follow-up after cron Run #71 confirmed
    the retirement empirically".

  - PHASE_STATUS_INFLIGHT.md — full in-flight entry appended per
    PR #237 side-file convention.

Verification ladder:

  - ruff check .                          PASS
  - python -m compute.output.schema_check PASS (no schema touched)
  - python -m pytest tests/test_valuation/test_ensemble.py
    tests/test_scoring/test_risk_overlay.py tests/test_config.py -q
                                          120 PASS (was 123 — net -3
                                          as planned: 3 deleted tests
                                          + 1 modified L2 guard)
  - grep -rn "_has_corrupt_input|_data_quality_corrupt_result"
    compute/ tests/                       only legitimate references
                                          remain (retirement comments
                                          + new removal-guard
                                          assertion strings)

Hard constraints honored:

  - No scoring formula change · No Rule 16 / Top-5 violation
  - Schema version UNCHANGED at 0.10.10-phase4.6
  - Site-1 input-corruption veto path UNCHANGED
  - FAIR_PRICE_DATA_QUALITY_CEILING constant retained (Site-1 shared)
  - Writer-parity valuation_output_anomalous emit at compute/main.py
    UNCHANGED — UI explanation chip continues for Site-1 cohort
  - test_L3 PR #293 end-to-end guard UNCHANGED — keeps verifying NVR
    cohort produces non-null median

Closes the PR #293 retirement contract.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 30, 2026
…#326 §Gotchas (#327)

Full docs-reviewer substance pass on CLAUDE.md → 5 MUST-FIX + 4 SHOULD-FIX, all fixed. CLAUDE.md only (+ PHASE_STATUS_INFLIGHT.md lockstep entry); no code/schema/compute/frontend change.

MUST-FIX: §Phase status "In flight" marked PR #312 (tasteful-motion) as "THIS PR" → merged (e602485); "Recently merged" frozen at #310 → drained #311#326 (16 entries, SHAs verified); "Next deliverables" Issue #67 flip listed pending → DONE (PR #294, USE_SECTOR_COE=True); "Section A-H/A-J" → "A-L" (helper is A-L since PR #221).

SHOULD-FIX: §Stack "Phase 3b on this PR" → "(merged)" + edgartools 5.31→5.32; §Gotchas compute/main.py line refs re-anchored (840→879 · 1965-66→2084-85 · 717→728 · 785→805 · 972→1025); +2 §Gotchas for PR #326 invariants (sidebar data-rail↔globals.css pre-paint lockstep; AreaChart re-park debounce ≥300ms).

Confirmed no drift: schema 0.10.11-phase4.6 · skills 46 · agents 19 · hooks 3. docs-reviewer re-check: DOCS-CLEAN (all 16 SHAs match, 5 line refs accurate, 2 gotchas code-backed). Lockstep via PHASE_STATUS_INFLIGHT.md side-file (PR #237).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants