diff --git a/.gitignore b/.gitignore index 29b6af6f9..3dc9c6aac 100644 --- a/.gitignore +++ b/.gitignore @@ -530,6 +530,8 @@ docs/architecture/session-memory/* docs/architecture/plan-integration/* !docs/architecture/plan-integration/council/ !docs/architecture/plan-integration/receipts/ +# Un-ignored 2026-09-18 — item 6's written proposals (D-D, D-E), Markdown only. +!docs/architecture/plan-integration/proposals/ !docs/architecture/plan-integration/*.architecture.json !docs/architecture/plan-integration/*.md diff --git a/docs/architecture/plan-integration/proposals/2026-09-16-context-derived-plan-bias.md b/docs/architecture/plan-integration/proposals/2026-09-16-context-derived-plan-bias.md new file mode 100644 index 000000000..251922c42 --- /dev/null +++ b/docs/architecture/plan-integration/proposals/2026-09-16-context-derived-plan-bias.md @@ -0,0 +1,106 @@ +# Proposal: a context-derived plan bias (D-D) + +**Status:** written proposal, no code (plan-integration follow-on item 6, T6.4). Becomes a +`ready-for-agent` issue at the push step (HANDOFF §3 item 7, step 4). Written 2026-09-18 against +`main` at `a5b9f903`; every code fact below was read from that tree. + +## The owner's decision, verbatim + +> **D-D** | **F2 → open a ticket, don't park:** a context-derived plan bias (prerequisite edges from the +> concept store via `get_concept_context`, milestone order; per-item energy demand from struggle state) +> — deterministic and rubric-testable. Not an LLM tie-break (unauditable; defeats D-16). Scenario 1's +> "the logical step before" was the first evidence. +> +> — `HANDOFF-2026-09-16.md` §2, owner, 2026-09-16 + +The evidence it names: rubric row 1 (`receipts/now-rubric-2026-09-16.md`), owner's verdict **yes** with +the line *"window function is the logical step before decorating it"* — a prerequisite-order argument the +engine did not make. The engine ranked the plan-matching due item first because it carried the plan +bias; the owner ranked it first because of what depends on it. Same answer, different reason, and the +reason is the one that generalises. + +## What the engine does today (`studyloop/learning/decision.py`) + +- **Rule 5, one constant.** `PLAN_RELATED_BIAS = 12`, added to a candidate's score when it carries a + `plan_ref` or its concept/topic keys intersect a matchable plan's keys. The constant is sized to decide + a near-tie *inside one urgency class* and to lose to a clearly more-urgent unrelated candidate + (a struggling repair at +35, an overdue review whose base is `100 + min(days_ago, 30)`): a bias, + never a filter. Review 3 F2 asked whether `now` should grow explicit urgency classes; the council kept + the bias and pinned the constant to today's bands. +- **Rule 3, one floor per plan.** `ENERGY_CAPABILITY = {"low": 3, "medium": 6, "high": 10}` is compared + with the plan's `energy_floor` (1–10); below the floor *new* milestone work is deferred while + plan-related due recall and struggle repair stay eligible, "because repair is cheaper than encoding". + Rubric row 3 (owner: **no**) is the counter-example — a live-struggle repair on a low-energy day — + and is item 5's subject, not this proposal's. +- **Rule 6, milestone order by position.** A synthesised next-milestone candidate is the plan's first + open milestone, base `MILESTONE_BASE_SCORE = 48` plus a target-urgency bonus. Milestone order is the + document's order; nothing reads what a milestone's concepts require. +- **What the concept store already knows.** `learning/mastery.py::list_dependencies(topic)` returns + edges with `source_concept`, `target_concept`, `relation_type`; `weak_links_for_topic` already + consumes `relation_type == "prerequisite"` to name struggling concepts that block downstream ones. + `get_concept_context` (MCP) returns `mastery_graph_json`'s `edges` for a topic, with the same fields, + capped at 32 KiB and explicit that omitted edges cannot support a claim that no alternative exists. + +So the ingredients exist and are already read for another purpose; what is missing is a rule that turns +them into a ranking signal for `now`. + +## The proposal + +Replace the single constant with a **derived bias** computed from three deterministic inputs, each a +rule with its own rubric row. The total stays bounded so the "bias, never a filter" invariant holds: +a clearly more-urgent unrelated candidate must still win. + +| Rule | Input | Signal | Bound | +|---|---|---|---| +| 5a — plan relevance (today's rule 5) | plan refs / key intersection | the existing `+12` | as today | +| 5b — prerequisite order | `list_dependencies(topic)` edges with `relation_type == "prerequisite"` | a candidate whose concept is a **prerequisite of an open milestone's concept** in a matchable plan gains a small bonus; a candidate whose concept **depends on** a concept the learner is `struggling` with loses the same amount ("the logical step before" comes first) | ± a value below the urgency-class gap, pinned like `PLAN_RELATED_BIAS` | +| 5c — per-item energy demand | struggle state (`confidence`, `last_teachback_score`) and action type | a **demand** per candidate (repair of a live struggle is high demand; a recall of a `learning` concept is low) compared with `ENERGY_CAPABILITY[energy]`, so rule 3's floor stops being the plan's only energy fact | shared with item 5 (D-F); this proposal takes item 5's definition when it lands rather than inventing a second | + +Design constraints, from the decision: + +1. **Deterministic.** Every input is a stored fact (an edge, a confidence, a score); the bias for a + fixed world is a pure function. No model call anywhere in ranking — an LLM tie-break is + unauditable and defeats the D-16 rubric, which asks a human "would you do the primary?" and needs + the answer to follow from stated rules. +2. **Rubric-testable.** Each rule gets a D-16 rubric row with a planted world and an expected primary, + scored by the owner before it ships: row 1 re-run under 5b should still be **yes** and now for the + engine's reason; a new row plants a struggling prerequisite and expects the dependent milestone + *not* to be primary. +3. **Still a bias.** The golden `now_plan_no_active.json` stays byte-identical (no plan → no bias); the + existing rule-5 pins (`test_matching_due_concept_outranks_unrelated_same_urgency`, + `test_unrelated_more_urgent_due_outranks_new_milestone`, `test_weak_due_still_beats_overdue_synthesised_milestone`) + stay green with the derived value in place of the constant. +4. **Partial knowledge is not knowledge.** `get_concept_context` is explicit that omitted edges cannot + support "no alternative exists"; 5b must treat an absent edge as *no signal*, never as "not a + prerequisite". +5. **Read once, through the seam.** Edges are read in `_PlanContext.build` beside the plan read, so the + rule-1 pin (no per-consumer plan reads, no checkpoint-history reads) holds; the concept store is a + second read, budgeted and measured on the live database before it ships (the item-4 preview read was + measured at ~320 ms on an 877 MB `sessions.db` and recorded; this must be too). + +## Out of scope + +- Item 5 (D-F) owns the energy-demand *definition* and the body-doubling floor; this proposal consumes + it and must not define a second one. +- No change to what a plan document stores. Prerequisite knowledge lives in the concept store; the + plan keeps naming concepts per milestone. +- No filter, no reordering of urgency classes (review 3 F2 stands). + +## Acceptance (for the issue) + +- `PLAN_RELATED_BIAS` is replaced by a derived value with a pinned upper bound; the three rule-5 pins + above and the golden byte-identity stay green unchanged. +- RED tests, one per rule: a prerequisite-of-open-milestone candidate outranks a same-class sibling; + a candidate dependent on a struggling concept loses to that concept's repair; an absent edge changes + no score. +- A measured read cost on a real `sessions.db`, recorded beside the constant. +- Rubric rows added and scored by the owner (D-16), row 1 re-run. +- Docs: `docs/study-plans.md` "Plan-aware now" describes the derived bias in the eligibility terms the + contract test pins. + +## References + +`HANDOFF-2026-09-16.md` §2 D-D; `receipts/now-rubric-2026-09-16.md` rows 1–3; `council/review-3-arbitration-2026-09-16.md` +F2; `studyloop/learning/decision.py` (`PLAN_RELATED_BIAS`, `ENERGY_CAPABILITY`, `MILESTONE_BASE_SCORE`, +rule 5 application site); `studyloop/learning/mastery.py` (`list_dependencies`, `weak_links_for_topic`, +`agent_concept_context`). diff --git a/docs/architecture/plan-integration/proposals/2026-09-16-overdue-nudge-and-retire.md b/docs/architecture/plan-integration/proposals/2026-09-16-overdue-nudge-and-retire.md new file mode 100644 index 000000000..08f334ecf --- /dev/null +++ b/docs/architecture/plan-integration/proposals/2026-09-16-overdue-nudge-and-retire.md @@ -0,0 +1,114 @@ +# Proposal: an age-aware nudge and a retire/snooze door for an overdue item (D-E) + +**Status:** written proposal, no code (plan-integration follow-on item 6, T6.4). Becomes a +`ready-for-agent` issue at the push step (HANDOFF §3 item 7, step 4). Written 2026-09-18 against +`main` at `a5b9f903`; every code fact below was read from that tree. + +## The owner's decision, verbatim + +> **D-E** | Scenario 2 note: an overdue item **unrelated** to the plan must not sit as an alternate +> indefinitely. Fact: due score already grows `+1/day` (cap +30), so it overtakes the +12 bias in +> ~2 weeks; missing are an **age-aware nudge line** and a **retire/snooze** action for a due card (only +> backlog topics can be `resolved` today). +> +> — `HANDOFF-2026-09-16.md` §2, owner, 2026-09-16 + +The evidence it names: rubric row 2 (`receipts/now-rubric-2026-09-16.md`), owner's verdict **yes** — +clear the overdue review first — with the note: *"an overdue item unrelated to the plan must not sit as +an alternate indefinitely — propose it explicitly (age-aware nudge) or let the learner retire it."* + +## What the engine and the store do today + +- **A due item is a `study_progress` row whose `last_seen` is old enough.** `history/progress.py` + computes `days_ago` from `last_seen` and marks the row due when it reaches an interval in + `REVIEW_INTERVALS` — 1, 3, 7, 14, 30 days, each with a review type ("5-min recall quiz" … + "Teach-back session"); a `struggling` row is always due as "Guided repair + tiny practice". There is + **no `next_review` column** and no state that says "not due": a `mastered` row keeps coming back on + the same intervals, and the only thing that moves `last_seen` is studying it again + (`record_progress`). A row therefore stays due — and stays a `now` candidate — until it is studied + or deleted. +- **The score arithmetic the decision cites is right.** `learning/decision.py` scores a due candidate + `100 + min(days_ago, 30)` (+35 if `struggling`, +15 if `learning`). A plan-related candidate adds + `PLAN_RELATED_BIAS = 12` (rule 5). So an unrelated due item at *d* days beats a plan-related one at + *d′* days once `d > d′ + 12`: roughly two weeks of sitting as an alternate, then it wins the primary + on age alone — silently. The learner sees it move up; nothing tells them why, and nothing offers a + way to say "I have moved on from this". +- **`resolved` exists only for parked topics.** `record_topic_progress(confidence="resolved")` (MCP) + calls `parking.resolve_parked_topic(topic_id)` — a *backlog* row, not a `study_progress` row. The + Today card and CLI `now` have no control on a due item at all. + +## The proposal + +Two additions, both small, both on the existing candidate — no new ranking rule. + +### 1. An age-aware nudge line + +When an eligible due candidate is **unrelated to every matchable plan** and has been due long enough +to have overtaken the plan bias — i.e. `days_ago ≥ threshold`, with the threshold **derived from the +constants**, not a second number: the smallest `days_ago` at which `min(days_ago, 30) ≥ PLAN_RELATED_BIAS` +plus the plan-related candidate's own age — the candidate carries a `nudge` line that says so in plain +words: *"Overdue 16 days and unrelated to your plan — do it, or retire it: `studyloop review retire + `."* Rendered where the completion review's evidence lines are rendered today (CLI +`now` beneath the action, Today card beside it, MCP payload as an additive key). The line is a fact +about age, not a proposal to rank differently. + +### 2. A retire/snooze door for a due item + +A `study_progress` row gains one of two learner-issued states, through the seam every surface uses: + +| Door | Meaning | Mechanism | Undo | +|---|---|---|---| +| **retire** | "I am done with this concept for now" | a `retired_at` timestamp on the row (or a `confidence` value the due predicate excludes — decided at RED with the migration); the row is no longer due, keeps its history, and disappears from every consumer of `spaced_repetition_due` — `now` (CLI, Today, MCP `get_next_action`), recap, `studyloop review`, and the plan evaluation's due count | studying it again (`record_progress`) clears the state | +| **snooze** | "not this week" | a `snoozed_until` date; the row is not due before it and returns to the normal intervals after | expiry, or an explicit un-snooze | + +Exposed the same way the plan lifecycle is: a CLI verb (`studyloop review retire|snooze`), a Today-card +control on the due item, and one MCP tool beside `record_study_progress` — the mirror of +`record_topic_progress(confidence="resolved")` for cards that the decision asks for. The store writes +one row; retire is never inferred from age (the decision says *let the learner* retire it). + +Design constraints: + +1. **Learner-issued only.** Nothing retires or snoozes on the learner's behalf — the RSD-safe framing + the project keeps: the nudge proposes, the learner decides (same shape as item 4's consensual + close). +2. **History kept.** A retired row is excluded from the due set, not deleted; `get_study_history` and + the plan evaluation's `unverified_milestones` still see it. +3. **Plan-aware, not plan-bound.** The nudge fires only for items unrelated to every matchable plan; + a plan-related overdue item is the plan's business (item 4's closing review already counts it). +4. **Golden unchanged.** The no-plan golden holds a `source=starter` primary with no due items, so it + is byte-identical by construction; the nudge is an additive key on a due candidate. +5. **The threshold is derived, not a new constant** — so when D-D replaces `PLAN_RELATED_BIAS` with a + derived bias, the nudge moves with it. + +## Out of scope + +- Any change to `REVIEW_INTERVALS` or to how `days_ago` is computed. +- Reranking. The overtaking-on-age behaviour is correct (row 2: clear the overdue review first); what + is missing is the *explanation* and the *exit*, not a different order. +- Flashcard/quiz `card_reviews` — a different store with its own scheduler (a simplified SM-2 in + `review_db.py`, read by `get_due_cards` through `review_service.due_cards`); a retire door for those + is a separate ticket if wanted. + +## Acceptance (for the issue) + +- RED: a due unrelated item past the derived threshold carries the nudge line on CLI, Today and MCP; + a plan-related one does not; one below the threshold does not. +- RED: `retire` removes the row from the due set and from `now` candidates, keeps its history row, + and `record_progress` on the same concept clears it; `snooze` excludes it until the date and not + after. +- The migration is in `agent_session_tools.migrations` and the clean-rebuild closure classifies the + new column/state (the 2026-09-12 rebuild caught two tables the migrations did not create; do not + repeat that). +- Golden `now_plan_no_active.json` byte-identical; `test_docs_plan_integration_contract.py` green + with the doc sentence added to `docs/study-plans.md` "Plan-aware now" and `docs/cli-reference.md`. +- A D-16 rubric row: row 2's world advanced 16 days, the owner asked whether the nudge line is one + they would act on. + +## References + +`HANDOFF-2026-09-16.md` §2 D-E; `receipts/now-rubric-2026-09-16.md` row 2; `studyloop/history/progress.py` +(`REVIEW_INTERVALS`, `_review_type_for`, `_progress_review_due`, `spaced_repetition_due`, +`record_progress`); `studyloop/learning/decision.py` (due scoring `100 + min(days_ago, 30)`, +`PLAN_RELATED_BIAS`); `studyloop/mcp/tools.py` (`record_topic_progress`, `resolve_parked_topic`); +`agent_session_tools/migrations.py` (`study_progress` columns: `id, topic, concept, confidence, +first_seen, last_seen, session_count, notes, created_at, updated_at`). diff --git a/docs/architecture/plan-integration/receipts/issue-closeout-draft-2026-09-16.md b/docs/architecture/plan-integration/receipts/issue-closeout-draft-2026-09-16.md index 3a4c3bc3d..2d42855f1 100644 --- a/docs/architecture/plan-integration/receipts/issue-closeout-draft-2026-09-16.md +++ b/docs/architecture/plan-integration/receipts/issue-closeout-draft-2026-09-16.md @@ -1,6 +1,22 @@ # Plan integration (#7–#15) — issue close-out DRAFT · 2026-09-16 -**Status: DRAFT, not posted.** Written unattended by the Phase-6 agent for the owner to post in the +**Status: POSTED 2026-09-18** (was: DRAFT, not posted). The per-issue tables below were posted as +status comments on #8–#15 after re-verification against `main` at `a5b9f903`: every `T:` node id +checked against the collected suite, every `C:` sha against `main` — seven pre-consolidation shas +(`1d071758`, `3a4f6b01`, `705ba58b`, `c16ffa35`, `daf46c81`, `dc7de0be`, `e16340ca`) were rewritten +at the history consolidation and were replaced in the comments by their `main` equivalents, found by +exact subject (`38f6f41d`, `d7f568bf`, `e50106af`, `e74c2a63`, `bfe0695c`, `3d9476d5`, `1aed49f2`); +`test_malformed_plan_browse_matches_store_list` had moved to `test_plan_application_mutations.py`. +Rows the follow-on programme had overtaken were restated in the comments, not here: #11's mission row +(revisable since item 3b), #13's "partly" (closed by item 1), #14/#15/#7's brain-dump and cancellation +gaps (closed by item 2 and the 2026-09-18 abandonment decision), #10's rubric state (rows scored except +3b). **Closed as completed:** #8, #9, #11, #12, #13, #14. **Open:** #10 (row 3b, item 5), #15 (closes +with #10), and #7 — which GitHub had auto-closed at PR #20's merge on the body's "Closes the two bugs" +phrase and which was reopened with the parent mapping so it closes after its children, as §3 item 7 +of the handover intends. PR #20's body was not replaced: the PR is merged. The original draft text +follows unchanged as the record of what was known on 2026-09-16. + +**Original status (2026-09-16): DRAFT, not posted.** Written unattended by the Phase-6 agent for the owner to post in the morning, and refreshed after council review 5 (`GATE: ACCEPT`, `council/review-5-arbitration-2026-09-16.md`), the verification receipt and the archive. Nothing here has been sent to GitHub: no issue closed, no comment left, PR #20 untouched. Every claim below names the diff --git a/openspec/changes/plan-integration-followons/design.md b/openspec/changes/plan-integration-followons/design.md index 0d3a4c922..c847d6967 100644 --- a/openspec/changes/plan-integration-followons/design.md +++ b/openspec/changes/plan-integration-followons/design.md @@ -238,6 +238,36 @@ among what MCP revises and the row names every schema property. - **Rubric row 3b** (owner scores): scenario 3's fixture at low energy now yields the deferred repair named in `energy_deferred` and a body-double primary (or the due recall if one exists). +**T5.1 review against the code (2026-09-18, tree `7208eb67`) — three amendments, each from reading +`learning/decision.py`, not the text above:** + +1. **Demand classes are the struggle collector's classes.** `_struggle_candidates` emits a row only when + `confidence in ("struggling", "learning")` or `last_teachback_score < 14`; "recovered / gentle review" is not a + row it produces. So: `struggling` with `last_seen` ≤ 14 days → `high`; `struggling` older than 14 days, or any + row whose only signal is a weak teach-back → `medium`; `learning` → `low`. Demand is derived once, in the + collector, and carried in the candidate's `metadata` beside `confidence` so the scorer and the renderers read + one value. Required capability `high → 6`, `medium → 4`, `low → 0` stands (the `low` class is what "repair is + cheaper than encoding" was always about). +2. **`energy_deferred` is milestone-shaped and cannot carry a repair as it is.** `DeferredMilestone` has a + mandatory `milestone_index`, and all three renderers (`cli/_now.py`, `learning/recap.py`, + `today-panel.js::deferredNotes`) print `milestone {index + 1} "{title}" needs energy {floor}/10`. A deferred + repair gets its own frozen `DeferredRepair` (`plan_id`/`plan_title` when plan-related, else `None`, `concept`, + `topic`, `confidence`, `energy_demand`, `required_capability`, `energy_capability`, `reason` naming the + struggle), carried in a **new additive key `energy_deferred_repairs`** — not folded into `energy_deferred`, + whose consumers would print "milestone None". Same "readable off the top" rule as the closing review's + evidence lines: each renderer gains one line per deferred repair. +3. **The body-double door is a session start, not `web/routes/body_double.py`.** That route is the read-only focus + reader (`GET /api/body-double/focus`). The session door is `studyloop study "" --mode co-study` on the + CLI and a session start from the Body Double view (origin `body-double`) on the Web. `_evidence_command` has + no branch for a `conversation` candidate and would fall through to `studyloop progress … -c learning`, which is + a write, not a door — so the body-double candidate carries `evidence_command = 'studyloop study "" + --mode co-study'` set explicitly, and `_evidence_command` is not asked to guess. `source="body_double"`, + `action_type="conversation"`, base score below `MILESTONE_BASE_SCORE` (48) so any real candidate outranks it. + +Rule 3's *deferral* of repair is the change; rule 3's *eligibility* of plan-related due recall is untouched. The +no-plan golden stays byte-identical because a body-double candidate requires an active plan and the golden world +has none; `INTERLEAVE_RATIOS["low"]` unchanged. + ## 6. Verification `scripts/verify/plan_integration.py` gains registered checks for: the two architect grants (the ten names in diff --git a/openspec/changes/plan-integration-followons/tasks.md b/openspec/changes/plan-integration-followons/tasks.md index c0cf4814e..a8f912e72 100644 --- a/openspec/changes/plan-integration-followons/tasks.md +++ b/openspec/changes/plan-integration-followons/tasks.md @@ -174,7 +174,9 @@ writer, through the existing gate, closes that. Kept out of item 3 so item 3's f mutation-proved. Refuted with a named test: qwen's empty-agent claim. Carried to the owner (arbitration §"Still open"): the abandonment contract (F3b), `plan close` on checked non-active plans, the `session-db` prompt probe. Two cheap pins deferred: `test_plan_repair_nonactive_unready_is_noop_with_pointer`, - `test_duplicate_learning_record_with_mission_revision_still_saves_once`.) Reproduce every 🔴/🟡 by probe or + `test_duplicate_learning_record_with_mission_revision_still_saves_once` — **landed 2026-09-18**, each proved + discriminating by mutating the branch it guards: the repair pin fails when the non-active branch falls through + to the launch, the record pin fails when `not mission_updates` is dropped from the short-circuit.) Reproduce every 🔴/🟡 by probe or RED test before accepting; arbitration `council/review-6-arbitration-2026-09-16.md` ends `GATE: ACCEPT|FAIL`; corrections one commit per finding. - [x] **T6.3** (`receipts/verify-d0251fd1.json`, tree clean, 31 checks — 29 + `architect-grants` (in-process, @@ -190,8 +192,13 @@ writer, through the existing gate, closes that. Kept out of item 3 so item 3's f ## Item 5 — energy demand + body-doubling floor (D-F) · own round -- [ ] **T5.1** Design §5 reviewed against the code (`_struggle_candidates`, `_score_candidates`, rule 3) — amend if - the code contradicts it. +- [x] **T5.1** Design §5 reviewed against the code (`_struggle_candidates`, `_score_candidates`, rule 3) — amend if + the code contradicts it. (2026-09-18: three amendments recorded under §5 — the demand classes are the struggle + collector's own (`struggling` fresh/old, weak teach-back, `learning`; no "recovered" row exists); a deferred + repair needs its own `DeferredRepair` in a new additive `energy_deferred_repairs` key because `DeferredMilestone` + and its three renderers are milestone-shaped; the body-double door is `studyloop study … --mode co-study` / + the Body Double view's session start, not the read-only `body_double.py` focus route, so the candidate sets + its `evidence_command` explicitly. T5.2's RED names hold; a sixth test pins the new key's rendering.) - [ ] **T5.2** RED `tests/test_now_plan_guidance.py`: `test_live_struggle_repair_defers_at_low_energy_like_new_work`, `test_recovered_repair_stays_eligible_at_low_energy`, `test_body_double_candidate_is_synthesised_when_nothing_plan_related_fits`, `test_body_double_is_a_proposal_not_a_filter`, `test_body_double_never_appears_without_an_active_plan`, @@ -203,9 +210,25 @@ writer, through the existing gate, closes that. Kept out of item 3 so item 3's f ## Item 6 — proposals (no code) -- [ ] **T6.4** `docs/architecture/plan-integration/proposals/2026-09-16-context-derived-plan-bias.md` (D-D) and - `…-overdue-nudge-and-retire.md` (D-E); both become issues at the push step. +- [x] **T6.4** `docs/architecture/plan-integration/proposals/2026-09-16-context-derived-plan-bias.md` (D-D) and + `…-overdue-nudge-and-retire.md` (D-E); both become issues at the push step. (Written 2026-09-18 against + `main` `a5b9f903`: each quotes the owner's decision verbatim, states what the engine and store do today from + the code (`PLAN_RELATED_BIAS = 12`, `ENERGY_CAPABILITY`, due score `100 + min(days_ago, 30)`, + `REVIEW_INTERVALS`, `list_dependencies` prerequisite edges, `resolve_parked_topic` as the only `resolved` + path), then the proposal, constraints, out-of-scope and issue acceptance. D-D consumes item 5's energy-demand + definition rather than defining a second. The two issues are item 7 step 4.) ## Item 7 — push step (owner present; HANDOFF §3 item 7) -- [ ] **T7.1** Not run unattended. Ruleset D-I; tokens D-J. +- [ ] **T7.1** Not run unattended. Ruleset D-I; tokens D-J. **State 2026-09-18** (HANDOFF §3 item 7's seven + steps): (1) pushes — done, owner pushed `main` three times (#20 `46262d23`, #22 `4bba58b6`, #23 `a5b9f903`), + each a fast-forward after CI green on the PR; (2) ruleset D-I — done three times (2026-09-17 + `feat/knowledge-proof` + `fix/plan-integration-bugs`; 2026-09-18 `feat/plan-close` + + `fix/seam-test-db-bootstrap`), each a ≤ 6 s window with the GH013 refuse-proof first and the ruleset re-read + byte-equal afterwards; single `main` on both sides. (3) closeout comments — done: posted on #8–#15 from the + re-verified draft (`receipts/issue-closeout-draft-2026-09-16.md`, status paragraph records the shas rewritten + and the rows overtaken); #8, #9, #11, #12, #13, #14 closed as completed; #10 and #15 open (row 3b); #7 + reopened (auto-closed by PR #20's body) with the parent mapping. PR #20's body left as merged. **Open:** + (4) the two item-6 issues (after T6.4) and #21's closing comment or decision; (5) the local tag + `archive/feat-clean-start-2026-09-15` — owner: push or discard; (6) the GitHub Support ticket text — owner; + (7) revoke both tokens and delete `~/tmp/.env` — owner (D-J). diff --git a/packages/studyloop/tests/e2e/test_session_recovery_journey.py b/packages/studyloop/tests/e2e/test_session_recovery_journey.py index d6a6901a2..7ee1c31a5 100644 --- a/packages/studyloop/tests/e2e/test_session_recovery_journey.py +++ b/packages/studyloop/tests/e2e/test_session_recovery_journey.py @@ -347,7 +347,43 @@ def test_409_from_a_second_tab_offers_reattach_that_adopts_the_session( d.agent = 'codex'; }""" ) - page.locator("[data-testid='study-start-session']").click() + # Twice on CI this click has timed out with "element is not visible" + # (runs 35341660469 and 35380095596) after the settled wait above passed, + # and neither run produced evidence of WHY the picker was hidden: the + # timeout fires before this test's own _diag hook, so the artifact held + # nothing for it. Every adopt path in the timer was read against the + # second failure and none explains it without a click. Capture the + # state at the click so a third occurrence names the mechanism instead + # of the symptom -- the fix must rest on that, not on a guess. + try: + page.locator("[data-testid='study-start-session']").click() + except Exception: # pragma: no cover - diagnostics only + state = page.evaluate( + """() => { + const root = document.querySelector('[x-data="sessionTimer()"]'); + const d = root && window.Alpine.$data(root); + const picker = document.querySelector('.study-start-picker'); + const button = document.querySelector("[data-testid='study-start-session']"); + return { + view: window.Alpine.store('nav').current, + sessionActive: d && d.sessionActive, + starting: d && d.starting, + topic: d && d.topic, + agent: d && d.agent, + resolvedTopic: d && d.resolvedTopic(), + conflict: d && d.conflictSession ? d.conflictSession.study_session_id : null, + pickerDisplay: picker && getComputedStyle(picker).display, + buttonDisabled: button && button.disabled, + url: location.href, + }; + }""" + ) + RESULTS.mkdir(parents=True, exist_ok=True) + (RESULTS / "session-recovery-409-click-timeout.json").write_text( + json.dumps(state, indent=2), encoding="utf-8" + ) + _diag(page, "409-click-timeout") + raise error = page.locator(".study-start-picker .picker-error") try: diff --git a/packages/studyloop/tests/test_cli_plan_seam.py b/packages/studyloop/tests/test_cli_plan_seam.py index da3bd97f3..6508eddc8 100644 --- a/packages/studyloop/tests/test_cli_plan_seam.py +++ b/packages/studyloop/tests/test_cli_plan_seam.py @@ -680,6 +680,39 @@ def test_plan_repair_on_a_ready_plan_says_nothing_to_repair(runner, tmp_path, mo assert calls == [] # no launch +def test_plan_repair_nonactive_unready_is_noop_with_pointer( + runner, isolated_plans_dir, tmp_path, monkeypatch +) -> None: + """Council review 6, F8 (deferred pin): a plan that is not active is not a + husk — nothing refuses its writes — so ``plan repair`` on an unready draft + has nothing to unblock. It exits 0, says why, points at ``plan architect`` + to finish the plan, prints the readiness so the learner sees what is + missing, launches nothing and writes nothing. The other branch — active + and unready — is the launch; this pin keeps the two from being confused.""" + from contextlib import ExitStack + + runner.invoke(cli, ["plan", "new", "--title", "Vague Draft"]) # draft, unready + assert store.load_plan("vague-draft").status == "draft" + assert not ReadinessView.from_plan(store.load_plan("vague-draft")).ready + before = _documents(isolated_plans_dir) + + calls: list = [] + with ExitStack() as stack: + for p in _launch_patches(tmp_path, {}, calls): + stack.enter_context(p) + monkeypatch.setenv("TMUX", "/tmp/tmux") + result = runner.invoke(cli, ["plan", "repair", "vague-draft"]) + + assert result.exit_code == 0, result.output + clean = _ANSI.sub("", result.output) + assert "'vague-draft' is draft, so nothing blocks it" in clean + assert "studyloop plan architect" in clean + assert "Nothing to repair" not in clean # that sentence is the ready plan's, not this one's + assert calls == [] # no launch + assert _documents(isolated_plans_dir) == before # no write + assert store.load_plan("vague-draft").status == "draft" + + def test_plan_repair_unknown_id_is_the_seams_not_found(runner) -> None: result = runner.invoke(cli, ["plan", "repair", "nope"]) diff --git a/packages/studyloop/tests/test_plan_application_mutations.py b/packages/studyloop/tests/test_plan_application_mutations.py index 2e4db03b5..3f6d35e55 100644 --- a/packages/studyloop/tests/test_plan_application_mutations.py +++ b/packages/studyloop/tests/test_plan_application_mutations.py @@ -282,6 +282,30 @@ def test_duplicate_record_beside_a_field_change_saves_once( assert len(detail.learning_records) == 1 +def test_duplicate_learning_record_with_mission_revision_still_saves_once( + app: PlanApplication, monkeypatch +) -> None: + """Council review 6, F8 (deferred pin): item 3b made the mission revisable + through ``RevisePlan``. The duplicate-record short-circuit must see a + mission edit as a change — ``not mission_updates`` is its own clause — or + a wind-down that re-sends yesterday's record beside a sharpened ``why`` + would drop the ``why`` and report "already recorded (no change)". One + save, the mission applied, the record still single.""" + _plan("demo") + spec = LearningRecordSpec(title="Once", body="only") + app.apply(RevisePlan(plan_id="demo", learning_record=spec)) + saves = _count_saves(monkeypatch) + + detail = app.apply( + RevisePlan(plan_id="demo", learning_record=spec, why="Own the nightly pipeline unaided") + ) + + assert len(saves) == 1, "a duplicate record beside a mission edit is one write, not zero" + assert detail.mission.why == "Own the nightly pipeline unaided" + assert len(detail.learning_records) == 1 + assert store.load_plan("demo").mission.why == "Own the nightly pipeline unaided" + + def test_empty_revision_is_still_a_touch(app: PlanApplication, monkeypatch) -> None: """The Phase-1 contract stands: an empty PATCH body has always been a save that bumps ``updated``. Only a duplicate-record-only revision is exempt.""" diff --git a/packages/studyloop/tests/test_verify_plan_integration_script.py b/packages/studyloop/tests/test_verify_plan_integration_script.py index 5509059da..6e8b7ab71 100644 --- a/packages/studyloop/tests/test_verify_plan_integration_script.py +++ b/packages/studyloop/tests/test_verify_plan_integration_script.py @@ -63,7 +63,7 @@ def script(): "inventory-in-process", "plan-suites", "docs-contract", - "protected-files-3a4f6b01", + "protected-files-early-base", "protected-files-late-base", "rg-plan-application-cli", "rg-plan-application-web-routes", @@ -149,20 +149,48 @@ def test_zero_hit_rg_invariants_expect_exit_one(self, script) -> None: def test_protected_file_checks_name_the_ten_files_against_their_bases(self, script) -> None: by_name = {check.name: check for check in script.build_checks(REPO_ROOT)} - early = by_name["protected-files-3a4f6b01"].command + early = by_name["protected-files-early-base"].command late = by_name["protected-files-late-base"].command assert not callable(early) and not callable(late) - assert "3a4f6b01" in early and script.PROTECTED_LATE_BASE in late - # The late base is a moving pin by design: it advances only when a + assert script.PROTECTED_EARLY_BASE in early and script.PROTECTED_LATE_BASE in late + # Both bases are moving pins by design: a base advances only when a # protected file legitimately changes and the diff has been read - # (recorded next to the constant). It must never regress to the seam base. - assert script.PROTECTED_LATE_BASE != "3a4f6b01" + # (recorded next to the constant), and the check is named by role so + # the name never has to move with it. The late base must never regress + # to the early one. + assert script.PROTECTED_LATE_BASE != script.PROTECTED_EARLY_BASE early_files = [part for part in early if part.endswith(".py")] late_files = [part for part in late if part.endswith(".py")] assert len(early_files) == 3 and len(late_files) == 7 for rel in (*early_files, *late_files): assert (REPO_ROOT / rel).exists(), rel + def test_protected_file_bases_are_reachable_from_main(self, script) -> None: + """A base that exists only as an unreachable object in one clone makes the + check fail on every fresh checkout (2026-09-18: ``3a4f6b01`` survived the + history consolidation as a dangling object here and nowhere else). Both + bases must be ancestors of the local ``main`` -- the property a rewritten + SHA loses. + + Judged only where it can be: CI's ``actions/checkout`` is a depth-1 + clone of the PR ref with no ``main``, where neither base is even an + object (run 35380095596 failed the 3.12 and 3.13 matrix on exactly + this). There the test skips and says why, rather than reporting a + failure the checkout cannot distinguish from the real one.""" + import subprocess + + def git(*args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run(["git", *args], cwd=REPO_ROOT, capture_output=True, text=True) + + if git("rev-parse", "--is-shallow-repository").stdout.strip() == "true": + pytest.skip("shallow checkout: the bases' reachability cannot be judged here") + if git("rev-parse", "--verify", "-q", "main^{commit}").returncode != 0: + pytest.skip("no local `main` ref: the bases' reachability cannot be judged here") + + for base in (script.PROTECTED_EARLY_BASE, script.PROTECTED_LATE_BASE): + result = git("merge-base", "--is-ancestor", base, "main") + assert result.returncode == 0, f"{base} is not an ancestor of main: {result.stderr}" + def test_architect_grants_check_derives_the_ten_names_from_the_inventory(self, script) -> None: """Design §6 (follow-on item 1, D-A): the Kiro and Claude architect definitions carry exactly the nine plan tools + ``record_plan_learning`` diff --git a/scripts/maintenance/stage9-owner-remote-cleanup.sh b/scripts/maintenance/stage9-owner-remote-cleanup.sh index 35a0c8c36..5224a6173 100755 --- a/scripts/maintenance/stage9-owner-remote-cleanup.sh +++ b/scripts/maintenance/stage9-owner-remote-cleanup.sh @@ -1,7 +1,11 @@ #!/usr/bin/env bash # Stage 9 owner script — the remote half of the 2026-09-10 branch cleanup. -# Agents cannot push or delete remote refs on this repo (platform policy), so the -# owner runs this. Every step is idempotent, so re-running after a partial run is +# The owner runs this because it pushes main and asks before switching a +# repository ruleset off. (An earlier header said agents cannot delete remote +# refs by platform policy; that was wrong — the harness allows +# `git push origin --delete `, proven on 2026-09-17 and +# 2026-09-18. What refuses the deletion is the "Default" ruleset below, for +# everyone.) Every step is idempotent, so re-running after a partial run is # safe: already-pushed refs report "Everything up-to-date". # # First run (2026-09-10 23:47): steps 1-2 succeeded; step 3 was rejected for all diff --git a/scripts/verify/plan_integration.py b/scripts/verify/plan_integration.py index aff7940bd..7de99fc30 100644 --- a/scripts/verify/plan_integration.py +++ b/scripts/verify/plan_integration.py @@ -67,7 +67,13 @@ CORE_TOOLS = frozenset({"list_courses", "get_study_backlog", "end_session"}) #: Protected test files: byte-identical to their base since the programme began. -PROTECTED_EARLY_BASE = "3a4f6b01" +# Moved 3a4f6b01 -> d7f568bf on 2026-09-18: the history consolidation rewrote +# the programme's early commits, and 3a4f6b01 survived only as an unreachable +# object in one clone (`git fetch origin 3a4f6b01` finds no such ref), so the +# check would fail on any fresh checkout. d7f568bf is the same commit ("RED -- +# pin the two plan bugs issue #7 named as must-fix-first") on main; the three +# protected files are byte-identical between the two (git diff --stat empty). +PROTECTED_EARLY_BASE = "d7f568bf" PROTECTED_EARLY = ( "packages/studyloop/tests/test_web_plans.py", "packages/studyloop/tests/test_cli_plan.py", @@ -354,7 +360,7 @@ def build_checks(repo_root: Path) -> list[Check]: ), # --- protected files: byte-identical to their bases ------------------- Check( - "protected-files-3a4f6b01", + "protected-files-early-base", ["git", "diff", "--quiet", PROTECTED_EARLY_BASE, "--", *PROTECTED_EARLY], ), Check( diff --git a/uv.lock b/uv.lock index 56d185806..698eef577 100644 --- a/uv.lock +++ b/uv.lock @@ -148,15 +148,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -600,7 +600,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, @@ -631,43 +631,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufft = [ - { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufile = [ - { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] curand = [ - { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] [[package]] @@ -1642,7 +1642,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -1681,7 +1681,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -1693,7 +1693,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -1723,9 +1723,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -1737,7 +1737,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -2871,8 +2871,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "sys_platform != 'win32'" }, + { name = "jeepney", marker = "sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -3412,11 +3412,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]]