From 8805bed263b9d96b7b61e81cd944d31a80f7795e Mon Sep 17 00:00:00 2001 From: Jean-Sylvain Boige Date: Fri, 11 Sep 2026 21:00:43 +0200 Subject: [PATCH 01/10] fix(guard,#15578): le detecteur de collisions se tait quand la substance passe sur main (#15591) Le pool de candidats etait `--state open` exclusivement : une collision DISPARAISSAIT du rapport a l'instant precis ou elle devenait irreversible (un cote merge). L'organe etait donc le plus bruyant tant que le risque etait theorique, et muet une fois la substance sur `main`. Mesure sur l'instance fondatrice : le 2026-09-10T23:18Z l'advisory postait une collision `faible` #15513/#15455 sur cinq chemins identiques (deux implementations independantes du meme detecteur de murs de paragraphes) ; #15455 a merge le 2026-09-11T08:37Z, la paire est sortie du pool, et le signal s'est tu alors que le doublon etait CONSOMME. - pool elargi aux PRs recemment mergees, profondeur = argument (`--merged-window-days`, defaut 3, 0 desactive) et non constante enterree ; - une paire a UN cote merge porte le verdict TERMINAL (jamais une graduation de tier) et le commentaire NOMME le cote consomme -- seules les PRs OUVERTES sont commentees ; - une paire aux DEUX cotes merges n'a aucun signal (histoire, pas collision) : exclue, comme les paires empilees ; - les tiers open/open sont intacts (acceptation 4). Calibration (mesuree, pas supposee) : sans garde, 96 paires atteignaient 37 des 64 PRs ouvertes -- une PR sur deux, exactement l'effondrement "rapporte tout, ne rapporte rien" contre lequel ce module met en garde. La garde `min(recouvrement des DEUX cotes) >= 0.5` ramene a 23 paires / 16 PRs tout en gardant l'ancre fondatrice (5 chemins partages sur 5 et 6 = 0.83). Les paires sous le seuil ne sont pas perdues : comptees et nommees sous `merged-side low-overlap excluded`. Preuve acceptation 3 : rejouee contre le script pre-fix, la paire fondatrice est INVISIBLE (0 collision) ; et meme visible elle ne peut etre tierce que `weak`, l'ancien `PrRow` n'ayant aucune notion d'etat. Self-test 14/14, suite unitaire 65 passed. See #15578 Co-authored-by: Claude Sonnet 5 --- .../workflows/pr-path-collision-advisory.yml | 20 +- scripts/check_pr_path_collisions.py | 424 +++++++++++++++++- .../tests/test_check_pr_path_collisions.py | 252 ++++++++++- 3 files changed, 672 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pr-path-collision-advisory.yml b/.github/workflows/pr-path-collision-advisory.yml index a7ae62a4f4..f666f58359 100644 --- a/.github/workflows/pr-path-collision-advisory.yml +++ b/.github/workflows/pr-path-collision-advisory.yml @@ -42,6 +42,24 @@ name: PR path-collision advisory # is absent from the required set and from the PR gate roster. Its own run, # however, goes red when writes are lost (#14236, --fail-on-write-loss) -- # a muted organ has no other way to say that it is mute. +# +# Candidate pool widened to recently-MERGED PRs (#15578). The pool was +# `--state open` only, so a collision vanished from the report at the exact +# moment it became irreversible. Measured on the founding instance: at +# 2026-09-10T23:18Z the advisory posted a `faible` collision between #15513 +# and #15455 over five identical paths; #15455 merged at 2026-09-11T08:37Z and +# the pair left the pool, though the duplicate was by then consumed. A pair +# with one merged side now carries the TERMINAL verdict (never a tier: the +# substance is on `main`), all-merged pairs stay excluded, and open/open tiers +# are untouched. Depth is `--merged-window-days` (default 3, 0 disables). +# +# The merged side must also OVERLAP substantially (>= TERMINAL_MIN_OVERLAP_RATIO +# of BOTH sides' signal paths, 0.5): measured on the 2026-09-11 snapshot, an +# ungated rule reached 37 of the 64 open PRs -- every other open PR, the +# "reports everything reports nothing" collapse -- while the gate cuts it to 16 +# and still keeps the founding duplicate (5 shared out of 5 and of 6 = 0.83). +# A sub-threshold pair is not silently dropped: it is counted and named under +# `merged-side low-overlap excluded` in the run summary. on: # Off-round minutes: the top of the hour is when every cron in the account @@ -56,7 +74,7 @@ on: type: boolean default: true same_issue_only: - description: 'Post only strong-tier collisions (common cited issue)' + description: 'Post only the actionable tiers: strong (common cited issue) or terminal (other side already merged)' required: false type: boolean default: false diff --git a/scripts/check_pr_path_collisions.py b/scripts/check_pr_path_collisions.py index b491b88455..a595a5d370 100644 --- a/scripts/check_pr_path_collisions.py +++ b/scripts/check_pr_path_collisions.py @@ -55,6 +55,35 @@ Strong pairs are additionally labelled ``pr-overlap`` (label on both sides), giving the coordinator a filterable queue. Weak pairs get the comment only. +Terminal verdict: a merged side (#15578) +---------------------------------------- +The candidate pool was ``--state open`` exclusively, so a collision *vanished +from the report at the exact moment it became irreversible*: when one side +merged. The organ was loudest while the risk was theoretical, and mute once +the substance was on ``main``. + +Measured on the founding instance: at 2026-09-10T23:18Z the advisory posted a +``faible`` collision between #15513 and #15455 over five identical paths -- +two independent implementations of the same paragraph-length detector. #15455 +merged at 2026-09-11T08:37Z, the pair left the pool, and the signal went +silent although the duplicate was by then CONSUMED: the lane found it by +itself and escalated without delivering. + +The pool therefore also carries PRs merged within a bounded window +(``--merged-window-days`` -- an argument, never a buried constant): + +- a pair with ONE merged side is **terminal**. It is not graduated and not + tiered: the substance is already on ``main``, and the comment says exactly + that. Terminal *replaces the silence*, it does not inflate the open tiers; +- a pair with BOTH sides merged carries no signal (both are already on + ``main``: history, not a collision) and is excluded, like stacked pairs; +- the tier of OPEN/OPEN pairs is untouched (#15578 acceptance 4). The lesson + is not "everything is strong": it is that the state of the other side + dominates the path overlap. + +Only OPEN PRs are ever commented. An advisory written on a merged PR would be +noise on a closed thread, and the actionable side is always the open one. + Scope (from #13359) ------------------- - Invent NO new claim lock. Do not touch `check_lane_claim.py` -- the claim @@ -90,11 +119,15 @@ ``--limit`` cap PRs fetched via ``gh pr list`` (default 500; the default ``gh pr list`` limit is 30 -- a silent cap that hid entire stacks this month). 500 covers the whole open pool here. +``--merged-window-days`` + depth of the MERGED pool, in days (default 3; 0 disables). + The bound is an argument, never a buried constant (#15578). ``--repo`` override repo (default: gh default / GITHUB_REPOSITORY). ``--dry-run`` log detections, post/retract/label nothing. ``--same-issue-only`` - post only STRONG-tier collisions (both PRs cite a common - issue). Cuts the shared-README noise. + post only the actionable tiers: STRONG (both PRs cite a + common issue) and TERMINAL (the other side already merged). + Cuts the shared-README noise. --json always on stdout. --self-test run the offline control suite; exit 2 if any control fails. """ @@ -103,7 +136,7 @@ import argparse import json -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import os import re import subprocess @@ -127,6 +160,34 @@ TIER_STRONG = "strong" TIER_WEAK = "weak" +# A pair with a merged side is not a tier, it is a terminal state (#15578). +TIER_TERMINAL = "terminal" + +# ``gh`` reports PR state as OPEN / MERGED / CLOSED; only "merged" is terminal. +MERGED_STATE = "merged" + +# Default depth of the merged window. The useful window is the one in which a +# still-open branch could have been opened before the merge and can still be +# unaware of it -- a few days, not a quarter. Overridden by the CLI argument +# ``--merged-window-days`` (the bound is an argument, not a buried constant). +DEFAULT_MERGED_WINDOW_DAYS = 3 + +# A merged side is TERMINAL only when the two deliveries actually overlap -- +# the "full double delivery" of #15578, never a coincidental shared +# ``.gitignore``. The measure is the share of EACH side's signal paths that is +# covered by the shared ones: BOTH sides must be substantially covered, which +# is what separates "the same substance delivered twice" from "a long-lived PR +# that merely neighbours a small merge". +# +# Calibrated on ONE live snapshot (2026-09-11, 64 open + 282 merged, 3-day +# window; the pool drifts, so these are that snapshot's numbers): ungated, 96 +# merged-side pairs reached 37 of the 64 open PRs -- every other open PR, i.e. +# the "reports everything" collapse this module's docstring warns about. At 0.5 +# the same snapshot yields 23 pairs / 16 open PRs, and the founding +# #15513/#15455 duplicate scores 0.83 (5 shared, out of 5 and of 6). +# Rejected alternative, measured: ``shared / min(sizes)`` keeps the same ~37 -- +# a 1-file merged PR sharing its only file scores 1.00 for trivial reasons. +TERMINAL_MIN_OVERLAP_RATIO = 0.5 # The synthetic positive control. NOT a live repo pair (those are volatile: # they merge and vanish). It is seeded straight into the pure detector, so the @@ -149,6 +210,28 @@ 13313), ) +# The founding instance of #15578, replayed at its state of then: #15455 merged +# at 2026-09-11T08:37Z while #15513 was still open, over the same five paths -- +# two independent implementations of the same paragraph-length detector. The +# paths are READ FROM THE TWO PRS (``gh pr view --json files``), not invented. +# Under the pre-#15578 pool (``--state open`` only) the pair is invisible, and +# that invisibility IS the defect. +FOUNDING_TERMINAL_PAIR: tuple[int, int, tuple[str, ...], str] = ( + 15513, 15455, + ( + ".github/workflows/paragraph-length-advisory.yml", + "scripts/notebook_tools/README.md", + "scripts/notebook_tools/detect_paragraph_length.py", + "scripts/notebook_tools/tests/fixtures/paragraph_wall_md.md", + "scripts/notebook_tools/tests/test_detect_paragraph_length.py", + ), + # #15455's sixth path, unshared -- the real overlap is 5 of 5 and 5 of 6, + # i.e. 0.83, NOT a trivial 1.00. A fixture that scores a perfect ratio + # would prove the gate admits the founding case for a reason the live pool + # never offers. + "scripts/ci/check_self_hosted_runner_policy.py", +) + # Generated artifacts with permanent structural overlap and zero signal # (#13615: a guard that reports everything reports nothing). EXCLUDED_EXACT_PATHS = frozenset({ @@ -177,12 +260,21 @@ def _is_signal_path(path: str) -> bool: @dataclass(frozen=True) class PathCollision: - """An unordered pair of open PRs sharing at least one signal file path.""" + """An unordered pair of PRs sharing at least one signal file path. + + ``merged_side`` is set only for a terminal pair (#15578): it names the side + already on ``main``, so the comment can say which number is consumed + instead of guessing from the tier. ``overlap_ratio`` carries the measured + ``min`` coverage that put the pair over ``TERMINAL_MIN_OVERLAP_RATIO``, so + a reader can audit the gate's decision instead of trusting it. + """ a_number: int b_number: int shared_paths: tuple[str, ...] tier: str = TIER_WEAK common_issues: tuple[int, ...] = () + merged_side: int | None = None + overlap_ratio: float | None = None def other(self, number: int) -> int: if number == self.a_number: @@ -198,18 +290,31 @@ def as_dict(self) -> dict: "shared_paths": list(self.shared_paths), "tier": self.tier, "common_issues": list(self.common_issues), + "merged_side": self.merged_side, + "overlap_ratio": self.overlap_ratio, } @dataclass(frozen=True) class PrRow: - """Minimal open PR record the detector needs.""" + """Minimal PR record the detector needs. + + ``state`` defaults to ``"open"``: every pre-#15578 construction -- and + every fixture in the unit suite -- is an open PR, so widening the pool + cannot silently move an existing pair into the terminal verdict. + """ number: int title: str paths: tuple[str, ...] body: str = "" base_ref: str = "" head_ref: str = "" + state: str = "open" + merged_at: str = "" + + @property + def is_merged(self) -> bool: + return self.state.strip().lower() == MERGED_STATE @classmethod def from_gh_dict(cls, d: dict) -> "PrRow": @@ -225,6 +330,8 @@ def from_gh_dict(cls, d: dict) -> "PrRow": body=(d.get("body") or ""), base_ref=(d.get("baseRefName") or "").strip(), head_ref=(d.get("headRefName") or "").strip(), + state=(d.get("state") or "open").strip().lower(), + merged_at=(d.get("mergedAt") or "").strip(), ) def cited_issues(self) -> set[int]: @@ -234,6 +341,25 @@ def cited_issues(self) -> set[int]: return from_title | {int(n) for n in _CITES_RE.findall(self.body or "")} +def terminal_overlap_ratio( + shared_paths: Iterable[str], a: PrRow, b: PrRow, +) -> float: + """Share of EACH side's signal paths covered by the shared ones (min). + + ``min`` over the two sides: a large PR overlapping a small one on a single + file scores low, because that is not a duplicate delivery. Returns 0.0 when + either side carries no signal path -- an empty denominator must never read + as a perfect overlap. + """ + shared = set(shared_paths) + a_sig = {p for p in a.paths if _is_signal_path(p)} + b_sig = {p for p in b.paths if _is_signal_path(p)} + if not a_sig or not b_sig: + return 0.0 + return min(len(shared & a_sig) / len(a_sig), + len(shared & b_sig) / len(b_sig)) + + def is_stacked(a: PrRow, b: PrRow) -> bool: """True when one PR is stacked directly on the other's head. @@ -250,10 +376,13 @@ def is_stacked(a: PrRow, b: PrRow) -> bool: class ScanResult: """Aggregate of one detection run.""" total_prs_scanned: int = 0 + merged_prs_scanned: int = 0 distinct_paths: int = 0 collisions: list[PathCollision] = field(default_factory=list) colliding_prs: list[int] = field(default_factory=list) stacked_pairs_excluded: list[tuple[int, int]] = field(default_factory=list) + merged_pairs_excluded: list[tuple[int, int]] = field(default_factory=list) + merged_low_overlap_excluded: list[tuple[int, int]] = field(default_factory=list) @property def n_collisions(self) -> int: @@ -263,30 +392,64 @@ def n_collisions(self) -> int: def strong_collisions(self) -> list[PathCollision]: return [c for c in self.collisions if c.tier == TIER_STRONG] + @property + def terminal_collisions(self) -> list[PathCollision]: + """Pairs whose substance is already on ``main`` (#15578).""" + return [c for c in self.collisions if c.tier == TIER_TERMINAL] + + def actionable_collisions(self) -> list[PathCollision]: + """Pairs worth posting when the caller wants the noise cut. + + STRONG and TERMINAL, never the open tiers' weak family-README noise. + A terminal pair is by definition not that noise: the substance is + already merged, which is the strongest thing this organ can observe. + """ + return [c for c in self.collisions if c.tier in (TIER_STRONG, TIER_TERMINAL)] + def as_dict(self) -> dict: return { "total_prs_scanned": self.total_prs_scanned, + "merged_prs_scanned": self.merged_prs_scanned, "distinct_paths": self.distinct_paths, "n_collisions": self.n_collisions, + "n_strong": len(self.strong_collisions), + "n_weak": sum(1 for c in self.collisions if c.tier == TIER_WEAK), + "n_terminal": len(self.terminal_collisions), "collisions": [c.as_dict() for c in self.collisions], "colliding_prs": self.colliding_prs, "stacked_pairs_excluded": [list(p) for p in self.stacked_pairs_excluded], + "merged_pairs_excluded": [list(p) for p in self.merged_pairs_excluded], + "merged_low_overlap_excluded": [ + list(p) for p in self.merged_low_overlap_excluded + ], } def detect_path_collisions(prs: Iterable[PrRow]) -> ScanResult: - """Group open PRs by signal file path; report every pair sharing >=1 path. + """Group the pool by signal file path; report every pair sharing >=1 path. ``prs`` is consumed once. The result is deterministic: collisions sorted by (lowest number, highest number), shared paths sorted by path string. - Excluded before pairing (#13615): generated-artifact paths (dropped from - every row -- they never carry signal), and stacked PR pairs (dropped from - the pair set -- their overlap is the stack itself). + Excluded before pairing: + - generated-artifact paths (#13615): dropped from every row -- they never + carry signal; + - stacked PR pairs (#13615): dropped from the pair set -- their overlap is + the stack itself; + - all-merged pairs (#15578): dropped from the pair set -- both sides are + already on ``main``, which is history, not a collision; + - merged-side pairs below ``TERMINAL_MIN_OVERLAP_RATIO`` (#15578): dropped, + and recorded in ``merged_low_overlap_excluded`` -- an incidental shared + ``.gitignore`` is not a double delivery. + + A pair with EXACTLY ONE merged side and a substantial overlap is reported + with the terminal verdict and carries ``merged_side`` + ``overlap_ratio``; + the open/open tiering is untouched. """ result = ScanResult() rows = list(prs) result.total_prs_scanned = len(rows) + result.merged_prs_scanned = sum(1 for r in rows if r.is_merged) row_by_number = {r.number: r for r in rows} path_to_numbers: dict[str, set[int]] = {} @@ -316,15 +479,34 @@ def detect_path_collisions(prs: Iterable[PrRow]) -> ScanResult: if ra is not None and rb is not None and is_stacked(ra, rb): result.stacked_pairs_excluded.append((a, b)) continue + merged_sides = [ + r.number for r in (ra, rb) if r is not None and r.is_merged + ] + if len(merged_sides) == 2: + result.merged_pairs_excluded.append((a, b)) + continue common: tuple[int, ...] = () if ra is not None and rb is not None: common = tuple(sorted(ra.cited_issues() & rb.cited_issues())) + if merged_sides: + ratio = ( + terminal_overlap_ratio(paths, ra, rb) + if ra is not None and rb is not None else 0.0 + ) + if ratio < TERMINAL_MIN_OVERLAP_RATIO: + result.merged_low_overlap_excluded.append((a, b)) + continue + tier, merged_side = TIER_TERMINAL, merged_sides[0] + else: + tier, merged_side, ratio = (TIER_STRONG if common else TIER_WEAK), None, None collisions.append(PathCollision( a_number=a, b_number=b, shared_paths=tuple(sorted(paths)), - tier=TIER_STRONG if common else TIER_WEAK, + tier=tier, common_issues=common, + merged_side=merged_side, + overlap_ratio=ratio, )) result.collisions = collisions result.colliding_prs = sorted( @@ -354,6 +536,11 @@ def render_comment(number: int, title: str, own_collisions: list[PathCollision]) Names each colliding PR by number, its tier, the shared paths, and (for the strong tier) the common cited issues. Marker-framed so a re-run can find, refresh, or retract it in place. + + The open-pair rendering is byte-identical to the pre-#15578 one, so the + widened pool does not churn a single existing comment: only a pair that + actually went terminal changes its body (and the closing note is appended + only when such a pair is present). """ lines = [ "", @@ -369,12 +556,34 @@ def render_comment(number: int, title: str, own_collisions: list[PathCollision]) ] for c in sorted(own_collisions, key=lambda c: c.other(number)): other = c.other(number) - tier_fr = "fort" if c.tier == TIER_STRONG else "faible" - line = f"- **{tier_fr}** -- **#{other}** partage : {', '.join(c.shared_paths)}" + if c.tier == TIER_TERMINAL: + consumed = c.merged_side if c.merged_side is not None else other + ratio = ( + f" (recouvrement {c.overlap_ratio:.0%})" + if c.overlap_ratio is not None else "" + ) + line = ( + f"- **terminal** -- **#{other}** partage : " + f"{', '.join(c.shared_paths)}{ratio} -- **#{consumed}** est " + "deja sur `main`, la substance est consommee." + ) + else: + tier_fr = "fort" if c.tier == TIER_STRONG else "faible" + line = ( + f"- **{tier_fr}** -- **#{other}** partage : " + f"{', '.join(c.shared_paths)}" + ) if c.common_issues: issues = ", ".join(f"#{i}" for i in c.common_issues) line += f" (issues communes : {issues})" lines.append(line) + if any(c.tier == TIER_TERMINAL for c in own_collisions): + lines.append("") + lines.append( + "Le verdict **terminal** (#15578) signifie que la substance est " + "deja sur `main` : le cote merge n'est plus une collision a " + "arbitrer, c'est du travail deja integre." + ) lines.append("") lines.append("") return "\n".join(lines) @@ -524,6 +733,40 @@ def list_open_prs(repo: str, limit: int) -> list[PrRow]: return [PrRow.from_gh_dict(d) for d in raw] +def list_recently_merged_prs( + repo: str, since_date: str, limit: int, +) -> list[PrRow]: + """PRs merged on or after ``since_date`` (``YYYY-MM-DD``), with paths. + + The window is bounded SERVER-side (``merged:>=``) because ``gh pr list`` + orders by CREATION date: a purely client-side window over a truncated page + would systematically lose the long-lived branches -- exactly the ones that + collide most. The client-side re-filter is kept as a cheap invariant (the + search qualifier and the ``mergedAt`` field must agree; a divergence would + make the pool silently wrong) and it is what makes the function testable + without the network. + + A failure is NOT swallowed here. The caller decides -- an advisory organ + that goes mute on a query quirk is the very defect #15578 reports. + """ + raw = _gh_json([ + "pr", "list", "--repo", repo, "--state", "merged", + "--search", f"merged:>={since_date}", + "--limit", str(limit), + "--json", + "number,title,body,baseRefName,headRefName,files,state,mergedAt", + ]) or [] + rows: list[PrRow] = [] + for d in raw: + row = PrRow.from_gh_dict(d) + if row.state != MERGED_STATE: + continue + if row.merged_at and row.merged_at[:10] < since_date: + continue + rows.append(row) + return rows + + def find_marker(repo: str, number: int) -> tuple[str, str] | None: """(id, body) of the marker comment on PR ``number``, or None. @@ -702,14 +945,25 @@ def label_strong_pairs( print(f" label {OVERLAP_LABEL} -> #{number}", file=sys.stderr) +def _fmt_pairs(pairs: list[tuple[int, int]], limit: int = 8) -> str: + """Render exclusion pairs, capped -- a 176-pair line drowns the summary.""" + shown = ", ".join(f"#{a}/#{b}" for a, b in pairs[:limit]) + if len(pairs) > limit: + return f"({len(pairs)}) {shown}, ..." + return shown + + def _format_human_summary(result: ScanResult) -> str: lines = [] + n_weak = sum(1 for c in result.collisions if c.tier == TIER_WEAK) lines.append( f"scanned={result.total_prs_scanned} " + f"(merged={result.merged_prs_scanned}) " f"distinct_paths={result.distinct_paths} " f"collisions={result.n_collisions} " f"(strong={len(result.strong_collisions)} " - f"weak={result.n_collisions - len(result.strong_collisions)})" + f"weak={n_weak} " + f"terminal={len(result.terminal_collisions)})" ) for c in result.collisions[:20]: lines.append( @@ -721,8 +975,20 @@ def _format_human_summary(result: ScanResult) -> str: if result.n_collisions > 20: lines.append(f" ... and {result.n_collisions - 20} more (see JSON)") if result.stacked_pairs_excluded: - pairs = ", ".join(f"#{a}/#{b}" for a, b in result.stacked_pairs_excluded) - lines.append(f" stacked excluded: {pairs}") + lines.append( + f" stacked excluded: {_fmt_pairs(result.stacked_pairs_excluded)}" + ) + if result.merged_pairs_excluded: + lines.append( + f" both-merged excluded: " + f"{_fmt_pairs(result.merged_pairs_excluded)}" + ) + if result.merged_low_overlap_excluded: + lines.append( + f" merged-side low-overlap excluded " + f"(< {TERMINAL_MIN_OVERLAP_RATIO:.0%}): " + f"{_fmt_pairs(result.merged_low_overlap_excluded)}" + ) return "\n".join(lines) @@ -771,6 +1037,83 @@ def check(name: str, ok: bool) -> None: and issue in coll.common_issues and path in coll.shared_paths) + # Founding instance of #15578 (#15578 acceptance 3): one side merged -> + # TERMINAL and the merged side is NAMED. Reverting the fix reds this + # control -- an open-only pool does not even carry the merged row, so the + # pair disappears (measured: see the PR body's replay of the suite against + # the pre-fix script). + f_open, f_merged, f_paths, f_extra = FOUNDING_TERMINAL_PAIR + founding = detect_path_collisions([ + PrRow(number=f_open, title="tooling(notebook): detecteur", paths=f_paths), + PrRow(number=f_merged, title="tooling(#15405): detecteur", + paths=f_paths + (f_extra,), + state=MERGED_STATE, merged_at="2026-09-11T08:37:34Z"), + ]) + term = next((c for c in founding.collisions + if {c.a_number, c.b_number} == {f_open, f_merged}), None) + check(f"founding #{f_open}/#{f_merged} flagged terminal, merged side named", + term is not None + and term.tier == TIER_TERMINAL + and term.merged_side == f_merged + and len(term.shared_paths) == len(f_paths)) + + # Calibration control: the gate must ADMIT the founding case, and it must + # do so at its REAL ratio (5 of 5 and 5 of 6 = 0.83), not a fixture-perfect + # 1.00. If someone raises TERMINAL_MIN_OVERLAP_RATIO past the measured + # duplicate, this reds -- which is the point. + check(f"founding pair clears the gate at its measured ratio " + f"(>= {TERMINAL_MIN_OVERLAP_RATIO:.0%})", + term is not None + and term.overlap_ratio is not None + and abs(term.overlap_ratio - 5 / 6) < 1e-9 + and len(f_extra) > 0) + + # Calibration negative: an incidental shared path is NOT a double + # delivery. A 12-file PR sharing one `.gitignore` with a 1-file merged PR + # is noise, and posting it is how the organ becomes unreadable. + low = detect_path_collisions([ + PrRow(number=900070, title="feat: big", + paths=(".gitignore",) + tuple(f"big/{i}.py" for i in range(11))), + PrRow(number=900071, title="fix: small", paths=(".gitignore",), + state=MERGED_STATE), + ]) + check("incidental low-overlap merged pair is excluded, not reported", + low.n_collisions == 0 + and (900070, 900071) in low.merged_low_overlap_excluded) + + # ... and a terminal pair is NOT dropped by the --same-issue-only filter: + # the substance being already merged is the loudest thing this organ sees. + check("terminal survives the actionable (same-issue-only) selector", + [c.tier for c in founding.actionable_collisions()] == [TIER_TERMINAL]) + + # Negative: TWO merged sides are history, not a collision -> NOTHING. + both_merged = detect_path_collisions([ + PrRow(number=900050, title="old a", paths=("gone/x.md",), + state=MERGED_STATE), + PrRow(number=900051, title="old b", paths=("gone/x.md",), + state=MERGED_STATE), + ]) + check("both-merged pair produces no collision", + both_merged.n_collisions == 0 + and (900050, 900051) in both_merged.merged_pairs_excluded) + + # Acceptance 4: a merged row must not inflate (nor deflate) an unrelated + # OPEN/OPEN pair -- the open tiers keep their own behaviour. + mixed = detect_path_collisions([ + PrRow(number=900060, title="fix(#500): a", paths=("open/y.md",)), + PrRow(number=900061, title="fix(#500): b", paths=("open/y.md",)), + PrRow(number=900062, title="merged z", paths=("gone/z.md",), + state=MERGED_STATE), + ]) + check("open pair keeps its strong tier beside a merged row", + any(c.tier == TIER_STRONG + and {c.a_number, c.b_number} == {900060, 900061} + for c in mixed.collisions)) + + # Acceptance 4 (negative half): a merged row sharing NO path adds nothing. + check("merged row without a shared path is not reported", + mixed.n_collisions == 1) + # Negative: stacked pair (base of one = head of the other) -> NOTHING. stacked = detect_path_collisions([ PrRow(number=900010, title="base tranche", paths=("stacked/x.md",), @@ -847,6 +1190,17 @@ def _cli(argv: list[str] | None = None) -> int: "(#14236), and a green run reads as `nothing to report`." ), ) + ap.add_argument( + "--merged-window-days", type=int, + default=DEFAULT_MERGED_WINDOW_DAYS, + help=( + "depth of the MERGED pool, in days (default " + f"{DEFAULT_MERGED_WINDOW_DAYS}). A pair whose other side merged " + "inside the window is reported with the terminal verdict instead " + "of vanishing (#15578). 0 disables the merged pool entirely, " + "restoring the pre-#15578 behaviour." + ), + ) ap.add_argument("--self-test", action="store_true", help="run the offline control suite; exit 2 if any fails") args = ap.parse_args(argv) @@ -856,15 +1210,38 @@ def _cli(argv: list[str] | None = None) -> int: repo = args.repo or _repo_default() try: - prs = list_open_prs(repo, args.limit) + open_prs = list_open_prs(repo, args.limit) except RuntimeError as e: print(str(e), file=sys.stderr) return 0 # advisory: a listing failure must not red the run + merged_prs: list[PrRow] = [] + if args.merged_window_days > 0: + since = ( + datetime.now(timezone.utc) - timedelta(days=args.merged_window_days) + ).strftime("%Y-%m-%d") + try: + merged_prs = list_recently_merged_prs(repo, since, args.limit) + except RuntimeError as e: + # Never silent. A lost merged pool returns the organ to the very + # muteness #15578 reports, and a green run would read as "nothing + # to report". The loss is logged here AND left visible in the JSON + # (merged_prs_scanned=0 over a window that cannot be empty). + print( + f"WARN: merged pool unavailable ({args.merged_window_days}d " + f"window, since {since}) -- falling back to open PRs only: {e}", + file=sys.stderr, + ) + + prs = open_prs + merged_prs + open_numbers = {p.number for p in open_prs} result = detect_path_collisions(prs) if args.same_issue_only and result.collisions: - result.collisions = strong_collisions(result.collisions) + # STRONG and TERMINAL (#15578): a terminal pair is not the family- + # README noise this flag exists to cut, it is the loudest signal the + # organ has -- the substance is already on main. + result.collisions = result.actionable_collisions() result.colliding_prs = sorted( {n for c in result.collisions for n in (c.a_number, c.b_number)} ) @@ -872,10 +1249,14 @@ def _cli(argv: list[str] | None = None) -> int: print(json.dumps(result.as_dict(), ensure_ascii=False, indent=2)) print(_format_human_summary(result), file=sys.stderr) - # Plan over the UNION colliding ∪ marker-carriers (#13489): reads only. - title_by_number = {p.number: p.title for p in prs} + # Plan over the UNION (colliding ∩ open) ∪ marker-carriers (#13489). + # Merged PRs are excluded from the write set (#15578): an advisory on a + # closed thread is noise, and the actionable side of a terminal pair is + # always the open one. The marker scan stays on the OPEN pool for the same + # reason -- no API call is spent on a PR that can never be written to. + title_by_number = {p.number: p.title for p in open_prs} marker_by_number, scan_failed = scan_markers( - repo, [p.number for p in prs] + repo, [p.number for p in open_prs] ) for number in sorted(scan_failed): print( @@ -885,7 +1266,8 @@ def _cli(argv: list[str] | None = None) -> int: ) plan = [ a for a in plan_actions( - set(result.colliding_prs), result.collisions, title_by_number, + sorted(set(result.colliding_prs) & open_numbers), + result.collisions, title_by_number, marker_by_number, datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%MZ"), ) diff --git a/scripts/tests/test_check_pr_path_collisions.py b/scripts/tests/test_check_pr_path_collisions.py index 8ce3dfc9a8..70f4829079 100644 --- a/scripts/tests/test_check_pr_path_collisions.py +++ b/scripts/tests/test_check_pr_path_collisions.py @@ -45,9 +45,11 @@ def _pr(number: int, paths: list[str], title: str = "", body: str = "", - base_ref: str = "", head_ref: str = "") -> PrRow: + base_ref: str = "", head_ref: str = "", state: str = "open", + merged_at: str = "") -> PrRow: return PrRow(number=number, title=title, paths=tuple(paths), body=body, - base_ref=base_ref, head_ref=head_ref) + base_ref=base_ref, head_ref=head_ref, state=state, + merged_at=merged_at) class TestDetectPathCollisions(unittest.TestCase): @@ -274,6 +276,252 @@ def test_historical_paths_exist_in_repo(self): ) +class TestTerminalMergedPairs(unittest.TestCase): + """#15578: the collision must NOT vanish when one side merges. + + The founding instance: #15455 merged at 2026-09-11T08:37Z while #15513 was + still open, over five identical paths. Under the pre-fix pool + (``--state open`` only) the pair was invisible -- so the organ was loudest + while the risk was theoretical and mute once the substance was on ``main``. + """ + + def test_founding_instance_is_terminal_not_silent(self): + """Acceptance 1+2: a merged side yields a TERMINAL verdict, named.""" + f_open, f_merged, f_paths, f_extra = _mod.FOUNDING_TERMINAL_PAIR + result = detect_path_collisions([ + _pr(f_open, list(f_paths), title="tooling(notebook): detecteur"), + _pr(f_merged, list(f_paths) + [f_extra], + title="tooling(#15405): detecteur", + state="merged", merged_at="2026-09-11T08:37:34Z"), + ]) + self.assertEqual(result.n_collisions, 1) + c = result.collisions[0] + # Deterministic pair order is (lowest, highest) -- #15455 < #15513. + self.assertEqual((c.a_number, c.b_number), (f_merged, f_open)) + self.assertEqual(c.tier, "terminal") + self.assertEqual(c.merged_side, f_merged) + self.assertEqual(len(c.shared_paths), len(f_paths)) + self.assertEqual(result.terminal_collisions, [c]) + + def test_terminal_is_distinct_from_the_open_tiers(self): + """The verdict is terminal, NOT a graduation of strong/weak.""" + _, f_merged, f_paths, _ = _mod.FOUNDING_TERMINAL_PAIR + result = detect_path_collisions([ + _pr(1, list(f_paths), title="fix(#42): same issue"), + _pr(f_merged, list(f_paths), title="fix(#42): same issue", + state="merged"), + ]) + c = result.collisions[0] + self.assertEqual(c.tier, "terminal") + self.assertNotEqual(c.tier, "strong") + # A terminal pair is NOT strong: it must not reach the label path, + # which would tag the merged side of a closed thread. + self.assertEqual(result.strong_collisions, []) + + def test_both_merged_pair_is_history_not_a_collision(self): + """Acceptance 4: two merged sides are both on main -> NOTHING.""" + result = detect_path_collisions([ + _pr(900050, ["gone/x.md"], state="merged"), + _pr(900051, ["gone/x.md"], state="merged"), + ]) + self.assertEqual(result.n_collisions, 0) + self.assertIn((900050, 900051), result.merged_pairs_excluded) + + def test_terminal_carries_the_measured_overlap_ratio(self): + """Acceptance 3: the verdict is auditable, not merely asserted. + + The founding pair's real overlap is 5 of 5 and 5 of 6 -> 0.833..., so a + fixture scoring a perfect 1.00 would hide how close the gate runs to + its threshold. + """ + f_open, f_merged, f_paths, f_extra = _mod.FOUNDING_TERMINAL_PAIR + result = detect_path_collisions([ + _pr(f_open, list(f_paths)), + _pr(f_merged, list(f_paths) + [f_extra], state="merged"), + ]) + c = result.collisions[0] + self.assertAlmostEqual(c.overlap_ratio, 5 / 6) + + def test_incidental_low_overlap_merged_pair_is_excluded(self): + """A shared ``.gitignore`` is not a double delivery (#15578 calibration). + + Ungated, this measurement put 95 pairs on 36 of 63 open PRs -- every + other open PR -- which is the "reports everything" collapse the module + docstring warns about. + """ + result = detect_path_collisions([ + _pr(900070, [".gitignore"] + [f"big/{i}.py" for i in range(11)]), + _pr(900071, [".gitignore"], state="merged"), + ]) + self.assertEqual(result.n_collisions, 0) + self.assertIn((900070, 900071), result.merged_low_overlap_excluded) + self.assertEqual(result.terminal_collisions, []) + + def test_overlap_ratio_boundary_is_inclusive(self): + """Exactly at the threshold is KEPT: the gate is a floor, not a cliff.""" + n_shared = int(round(_mod.TERMINAL_MIN_OVERLAP_RATIO * 4)) + shared = [f"s/{i}.py" for i in range(n_shared)] + result = detect_path_collisions([ + _pr(900080, shared + [f"a/{i}.py" for i in range(4 - n_shared)]), + _pr(900081, shared + [f"b/{i}.py" for i in range(4 - n_shared)], + state="merged"), + ]) + self.assertEqual(result.terminal_collisions[0].overlap_ratio, + _mod.TERMINAL_MIN_OVERLAP_RATIO) + + def test_overlap_ratio_of_an_empty_side_is_zero(self): + """A degenerate denominator must never read as a perfect overlap.""" + empty = _mod.PrRow(number=1, title="", paths=()) + other = _mod.PrRow(number=2, title="", paths=("a/x.md",)) + self.assertEqual( + _mod.terminal_overlap_ratio(("a/x.md",), empty, other), 0.0, + ) + + def test_open_pair_tier_untouched_beside_a_merged_row(self): + """Acceptance 4: no inflation, no deflation of open/open pairs.""" + result = detect_path_collisions([ + _pr(1, ["open/y.md"], title="fix(#500): a"), + _pr(2, ["open/y.md"], title="fix(#500): b"), + _pr(3, ["gone/z.md"], title="merged", state="merged"), + ]) + self.assertEqual(result.n_collisions, 1) + self.assertEqual(result.collisions[0].tier, "strong") + self.assertIsNone(result.collisions[0].merged_side) + + def test_weak_open_pair_stays_weak_beside_a_merged_row(self): + result = detect_path_collisions([ + _pr(1, ["README.md"], title="docs(#1): a"), + _pr(2, ["README.md"], title="docs(#2): b"), + _pr(3, ["gone/z.md"], state="merged"), + ]) + self.assertEqual(result.collisions[0].tier, "weak") + + def test_actionable_selector_keeps_terminal_and_drops_weak(self): + """--same-issue-only must not swallow the loudest signal it has.""" + _, f_merged, f_paths, _ = _mod.FOUNDING_TERMINAL_PAIR + result = detect_path_collisions([ + _pr(1, ["README.md"], title="docs(#1): a"), + _pr(2, ["README.md"], title="docs(#2): b"), + _pr(700, list(f_paths), title="open side"), + _pr(f_merged, list(f_paths), title="merged side", state="merged"), + ]) + self.assertEqual( + sorted(c.tier for c in result.actionable_collisions()), + ["terminal"], + ) + + def test_terminal_render_names_the_merged_side_and_main(self): + """Acceptance 2: the comment SAYS the substance is already on main.""" + f_open, f_merged, f_paths, _ = _mod.FOUNDING_TERMINAL_PAIR + result = detect_path_collisions([ + _pr(f_open, list(f_paths), title="tooling(notebook): detecteur"), + _pr(f_merged, list(f_paths), title="tooling(#15405): detecteur", + state="merged"), + ]) + body = render_comment( + f_open, "tooling(notebook): detecteur", + collisions_for_pr(f_open, result.collisions), + ) + self.assertIn("terminal", body) + self.assertIn(f"#{f_merged}", body) + self.assertIn("`main`", body) + self.assertIn(_mod.COMMENT_MARKER_END, body) + + def test_merged_side_render_does_not_claim_the_open_side_is_merged(self): + """Rendering for the merged number names the merged one, not the other.""" + f_open, f_merged, f_paths, _ = _mod.FOUNDING_TERMINAL_PAIR + result = detect_path_collisions([ + _pr(f_open, list(f_paths), title="open side"), + _pr(f_merged, list(f_paths), title="merged side", state="merged"), + ]) + body = render_comment( + f_merged, "merged side", collisions_for_pr(f_merged, result.collisions) + ) + self.assertIn(f"**#{f_merged}**", body) + self.assertIn("est deja sur", body) + + def test_widening_the_pool_does_not_churn_open_pair_comments(self): + """The pre-existing comment body is byte-identical -> no mass re-post. + + Widening the pool must not rewrite the comment of a PR whose situation + did not change: a churned body means a PATCH per open PR on the first + run, which is exactly the write storm the marker protocol avoids. + """ + open_rows = [ + _pr(1, ["x.ipynb"], title="fix(#9): a"), + _pr(2, ["x.ipynb"], title="fix(#9): b"), + ] + before = detect_path_collisions(open_rows) + after = detect_path_collisions( + open_rows + [_pr(3, ["gone/z.md"], title="m", state="merged")] + ) + b_before = render_comment(1, "fix(#9): a", + collisions_for_pr(1, before.collisions)) + b_after = render_comment(1, "fix(#9): a", + collisions_for_pr(1, after.collisions)) + self.assertEqual(b_before, b_after) + self.assertNotIn("terminal", b_before) + + +class TestMergedPoolWiring(unittest.TestCase): + """The gh-side wiring of #15578: window filter + never comment a merge.""" + + def test_list_recently_merged_prs_filters_state_and_window(self): + payload = [ + {"number": 10, "title": "in window", "state": "MERGED", + "mergedAt": "2026-09-10T00:00:00Z", "files": [{"path": "a.md"}]}, + {"number": 11, "title": "too old", "state": "MERGED", + "mergedAt": "2026-08-01T00:00:00Z", "files": [{"path": "a.md"}]}, + {"number": 12, "title": "not merged after all", "state": "OPEN", + "mergedAt": "", "files": [{"path": "a.md"}]}, + ] + with mock.patch.object(_mod, "_gh_json", return_value=payload): + rows = _mod.list_recently_merged_prs("owner/repo", "2026-09-08", 500) + self.assertEqual([r.number for r in rows], [10]) + self.assertTrue(rows[0].is_merged) + + def test_merged_window_zero_disables_the_merged_pool(self): + """0 is a real opt-out, and it must not call the merged listing.""" + calls = [] + + def _fake_merged(repo, since, limit): + calls.append(since) + return [] + + with mock.patch.object(_mod, "list_open_prs", return_value=[]), mock.patch.object(_mod, "list_recently_merged_prs", + side_effect=_fake_merged), mock.patch.object(_mod, "_repo_default", return_value="o/r"), mock.patch.object(_mod, "scan_markers", return_value=({}, set())), mock.patch.object(_mod, "label_strong_pairs", return_value=None): + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + rc = _mod._cli(["--merged-window-days", "0"]) + self.assertEqual(rc, 0) + self.assertEqual(calls, []) + + def test_cli_comments_the_open_side_and_never_the_merged_one(self): + """Acceptance 2 wired: the write set is the OPEN side of the pair.""" + f_open, f_merged, f_paths, _ = _mod.FOUNDING_TERMINAL_PAIR + open_rows = [_pr(f_open, list(f_paths), title="open side")] + merged_rows = [_pr(f_merged, list(f_paths), title="merged side", + state="merged")] + written: list[int] = [] + posted_bodies: list[str] = [] + + def _capture_post(repo, number, body, dry_run): + written.append(number) + posted_bodies.append(body) + return True + + with mock.patch.object(_mod, "list_open_prs", return_value=open_rows), mock.patch.object(_mod, "list_recently_merged_prs", + return_value=merged_rows), mock.patch.object(_mod, "_repo_default", return_value="o/r"), mock.patch.object(_mod, "scan_markers", return_value=({}, set())), mock.patch.object(_mod, "label_strong_pairs", return_value=None), mock.patch.object(_mod, "post_comment", side_effect=_capture_post): + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + rc = _mod._cli([]) + self.assertEqual(rc, 0) + self.assertEqual(written, [f_open]) + self.assertNotIn(f_merged, written) + self.assertIn("terminal", posted_bodies[0]) + self.assertIn(str(f_merged), posted_bodies[0]) + + class TestCommentProtocol(unittest.TestCase): def test_render_comment_is_marker_framed(self): result = detect_path_collisions( From 57bb34a468b6a821276561d3da4405ef7797ef64 Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 11 Sep 2026 14:24:31 +0200 Subject: [PATCH 02/10] =?UTF-8?q?feat(lean,#14962):=20fait=20de=20Fox=20?= =?UTF-8?q?=E2=80=94=20paire=20de=20dessus=20dans=20une=20classe=20d'arcs,?= =?UTF-8?q?=20somme=20de=20ligne=20nulle=20(FR+EN)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit arcPartition_sameClass_overStrand : en chaque croisement d'un diagramme a etiquettes en plage, e2 et e4 partagent une classe de arcPartition (repli sur mergePair). alexanderEntry_sum_zero : sous hypotheses d'unicite countP=1, la ligne d'alexanderEntry somme a zero — le socle du mineur independant de la colonne frappee. Sorry 10 -> 10 (grain additif). Voir #14962. Co-Authored-By: Claude Sonnet 5 --- .../Lean/knot_lean/Knots/Conway.lean | 203 ++++++++++++++++++ .../Lean/knot_lean/Knots/Conway_en.lean | 201 +++++++++++++++++ 2 files changed, 404 insertions(+) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway.lean b/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway.lean index f0ef1ad446..227954970b 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway.lean +++ b/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway.lean @@ -352,6 +352,151 @@ def arcPartition (d : KnotDiagram) : List (List Nat) := let pairs := d.crossings.map (fun c => (c.e2, c.e4)) pairs.foldl (fun P p => mergePair P p.1 p.2) singles +/-! #### Le fait de Fox : la paire de dessus partage une classe d'arcs + +La docstring d'`alexanderEntry` avance que « chaque ligne somme à zéro ». +Ce n'est pas une propriété de la ligne seule : c'est la conséquence d'un fait +structurel de `arcPartition` — en chaque croisement, les deux étiquettes de +dessus `e2` et `e4` appartiennent à une même classe. `mergePair` fusionne +précisément cette paire, et le repli ne fait ensuite qu'unir des classes, +jamais en scinder une : c'est la traduction combinatoire de la relation de +Wirtinger. Les lemmes qui suivent l'établissent pour tout diagramme dont les +étiquettes d'arêtes vivent dans la plage `1..numEdges` (cf `EdgesInRange`). +-/ + +/-- Deux étiquettes d'arêtes partagent une classe de la partition `P`. -/ +def SameClass (P : List (List Nat)) (x y : Nat) : Prop := + ∃ C ∈ P, x ∈ C ∧ y ∈ C + +/-- L'étiquette `z` est portée par au moins une classe de `P`. -/ +def Covered (P : List (List Nat)) (z : Nat) : Prop := ∃ C ∈ P, z ∈ C + +/-- Étape du repli de `arcPartition` : fusionner la paire de dessus. -/ +def mergeStep (P : List (List Nat)) (p : Nat × Nat) : List (List Nat) := + mergePair P p.1 p.2 + +/-- Forme dépliée de `mergePair` : les classes intactes, puis la classe +fusionnée. -/ +lemma mergePair_eq (P : List (List Nat)) (x y : Nat) : + mergePair P x y = + (P.filter (fun C => !C.contains x && !C.contains y)) ++ + [(P.filter (fun C => C.contains x || C.contains y)).flatten.eraseDups] := rfl + +/-- La classe fusionnée porte toute étiquette d'une classe du filtre `hit`. -/ +lemma mem_merged {P : List (List Nat)} {C : List Nat} {z x y : Nat} + (hC : C ∈ P) (hz : z ∈ C) (hxy : (C.contains x || C.contains y) = true) : + z ∈ (P.filter (fun C => C.contains x || C.contains y)).flatten.eraseDups := by + rw [List.mem_eraseDups, List.mem_flatten] + exact ⟨C, List.mem_filter.mpr ⟨hC, hxy⟩, hz⟩ + +/-- Une classe qui ne porte ni `x` ni `y` reste intacte dans `keep`. -/ +lemma keep_filter {P : List (List Nat)} {C : List Nat} {x y : Nat} + (hC : C ∈ P) (hmem : ¬(C.contains x || C.contains y) = true) : + C ∈ (P.filter (fun C => !C.contains x && !C.contains y)) := by + refine List.mem_filter.mpr ⟨hC, ?_⟩ + simpa using hmem + +/-- `mergePair` ne fait pas disparaître une étiquette déjà couverte. -/ +lemma covered_mergePair {P : List (List Nat)} {x y z : Nat} (h : Covered P z) : + Covered (mergePair P x y) z := by + obtain ⟨C, hC, hz⟩ := h + by_cases hmem : (C.contains x || C.contains y) = true + · refine ⟨_, ?_, mem_merged hC hz hmem⟩ + rw [mergePair_eq, List.mem_append]; right; exact List.mem_singleton.mpr rfl + · refine ⟨C, ?_, hz⟩ + rw [mergePair_eq, List.mem_append]; left + exact keep_filter hC hmem + +/-- `mergePair` ne scinde aucune classe : deux étiquettes qui partageaient une +classe continuent de la partager. -/ +lemma sameClass_mergePair {P : List (List Nat)} {x y a b : Nat} + (h : SameClass P a b) : SameClass (mergePair P x y) a b := by + obtain ⟨C, hC, ha, hb⟩ := h + by_cases hmem : (C.contains x || C.contains y) = true + · refine ⟨_, ?_, mem_merged hC ha hmem, mem_merged hC hb hmem⟩ + rw [mergePair_eq, List.mem_append]; right; exact List.mem_singleton.mpr rfl + · refine ⟨C, ?_, ha, hb⟩ + rw [mergePair_eq, List.mem_append]; left + exact keep_filter hC hmem + +/-- `mergePair` réunit effectivement `x` et `y` dans une même classe, dès lors +que l'un et l'autre sont couverts (le filtre `hit` n'est alors pas vide et la +classe fusionnée les porte tous les deux). -/ +lemma sameClass_mergePair_self {P : List (List Nat)} {x y : Nat} + (hx : Covered P x) (hy : Covered P y) : SameClass (mergePair P x y) x y := by + obtain ⟨Cx, hCx, hx'⟩ := hx + obtain ⟨Cy, hCy, hy'⟩ := hy + have hmx : (Cx.contains x || Cx.contains y) = true := by + rw [Bool.or_eq_true]; left; exact List.contains_iff_mem.mpr hx' + have hmy : (Cy.contains x || Cy.contains y) = true := by + rw [Bool.or_eq_true]; right; exact List.contains_iff_mem.mpr hy' + refine ⟨_, ?_, mem_merged hCx hx' hmx, mem_merged hCy hy' hmy⟩ + rw [mergePair_eq, List.mem_append]; right; exact List.mem_singleton.mpr rfl + +/-- Le repli préserve l'appartenance partagée. -/ +lemma sameClass_foldl {pairs : List (Nat × Nat)} {P : List (List Nat)} {a b : Nat} + (h : SameClass P a b) : SameClass (pairs.foldl mergeStep P) a b := by + induction pairs generalizing P with + | nil => exact h + | cons p ps ih => rw [List.foldl_cons]; exact ih (sameClass_mergePair h) + +/-- Toute paire rencontrée pendant le repli finit dans une même classe. -/ +lemma sameClass_foldl_of_mem {pairs : List (Nat × Nat)} {P : List (List Nat)} + (hcover : ∀ q ∈ pairs, Covered P q.1 ∧ Covered P q.2) : + ∀ q ∈ pairs, SameClass (pairs.foldl mergeStep P) q.1 q.2 := by + induction pairs generalizing P with + | nil => intro q hq; simp at hq + | cons p ps ih => + intro q hq + rw [List.foldl_cons] + rcases List.mem_cons.mp hq with rfl | hqs + · exact sameClass_foldl (sameClass_mergePair_self + (hcover q (List.mem_cons.mpr (Or.inl rfl))).1 + (hcover q (List.mem_cons.mpr (Or.inl rfl))).2) + · exact ih (P := mergeStep P p) + (fun r hr => ⟨covered_mergePair (hcover r (List.mem_cons.mpr (Or.inr hr))).1, + covered_mergePair (hcover r (List.mem_cons.mpr (Or.inr hr))).2⟩) + q hqs + +/-- Toute étiquette de la plage `1..n` est couverte par les singletons initiaux. -/ +lemma covered_singles {n z : Nat} (h1 : 1 ≤ z) (h2 : z ≤ n) : + Covered ((List.range n).map (fun i => [i + 1])) z := by + refine ⟨[z], ?_, by simp⟩ + rw [List.mem_map] + exact ⟨z - 1, by rw [List.mem_range]; omega, by simp only [Nat.sub_add_cancel h1]⟩ + +/-- Les quatre étiquettes d'arête de chaque croisement vivent dans la plage +`1..numEdges` du diagramme. -/ +def EdgesInRange (d : KnotDiagram) : Prop := + ∀ c ∈ d.crossings, 1 ≤ c.e1 ∧ c.e1 ≤ d.numEdges ∧ + 1 ≤ c.e2 ∧ c.e2 ≤ d.numEdges ∧ + 1 ≤ c.e3 ∧ c.e3 ≤ d.numEdges ∧ + 1 ≤ c.e4 ∧ c.e4 ≤ d.numEdges + +/-- Le repli de `arcPartition` en forme de `foldl` sur `mergeStep`. -/ +lemma arcPartition_eq (d : KnotDiagram) : + arcPartition d = (d.crossings.map (fun c => (c.e2, c.e4))).foldl mergeStep + ((List.range d.numEdges).map (fun i => [i + 1])) := rfl + +/-- **Le fait de Fox** : en chaque croisement d'un diagramme à étiquettes en +plage, les deux étiquettes de dessus appartiennent à une même classe de la +partition d'arcs. C'est ce fait — et non la seule garde de cardinal de +`alexanderPolynomialAux` — qui porte la somme de ligne nulle de la matrice +d'Alexander (cf `alexanderEntry_sum_zero` ci-dessous). -/ +theorem arcPartition_sameClass_overStrand (d : KnotDiagram) (h : EdgesInRange d) + {c : PDCrossing} (hc : c ∈ d.crossings) : + SameClass (arcPartition d) c.e2 c.e4 := by + rw [arcPartition_eq] + have hcover : ∀ q ∈ d.crossings.map (fun c => (c.e2, c.e4)), + Covered ((List.range d.numEdges).map (fun i => [i + 1])) q.1 ∧ + Covered ((List.range d.numEdges).map (fun i => [i + 1])) q.2 := by + intro q hq + rw [List.mem_map] at hq + obtain ⟨c', hc', rfl⟩ := hq + obtain ⟨_, _, h2lo, h2hi, _, _, h4lo, h4hi⟩ := h c' hc' + exact ⟨covered_singles h2lo h2hi, covered_singles h4lo h4hi⟩ + exact sameClass_foldl_of_mem hcover (c.e2, c.e4) (List.mem_map.mpr ⟨c, hc, rfl⟩) + /-- Contrôle : la partition d'arcs du code Conway corrigé — 11 arcs couvrant les 22 arêtes (condition de non-dégénérescence du mineur d'Alexander : la garde `arcs'.length = rest.length + 1` de `alexanderPolynomialAux` passe). @@ -383,6 +528,64 @@ noncomputable def alexanderEntry (c : PDCrossing) (C : List Nat) : Polynomial + (if C.contains c.e3 then -(1 : Polynomial ℤ) else 0) + (if C.contains c.e2 || C.contains c.e4 then 1 - Polynomial.X else 0) +/-! #### Les lignes d'Alexander somment à zéro + +Sous les hypothèses d'unicité — chaque étiquette du dessous portée par +exactement une classe, la paire de dessus rencontrant exactement une classe — +chaque ligne de la matrice d'Alexander somme à zéro : `t − 1 + (1 − t) = 0`. +C'est ce fait qui rend le mineur (n−1)×(n−1) indépendant, à un signe près, du +choix de la colonne frappée : l'affirmation normative de la docstring +d'`alexanderEntry` devient ici un théorème. `arcPartition_sameClass_overStrand` +fournit la moitié combinatoire (la paire de dessus partage une classe) ; +la vérification que `arcPartition` satisfait les hypothèses d'unicité +(`countP` = 1 par étiquette) reste à établir — tranche suivante de See #14962. +-/ + +/-- Somme d'une carte indicatrice : le `w` est compté une fois par classe +porteuse. -/ +lemma sum_map_indicator (P : List (List Nat)) (p : List Nat → Bool) (w : Polynomial ℤ) : + (P.map (fun C => if p C then w else 0)).sum = w * (P.countP p : Polynomial ℤ) := by + induction P with + | nil => simp + | cons D Ps ih => + by_cases hD : p D = true + · simp only [List.map_cons, List.sum_cons, ih, List.countP_cons, hD, if_true] + push_cast + ring + · simp only [List.map_cons, List.sum_cons, ih, List.countP_cons, hD, Bool.false_eq_true, + if_false] + push_cast + ring + +/-- La somme d'une carte à trois termes se distribue sur les trois sommes. -/ +lemma sum_map_three (P : List (List Nat)) (f g h : List Nat → Polynomial ℤ) : + (P.map (fun C => f C + g C + h C)).sum = + (P.map f).sum + (P.map g).sum + (P.map h).sum := by + induction P with + | nil => simp + | cons D Ps ih => simp only [List.map_cons, List.sum_cons, ih]; abel + +/-- **Somme de ligne nulle** : si chaque étiquette du dessous est portée par +exactement une classe et si la paire de dessus rencontre exactement une +classe, alors la ligne d'`alexanderEntry` somme à zéro. C'est le fait de Fox +qui rend le mineur (n−1)×(n−1) indépendant, à un signe près, du choix de la +colonne frappée — le socle demandé par See #14962 avant toute correction de +normalisation. -/ +theorem alexanderEntry_sum_zero (P : List (List Nat)) (c : PDCrossing) + (h1 : P.countP (fun C => C.contains c.e1) = 1) + (h3 : P.countP (fun C => C.contains c.e3) = 1) + (h24 : P.countP (fun C => C.contains c.e2 || C.contains c.e4) = 1) : + (P.map (alexanderEntry c)).sum = 0 := by + have hmap : (P.map (alexanderEntry c)) = P.map (fun C : List Nat => + ((if C.contains c.e1 then (Polynomial.X : Polynomial ℤ) else 0) + + (if C.contains c.e3 then (-(1 : Polynomial ℤ)) else 0) + + (if C.contains c.e2 || C.contains c.e4 then (1 : Polynomial ℤ) - Polynomial.X + else 0))) := by + congr 1 + rw [hmap, sum_map_three, sum_map_indicator, h1, sum_map_indicator, h3, sum_map_indicator, h24] + push_cast + ring + /-- Type des valeurs du polynôme d'Alexander : ℤ[t]. -/ abbrev AlexanderPoly := Polynomial ℤ diff --git a/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway_en.lean b/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway_en.lean index 84ef25844e..a7d4bc24ad 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway_en.lean +++ b/MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Conway_en.lean @@ -358,6 +358,150 @@ def arcPartition (d : KnotDiagram) : List (List Nat) := let pairs := d.crossings.map (fun c => (c.e2, c.e4)) pairs.foldl (fun P p => mergePair P p.1 p.2) singles +/-! #### The Fox fact: the over-strand pair shares one arc class + +The docstring of `alexanderEntry` claims that "every row sums to zero". +That is not a property of the row alone: it follows from a structural fact +about `arcPartition` — at every crossing, the two over-strand labels `e2` +and `e4` belong to one and the same class. `mergePair` merges precisely +that pair, and the fold afterwards only ever unites classes, never splits +one: this is the combinatorial translation of the Wirtinger relation. The +lemmas below establish it for every diagram whose edge labels live in the +range `1..numEdges` (cf `EdgesInRange`). +-/ + +/-- Two edge labels share one class of the partition `P`. -/ +def SameClass (P : List (List Nat)) (x y : Nat) : Prop := + ∃ C ∈ P, x ∈ C ∧ y ∈ C + +/-- The label `z` is carried by at least one class of `P`. -/ +def Covered (P : List (List Nat)) (z : Nat) : Prop := ∃ C ∈ P, z ∈ C + +/-- One step of the `arcPartition` fold: merge the over-strand pair. -/ +def mergeStep (P : List (List Nat)) (p : Nat × Nat) : List (List Nat) := + mergePair P p.1 p.2 + +/-- Unfolded form of `mergePair`: the untouched classes, then the merged +class. -/ +lemma mergePair_eq (P : List (List Nat)) (x y : Nat) : + mergePair P x y = + (P.filter (fun C => !C.contains x && !C.contains y)) ++ + [(P.filter (fun C => C.contains x || C.contains y)).flatten.eraseDups] := rfl + +/-- The merged class carries every label of a class of the `hit` filter. -/ +lemma mem_merged {P : List (List Nat)} {C : List Nat} {z x y : Nat} + (hC : C ∈ P) (hz : z ∈ C) (hxy : (C.contains x || C.contains y) = true) : + z ∈ (P.filter (fun C => C.contains x || C.contains y)).flatten.eraseDups := by + rw [List.mem_eraseDups, List.mem_flatten] + exact ⟨C, List.mem_filter.mpr ⟨hC, hxy⟩, hz⟩ + +/-- A class carrying neither `x` nor `y` stays untouched in `keep`. -/ +lemma keep_filter {P : List (List Nat)} {C : List Nat} {x y : Nat} + (hC : C ∈ P) (hmem : ¬(C.contains x || C.contains y) = true) : + C ∈ (P.filter (fun C => !C.contains x && !C.contains y)) := by + refine List.mem_filter.mpr ⟨hC, ?_⟩ + simpa using hmem + +/-- `mergePair` never drops an already covered label. -/ +lemma covered_mergePair {P : List (List Nat)} {x y z : Nat} (h : Covered P z) : + Covered (mergePair P x y) z := by + obtain ⟨C, hC, hz⟩ := h + by_cases hmem : (C.contains x || C.contains y) = true + · refine ⟨_, ?_, mem_merged hC hz hmem⟩ + rw [mergePair_eq, List.mem_append]; right; exact List.mem_singleton.mpr rfl + · refine ⟨C, ?_, hz⟩ + rw [mergePair_eq, List.mem_append]; left + exact keep_filter hC hmem + +/-- `mergePair` splits no class: two labels that shared a class still do. -/ +lemma sameClass_mergePair {P : List (List Nat)} {x y a b : Nat} + (h : SameClass P a b) : SameClass (mergePair P x y) a b := by + obtain ⟨C, hC, ha, hb⟩ := h + by_cases hmem : (C.contains x || C.contains y) = true + · refine ⟨_, ?_, mem_merged hC ha hmem, mem_merged hC hb hmem⟩ + rw [mergePair_eq, List.mem_append]; right; exact List.mem_singleton.mpr rfl + · refine ⟨C, ?_, ha, hb⟩ + rw [mergePair_eq, List.mem_append]; left + exact keep_filter hC hmem + +/-- `mergePair` effectively gathers `x` and `y` into one class, as soon as +both are covered (the `hit` filter is then nonempty and the merged class +carries them both). -/ +lemma sameClass_mergePair_self {P : List (List Nat)} {x y : Nat} + (hx : Covered P x) (hy : Covered P y) : SameClass (mergePair P x y) x y := by + obtain ⟨Cx, hCx, hx'⟩ := hx + obtain ⟨Cy, hCy, hy'⟩ := hy + have hmx : (Cx.contains x || Cx.contains y) = true := by + rw [Bool.or_eq_true]; left; exact List.contains_iff_mem.mpr hx' + have hmy : (Cy.contains x || Cy.contains y) = true := by + rw [Bool.or_eq_true]; right; exact List.contains_iff_mem.mpr hy' + refine ⟨_, ?_, mem_merged hCx hx' hmx, mem_merged hCy hy' hmy⟩ + rw [mergePair_eq, List.mem_append]; right; exact List.mem_singleton.mpr rfl + +/-- The fold preserves shared membership. -/ +lemma sameClass_foldl {pairs : List (Nat × Nat)} {P : List (List Nat)} {a b : Nat} + (h : SameClass P a b) : SameClass (pairs.foldl mergeStep P) a b := by + induction pairs generalizing P with + | nil => exact h + | cons p ps ih => rw [List.foldl_cons]; exact ih (sameClass_mergePair h) + +/-- Every pair met during the fold ends up in one class. -/ +lemma sameClass_foldl_of_mem {pairs : List (Nat × Nat)} {P : List (List Nat)} + (hcover : ∀ q ∈ pairs, Covered P q.1 ∧ Covered P q.2) : + ∀ q ∈ pairs, SameClass (pairs.foldl mergeStep P) q.1 q.2 := by + induction pairs generalizing P with + | nil => intro q hq; simp at hq + | cons p ps ih => + intro q hq + rw [List.foldl_cons] + rcases List.mem_cons.mp hq with rfl | hqs + · exact sameClass_foldl (sameClass_mergePair_self + (hcover q (List.mem_cons.mpr (Or.inl rfl))).1 + (hcover q (List.mem_cons.mpr (Or.inl rfl))).2) + · exact ih (P := mergeStep P p) + (fun r hr => ⟨covered_mergePair (hcover r (List.mem_cons.mpr (Or.inr hr))).1, + covered_mergePair (hcover r (List.mem_cons.mpr (Or.inr hr))).2⟩) + q hqs + +/-- Every label of the range `1..n` is covered by the initial singletons. -/ +lemma covered_singles {n z : Nat} (h1 : 1 ≤ z) (h2 : z ≤ n) : + Covered ((List.range n).map (fun i => [i + 1])) z := by + refine ⟨[z], ?_, by simp⟩ + rw [List.mem_map] + exact ⟨z - 1, by rw [List.mem_range]; omega, by simp only [Nat.sub_add_cancel h1]⟩ + +/-- The four edge labels of every crossing live in the diagram's range +`1..numEdges`. -/ +def EdgesInRange (d : KnotDiagram) : Prop := + ∀ c ∈ d.crossings, 1 ≤ c.e1 ∧ c.e1 ≤ d.numEdges ∧ + 1 ≤ c.e2 ∧ c.e2 ≤ d.numEdges ∧ + 1 ≤ c.e3 ∧ c.e3 ≤ d.numEdges ∧ + 1 ≤ c.e4 ∧ c.e4 ≤ d.numEdges + +/-- The `arcPartition` fold in `foldl` form over `mergeStep`. -/ +lemma arcPartition_eq (d : KnotDiagram) : + arcPartition d = (d.crossings.map (fun c => (c.e2, c.e4))).foldl mergeStep + ((List.range d.numEdges).map (fun i => [i + 1])) := rfl + +/-- **The Fox fact**: at every crossing of a diagram with labels in range, +the two over-strand labels belong to one and the same class of the arc +partition. It is this fact — not the mere cardinal guard of +`alexanderPolynomialAux` — that carries the zero row sum of the Alexander +matrix (cf `alexanderEntry_sum_zero` below). -/ +theorem arcPartition_sameClass_overStrand (d : KnotDiagram) (h : EdgesInRange d) + {c : PDCrossing} (hc : c ∈ d.crossings) : + SameClass (arcPartition d) c.e2 c.e4 := by + rw [arcPartition_eq] + have hcover : ∀ q ∈ d.crossings.map (fun c => (c.e2, c.e4)), + Covered ((List.range d.numEdges).map (fun i => [i + 1])) q.1 ∧ + Covered ((List.range d.numEdges).map (fun i => [i + 1])) q.2 := by + intro q hq + rw [List.mem_map] at hq + obtain ⟨c', hc', rfl⟩ := hq + obtain ⟨_, _, h2lo, h2hi, _, _, h4lo, h4hi⟩ := h c' hc' + exact ⟨covered_singles h2lo h2hi, covered_singles h4lo h4hi⟩ + exact sameClass_foldl_of_mem hcover (c.e2, c.e4) (List.mem_map.mpr ⟨c, hc, rfl⟩) + /-- Control: the arc partition of the corrected Conway code — 11 arcs covering the 22 edges (non-degeneracy condition of the Alexander minor: the guard `arcs'.length = rest.length + 1` of `alexanderPolynomialAux` @@ -389,6 +533,63 @@ noncomputable def alexanderEntry (c : PDCrossing) (C : List Nat) : Polynomial + (if C.contains c.e3 then -(1 : Polynomial ℤ) else 0) + (if C.contains c.e2 || C.contains c.e4 then 1 - Polynomial.X else 0) +/-! #### The Alexander rows sum to zero + +Under the uniqueness hypotheses — each under-strand label carried by exactly +one class, the over-strand pair meeting exactly one class — every row of the +Alexander matrix sums to zero: `t − 1 + (1 − t) = 0`. It is this fact that +makes the (n−1)×(n−1) minor independent, up to a sign, of the choice of the +struck column: the normative claim in the docstring of `alexanderEntry` +becomes a theorem here. `arcPartition_sameClass_overStrand` provides the +combinatorial half (the over-strand pair shares one class); the verification +that `arcPartition` satisfies the uniqueness hypotheses (`countP` = 1 per +label) remains to be established — the next tranche of See #14962. +-/ + +/-- Sum of an indicator map: `w` is counted once per carrying class. -/ +lemma sum_map_indicator (P : List (List Nat)) (p : List Nat → Bool) (w : Polynomial ℤ) : + (P.map (fun C => if p C then w else 0)).sum = w * (P.countP p : Polynomial ℤ) := by + induction P with + | nil => simp + | cons D Ps ih => + by_cases hD : p D = true + · simp only [List.map_cons, List.sum_cons, ih, List.countP_cons, hD, if_true] + push_cast + ring + · simp only [List.map_cons, List.sum_cons, ih, List.countP_cons, hD, Bool.false_eq_true, + if_false] + push_cast + ring + +/-- The sum of a three-term map distributes over the three sums. -/ +lemma sum_map_three (P : List (List Nat)) (f g h : List Nat → Polynomial ℤ) : + (P.map (fun C => f C + g C + h C)).sum = + (P.map f).sum + (P.map g).sum + (P.map h).sum := by + induction P with + | nil => simp + | cons D Ps ih => simp only [List.map_cons, List.sum_cons, ih]; abel + +/-- **Zero row sum**: if each under-strand label is carried by exactly one +class and the over-strand pair meets exactly one class, then the +`alexanderEntry` row sums to zero. This is the Fox fact that makes the +(n−1)×(n−1) minor independent, up to a sign, of the choice of the struck +column — the foundation requested by See #14962 before any normalization +fix. -/ +theorem alexanderEntry_sum_zero (P : List (List Nat)) (c : PDCrossing) + (h1 : P.countP (fun C => C.contains c.e1) = 1) + (h3 : P.countP (fun C => C.contains c.e3) = 1) + (h24 : P.countP (fun C => C.contains c.e2 || C.contains c.e4) = 1) : + (P.map (alexanderEntry c)).sum = 0 := by + have hmap : (P.map (alexanderEntry c)) = P.map (fun C : List Nat => + ((if C.contains c.e1 then (Polynomial.X : Polynomial ℤ) else 0) + + (if C.contains c.e3 then (-(1 : Polynomial ℤ)) else 0) + + (if C.contains c.e2 || C.contains c.e4 then (1 : Polynomial ℤ) - Polynomial.X + else 0))) := by + congr 1 + rw [hmap, sum_map_three, sum_map_indicator, h1, sum_map_indicator, h3, sum_map_indicator, h24] + push_cast + ring + /-- Type of Alexander polynomial values: ℤ[t]. -/ abbrev AlexanderPoly := Polynomial ℤ From 91a50fd6326e8af23cba498a68adafe0b2ee894d Mon Sep 17 00:00:00 2001 From: "Claude Haiku 4.5 (1M context)" Date: Fri, 11 Sep 2026 00:58:44 +0200 Subject: [PATCH 03/10] =?UTF-8?q?feat(ict,#15476):=20trace=20contract=20v1?= =?UTF-8?q?=20=E2=80=94=20instrument=20discriminant=20+=20alignement=20exp?= =?UTF-8?q?licite=20+=20asymetrie=20top-k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le contrat pose trois regles dures : (1) identite d'instrument via meta['instrument'] discriminant, (2) alignement explicite via check_alignment avec diagnostic actionnable, (3) asymetrie top-k documentee (SAE exact_zero vs J-Lens unobserved). Refactor sae_traces.load_traces + jlens_traces.load_traces pour brancher le contrat via _load_npz_unchecked (evite le cycle sae<-jlens). Retro-compat : legacy lens='sae'/'jacobian', alias d_model<->d_sae, inference sae_repo/lens_repo. Acceptance #1 verifiee first-hand via tests d'integration. Co-Authored-By: Claude Haiku 4.5 (1M context) --- .../IIT/ICT-Series/ict/jlens_traces.py | 69 +-- .../IIT/ICT-Series/ict/sae_traces.py | 46 +- .../IIT/ICT-Series/ict/trace_contract.py | 474 ++++++++++++++++++ .../IIT/ICT-Series/tests/test_jlens_traces.py | 24 +- .../IIT/ICT-Series/tests/test_sae_traces.py | 5 +- .../ICT-Series/tests/test_trace_contract.py | 449 +++++++++++++++++ 6 files changed, 1022 insertions(+), 45 deletions(-) create mode 100644 MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py create mode 100644 MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_trace_contract.py diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py index 04766dfb06..51f3b2359d 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py @@ -42,19 +42,25 @@ **exactement zero** : le SAE top-k force la troncature, donc :func:`densify` materialise une representation **exacte** (cf. :mod:`ict.sae_traces`). -Pour **J-Lens** (directions singulieres principales de la matrice jacobienne des -logits par token), ne garder que le top-k des directions par score est une -**troncature de rang-k** : les directions negligees ont un coefficient petit mais -**NON nul** en realite. :func:`densify` materialise donc une **approximation -rang-k** de la projection J-space, pas une exactitude absolue comme pour le SAE. +Pour **J-Lens** (IDs du vocabulaire du modele + logits signes captures +token par token), ne garder que le top-k par score est une **troncature +de rang-k** sur la projection : les identifiants et logits negliges +existent dans le buffer original mais **ne sont pas observes** dans la +trace exportee. :func:`densify` materialise donc une **vue partielle** +du J-space (un top-k d'identifiants vocabulaire + leurs logits signes), +pas une exactitude absolue comme pour le SAE -- les identifiants hors +top-k sont **non observes** (cf. :func:`ict.trace_contract.topk_semantics` +qui code cette asymetrie comme ``TOPK_UNOBSERVED`` vs ``TOPK_EXACT_ZERO`` +pour SAE). La batterie d'emergence tourne a l'identique (meme appareil, c'est l'objectif), MAIS le verdict doit etre rapporte avec cette nuance : la co-localisation SAE <-> J se mesure entre deux representations de natures differentes (l'une -exacte par construction, l'autre approximee par troncature). Ce n'est pas un -defaut -- c'est la meilleure approximation disponible d'un J-space qu'aucun -top-k n'epuise -- c'est une limite honnete a inscrire dans le notebook (garde-fou -#1 de :mod:`ict.workspace` : ne pas vendre comme equivalentes les deux lectures). +exacte par construction, l'autre partielle par troncature d'identifiants). +Ce n'est pas un defaut -- c'est la meilleure approximation disponible d'un +J-space qu'aucun top-k n'epuise -- c'est une limite honnete a inscrire dans +le notebook (garde-fou #1 de :mod:`ict.workspace` : ne pas vendre comme +equivalentes les deux lectures). Numpy uniquement : AUCUN import torch ici (le GPU reste confine au script d'extraction ``scripts/extract_jlens_traces.py``, piste GPU2 de #5681). @@ -82,7 +88,7 @@ binarize_quantile, states_from_panel, ) -from .sae_traces import load_traces as _sae_load_traces +from .sae_traces import _load_npz_unchecked __all__ = [ "load_traces", @@ -98,31 +104,36 @@ # --------------------------------------------------------------------------- # # Chargement (garde-fou anti-melange SAE <-> J-Lens, tete-a-tete #5681) # --------------------------------------------------------------------------- # -def load_traces(path: str | Path) -> dict: +def load_traces(path: str | Path, *, strict: bool = False) -> dict: """Recharge un ``.npz`` de traces **J-Lens** (meme schema que :mod:`ict.sae_traces`). - Garde-fou anti-melange : valide la nature de la trace via ``meta["lens"]``. + Validation du **contrat de trace v1** (:mod:`ict.trace_contract`) : + appelle :func:`ict.trace_contract.validate_manifest` puis + :func:`ict.trace_contract.enforce_instrument` avec ``expected="jlens"``. + Toute trace ``meta['instrument'] == 'sae'`` (ou un manifeste qui declare + un autre instrument du contrat) est REFUSEE avec un diagnostic + actionnable -- c'est l'acceptance #1 anti-melange du ticket #15476. - * ``meta["lens"] == "sae"`` -> **refuse** (c'est une trace SAE : utiliser - :func:`ict.sae_traces.load_traces`). Evite de charger une trace SAE comme - trace J-lens dans le notebook tete-a-tete #5681. - * ``meta["lens"] == "jacobian"`` -> **accepte** (trace J-Lens nominale). - * ``meta["lens"]`` absent -> **accepte** (retro-compatibilite avec un extracteur - qui n'ecrit pas encore le champ). L'extracteur GPU2 - (``scripts/extract_jlens_traces.py``) DOIT ecrire ``"lens": "jacobian"`` - pour la tracabilite du tete-a-tete. + Le contrat v1 introduit le champ ``meta['instrument']`` (canonique) en + plus du champ legacy ``meta['lens']``. Les deux sont lus en + retro-compatibilite : un manifeste sans ``instrument`` mais avec + ``lens='jacobian'`` est accepte comme J-Lens ; un manifeste avec + ``instrument='jlens'`` est accepte directement. + + Le parametre ``strict`` (defaut ``False``) suit la meme convention que + :func:`ict.sae_traces.load_traces` : ``False`` accepte les traces + historiques, ``True`` exige un manifeste v1 complet. Retourne ``{"meta": dict, "prompts": {(set_name, i): {"ids", "vals", "tokens"}}}`` -- meme structure que :func:`ict.sae_traces.load_traces` (les fonctions d'aval ``densify`` / ``differential_features`` / ... sont reexportees telles quelles depuis :mod:`ict.sae_traces`). """ - traces = _sae_load_traces(path) - lens = traces.get("meta", {}).get("lens") - if lens == "sae": - raise ValueError( - f"trace {path} porte meta['lens']='sae' : c'est une trace SAE, pas " - f"J-Lens. Utiliser ict.sae_traces.load_traces pour les traces SAE " - f"(garde-fou anti-melange du tete-a-tete #5681 Track S)." - ) - return traces + meta, prompts = _load_npz_unchecked(path) + # Contrat v1 : validation + anti-melange (acceptance #1). + # Import local pour eviter tout cycle d'import (trace_contract ne depend + # de rien du package). + from .trace_contract import validate_manifest, enforce_instrument + meta = validate_manifest(meta, strict=strict) + enforce_instrument(meta, "jlens") + return {"meta": meta, "prompts": prompts} diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py index 939af5512a..5e78242d4a 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py @@ -55,12 +55,15 @@ # --------------------------------------------------------------------------- # # Chargement # --------------------------------------------------------------------------- # -def load_traces(path: str | Path) -> dict: - """Recharge un ``.npz`` produit par ``scripts/extract_sae_traces.py``. - - Retourne ``{"meta": dict, "prompts": {(set_name, i): {"ids", "vals", - "tokens"}}}`` avec ``ids`` [T, k] int32, ``vals`` [T, k] float32 (depuis - float16), ``tokens`` [T] str. Aucun ``allow_pickle`` requis. +def _load_npz_unchecked(path: str | Path) -> tuple[dict, dict[tuple[str, int], dict]]: + """Lit un ``.npz`` et retourne ``(meta, prompts)`` SANS validation contrat. + + Fonction interne partagee entre :func:`load_traces` (SAE) et le chargeur + J-Lens (:mod:`ict.jlens_traces`) : la validation du contrat v1 et + l'enforcement d'instrument sont faits par l'appelant, PAS ici. Cela + evite que :func:`ict.jlens_traces.load_traces` (qui appelle historiquement + le chargeur SAE via delegation) refuse systematiquement les traces + J-Lens au premier controle d'instrument. """ data = np.load(Path(path), allow_pickle=False) meta = json.loads(str(data["__meta__"])) @@ -90,6 +93,37 @@ def load_traces(path: str | Path) -> dict: f"trace {set_name}__{idx} : {n_bad} vals non-finies — trace " f"pre-correctif (#12388), regenerer via extract_sae_traces.py " f"a jour (il exclut les positions non-finies a la capture)") + return meta, prompts + + +def load_traces(path: str | Path, *, strict: bool = False) -> dict: + """Recharge un ``.npz`` produit par ``scripts/extract_sae_traces.py``. + + Retourne ``{"meta": dict, "prompts": {(set_name, i): {"ids", "vals", + "tokens"}}}`` avec ``ids`` [T, k] int32, ``vals`` [T, k] float32 (depuis + float16), ``tokens`` [T] str. Aucun ``allow_pickle`` requis. + + Validation du **contrat de trace v1** (:mod:`ict.trace_contract`) : + appelle :func:`ict.trace_contract.validate_manifest` puis + :func:`ict.trace_contract.enforce_instrument` avec ``expected="sae"``. + Toute trace ``meta['instrument'] == 'jlens'`` (ou un manifeste sans + ``instrument`` et sans ``lens`` legacy) est REFUSEE avec un diagnostic + actionnable -- c'est l'acceptance #1 anti-melange du ticket #15476. + + Le parametre ``strict`` (defaut ``False``) regle la rigueur du contrat : + ``False`` accepte les manifestes historiques (sans ``instrument`` declare + ou avec le seul champ legacy ``meta['lens']='sae'``), ``True`` exige un + manifeste complet conforme au contrat v1. Les notebooks existants + chargent en ``strict=False`` ; les extracteurs GPU neuf et les tests + du contrat chargent en ``strict=True``. + """ + meta, prompts = _load_npz_unchecked(path) + # Contrat v1 : validation + anti-melange (acceptance #1). + # Import local pour eviter tout cycle d'import (trace_contract ne depend + # de rien du package, sae_traces trace_contract ne depend pas de sae_traces). + from .trace_contract import validate_manifest, enforce_instrument + meta = validate_manifest(meta, strict=strict) + enforce_instrument(meta, "sae") return {"meta": meta, "prompts": prompts} diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py new file mode 100644 index 0000000000..ba2a113d0b --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py @@ -0,0 +1,474 @@ +"""Contrat de trace ICT v1 -- manifeste, validation, alignement. + +Le **contrat de trace v1** est le format canonique des artefacts ICT-Series +(SAE, J-Lens, et tout futur instrument) qu'echangent les notebooks, les +extracteurs GPU et les loaders numpy-only. Il pose trois regles dures : + +1. **Identite d'instrument** : un champ ``meta["instrument"]`` discriminant + (``"sae"`` / ``"jlens"``) DOIT etre present. Une trace qui le declare + differemment du module qui la charge est REFUSEE -- c'est l'acceptance #1 + du ticket de fond #15476 (deux traces de meme forme mais d'instruments + differents ne peuvent plus etre melangees silencieusement). + +2. **Alignement explicite** : les champs d'alignement (modele / run / seed / + prompt set / prompt / token positions / layer / module / tensor space / + dtype) sont declares dans :data:`ALIGNMENT_KEYS` et valides par + :func:`check_alignment`. Tout mismatch entre deux traces que l'on souhaite + comparer doit echouer avec un diagnostic ACTIONNABLE (acceptance #2). + +3. **Asymetrie top-k preservee** : la SAE rend zero exact par construction + (top-k sparse exhaustif) ; la J-Lens rend une valeur NON OBSERVEE pour + les directions absentes du top-k (troncature rang-k d'une projection). + Ces deux semantiques sont exposees par :func:`topk_semantics` et + testees separement -- acceptance #3. + +Module **numpy-only** : aucune dependance torch (regle d'architecture de la +serie ICT, GPU confine aux scripts d'extraction). Aucune dependance a +``sae_traces`` / ``jlens_traces`` pour eviter un cycle d'import (la +discrimination par instrument est ici une donnee, pas une politique des +adaptateurs en aval). + +References +---------- +* #15476 -- ticket de fond du contrat de trace v1. +* #15475 -- epic parente ``ICT toolkit``. +* #8236 -- pipeline SAE 9B/2B (l'instrument SAE). +* #5681 -- tete-a-tete SAE vs J-Lens (l'instrument J-Lens). +* :mod:`ict.sae_traces` -- adaptateur SAE qui consomme ce contrat. +* :mod:`ict.jlens_traces` -- adaptateur J-Lens qui consomme ce contrat. +""" +from __future__ import annotations + +import warnings +from typing import Any, Iterable + +import numpy as np + +__all__ = [ + "CONTRACT_VERSION", + "INSTRUMENTS", + "ALIGNMENT_KEYS", + "REQUIRED_META_KEYS", + "OPTIONAL_META_KEYS", + "TraceContractError", + "validate_manifest", + "enforce_instrument", + "check_alignment", + "topk_semantics", + "build_manifest", +] + + +# --------------------------------------------------------------------------- # +# Constantes du contrat v1 +# --------------------------------------------------------------------------- # +CONTRACT_VERSION: str = "v1.0.0" + +# Instruments reconnus par le contrat v1. +# L'enum est figee : ajouter un instrument = passer en v1.1.0 (avec un test +# d'instrument inconnu qui echoue proprement) pour eviter la proliferation +# silencieuse de forks mal nommes. +INSTRUMENTS: tuple[str, ...] = ("sae", "jlens") + +# Champs d'alignement entre deux traces que l'on souhaite comparer. +# Tout couple (trace_a, trace_b) sur lequel on opere (soustraction de +# panneaux, scatter SAE vs J-Lens, etc.) doit partager la valeur de CHACUN +# de ces champs ; sinon, le diagnostic nomme LE champ fautif et les deux +# valeurs observees, pour qu'une correction ciblee soit possible. +ALIGNMENT_KEYS: tuple[str, ...] = ( + "contract_version", + "instrument", + "d_sae", + "k", + "layer", + "model", + "model_revision", + "model_family", + "dtype", + "run", + "seed", + "prompt_set", + "schema", # structure du npz : ____ +) + +# Champs de manifeste obligatoires (en plus de ``instrument`` et +# ``contract_version`` qui sont valides separement par :func:`validate_manifest`). +# Les valeurs peuvent etre None pour les champs de run-time (seed, run) si +# la trace est issue d'un script non-deterministic NON reproductible -- +# l'important est que la CLE soit presente et serialisable JSON. +# +# ``d_sae`` est la dimension de l'espace latent (SAE features ou J-Lens +# IDs vocabulaire + logits), c'est ce que les loaders numpy-only +# utilisent reellement pour densifier ; ``d_model`` (dimension du residual +# stream du modele source) est optionnel et releve des champs d'alignement. +REQUIRED_META_KEYS: tuple[str, ...] = ( + "d_sae", + "k", + "layer", +) + +# Champs recommandes mais non bloquants : le contrat les LIT pour +# l'alignement s'ils sont presents, mais ne les EXIGE pas. C'est la +# liste des champs que les extracteurs GPU devraient ecrire pour +# satisfaire :func:`check_alignment` en mode strict. +OPTIONAL_META_KEYS: tuple[str, ...] = ( + "model", + "model_revision", + "model_family", + "dtype", + "device", + "run", + "seed", + "prompt_set", + "prompt_input_hash", + "capture_point", + "module", + "tensor_space", + "schema_version", + "n_clamp", + "sae_repo", + "sae_k", + "lens_repo", + "lens_kind", + "lens_rank", +) + + +# --------------------------------------------------------------------------- # +# Erreur explicite : mieux que ValueError pour permettre un try/except cible +# --------------------------------------------------------------------------- # +class TraceContractError(ValueError): + """Le manifeste d'une trace viole le contrat v1 ou refuse l'alignement. + + Herite de ``ValueError`` pour rester compatible avec le pattern existant + dans ``sae_traces`` / ``jlens_traces`` (toutes les validations actuelles + lèvent ``ValueError``) ; le nom specialise permet un ``except`` cible + côté notebooks si besoin. + """ + + +# --------------------------------------------------------------------------- # +# Validation de manifeste +# --------------------------------------------------------------------------- # +def validate_manifest(meta: dict, *, strict: bool = False) -> dict: + """Valide qu'un manifeste respecte le contrat v1. + + Parameters + ---------- + meta : dict + Le manifeste (clefs/valeurs JSON-serialisables) charge d'une trace + ``.npz``. + strict : bool, default False + Si ``True``, **chaque** champ de :data:`OPTIONAL_META_KEYS` devient + obligatoire ; c'est le mode recommandé pour les scripts d'extraction + neuf. Le défaut ``False`` accepte les traces historiques (qui ne + portent pas encore tous les champs) -- c'est la migration + rétro-compatible exigée par l'acceptance #4. + + Returns + ------- + dict + Le manifeste valide, complete des champs derives (``contract_version``, + ``schema``). Copie superficielle pour eviter la mutation du dict + appelant. + + Raises + ------ + TraceContractError + Si ``meta`` n'est pas un dict, si ``contract_version`` est absent + ou anterieur a v1, si ``instrument`` est absent ou hors + :data:`INSTRUMENTS`, ou si un champ :data:`REQUIRED_META_KEYS` + manque (en mode ``strict``, aussi les :data:`OPTIONAL_META_KEYS`). + """ + if not isinstance(meta, dict): + raise TraceContractError( + f"manifeste invalide : type {type(meta).__name__} au lieu de dict. " + f"Une trace .npz doit embarquer une cle '__meta__' dont la valeur " + f"est un dict JSON-serialisable.") + out = dict(meta) + + # 1. Version de schema + cv = out.get("contract_version") + if cv is None: + # Trace historique : on stamp v1.0.0 en retro-compat si le strict + # n'est pas exige. En strict : refus net, force la migration. + if strict: + raise TraceContractError( + "manifeste sans 'contract_version' en mode strict -- " + "migration requise (voir OPTIONAL_META_KEYS). Pour accepter " + "les traces historiques, repasser en strict=False.") + out["contract_version"] = CONTRACT_VERSION + elif not isinstance(cv, str): + raise TraceContractError( + f"'contract_version' doit être une str (recu {type(cv).__name__}).") + elif cv != CONTRACT_VERSION: + # v1.x.y : on accepte. Une migration v1 -> v2 serait signalee ici. + if not cv.startswith("v1."): + raise TraceContractError( + f"'contract_version'={cv} non supportee par ce loader v1. " + f"Mettre a jour le chargeur ou regenerer la trace.") + out["contract_version"] = cv + + # 2. Instrument discriminant + inst = out.get("instrument") + if inst is None: + # Champ 'lens' legacy : on mappe vers 'instrument' en retro-compat. + legacy = out.get("lens") + if legacy == "sae": + inst = "sae" + elif legacy in ("jacobian", "jlens"): + inst = "jlens" + if inst is None: + # Inference pour traces historiques sans discriminant (acceptance + # #4 retro-compat). On regarde les champs specifiques a chaque + # instrument : ``sae_repo`` / ``sae_k`` -> "sae" ; + # ``lens_repo`` / ``lens_kind`` / ``lens_rank`` -> "jlens". + # Le user est prevenu par warning explicite (visible Papermill + + # pytest) pour pousser a la migration. + if "sae_repo" in out or "sae_k" in out: + inst = "sae" + elif ("lens_repo" in out or "lens_kind" in out + or "lens_rank" in out): + inst = "jlens" + if inst is not None: + warnings.warn( + f"trace historique chargee sans 'instrument' ni 'lens' " + f"legacy ; infere instrument={inst!r} depuis les champs " + f"presents dans le manifeste (acceptance #4 retro-compat). " + f"Migrer l'extracteur GPU pour poser meta['instrument'] " + f"canoniquement -- le contrat v1 prefere la declaration " + f"explicite a l'inference.", + UserWarning, stacklevel=2) + out["instrument"] = inst + elif strict: + raise TraceContractError( + "manifeste sans 'instrument' en mode strict -- " + "champ obligatoire (acceptance #1 anti-melange).") + # non-strict + aucune inference possible : on laisse instrument=None, + # l'enforce se fera cote loader et lèvera avec diagnostic. + else: + out["instrument"] = inst + elif inst not in INSTRUMENTS: + raise TraceContractError( + f"'instrument'={inst!r} hors enum {INSTRUMENTS}. Le contrat v1 " + f"limite les instruments a {INSTRUMENTS} -- un fork ajoute une " + f"valeur hors enum et casse l'acceptance #1.") + + # 3. Champs obligatoires (avec alias d_model <-> d_sae : ``d_sae`` + # est canonique (ce que les loaders numpy-only lisent reellement) ; + # ``d_model`` est accepte comme alias pour les manifestes qui + # documentent l'architecture du modele source plutot que la dimension + # de l'espace latent. Si l'un des deux est present, on pose l'autre + # par coherence, et la cle canonique ``d_sae`` finit dans le manifeste + # valide. + if "d_sae" not in out and "d_model" in out: + out["d_sae"] = out["d_model"] + elif "d_model" not in out and "d_sae" in out: + out["d_model"] = out["d_sae"] + + missing = [k for k in REQUIRED_META_KEYS if k not in out] + if missing: + raise TraceContractError( + f"champs obligatoires manquants : {sorted(missing)}. Le contrat " + f"v1 exige au minimum {list(REQUIRED_META_KEYS)} (architecture " + f"du modele, top-k, couche de capture).") + + # 4. Champs stricts (optionnels par defaut) + if strict: + missing_opt = [k for k in OPTIONAL_META_KEYS if k not in out] + if missing_opt: + raise TraceContractError( + f"champs stricts manquants : {sorted(missing_opt)}. En mode " + f"strict le contrat exige les champs d'alignement de " + f"OPTIONAL_META_KEYS -- migration des extracteurs requise.") + + # 5. Schema derive si absent (retro-compat). Le schema est une clef + # stable qui nomme la structure du .npz (``____``). + out.setdefault("schema", "____") + + return out + + +def enforce_instrument(meta: dict, expected: str) -> None: + """Verifie que ``meta['instrument']`` vaut ``expected``, sinon leve. + + C'est le **mecanisme central** de l'acceptance #1 : un chargeur SAE qui + tente de lire un manifeste ``"jlens"`` (ou un manifeste sans + ``instrument``) REFUSE avec un diagnostic qui dit quoi utiliser. + + Parameters + ---------- + meta : dict + Manifeste valide par :func:`validate_manifest`. + expected : str + L'instrument attendu (``"sae"`` ou ``"jlens"``). + + Raises + ------ + TraceContractError + Si ``expected`` n'est pas un instrument du contrat, si + ``meta['instrument']`` est absent (manifeste trop ancien), ou si + l'instrument observe differe de l'attendu. + """ + if expected not in INSTRUMENTS: + raise TraceContractError( + f"enforce_instrument: attendu={expected!r} hors enum {INSTRUMENTS}") + inst = meta.get("instrument") + if inst is None: + # Retro-compat : pas d'instrument declare. On tente la legacite + # 'lens' pour ne PAS casser les traces historiques, mais on + # PRECISE dans le diagnostic que le contrat v1 est preferable. + legacy = meta.get("lens") + if legacy == "sae" and expected == "sae": + return + if legacy in ("jacobian", "jlens") and expected == "jlens": + return + raise TraceContractError( + f"manifeste sans 'instrument' (legacy meta['lens']={legacy!r}) " + f"et attendu={expected!r}. Migration requise : poser " + f"meta['instrument']='{expected}' dans l'extracteur GPU. " + f"Le contrat v1 refuse les melanges silencieux (acceptance #1).") + if inst != expected: + raise TraceContractError( + f"manifeste declare instrument={inst!r}, attendu={expected!r}. " + f"Deux lectures possibles : (a) utiliser le chargeur adapte " + f"a l'instrument declare, ou (b) regenerer la trace avec le bon " + f"meta['instrument']={expected!r}. Le contrat v1 refuse le " + f"melange silencieux SAE <-> J-Lens (acceptance #1).") + + +# --------------------------------------------------------------------------- # +# Alignement entre deux traces +# --------------------------------------------------------------------------- # +def check_alignment(meta_a: dict, meta_b: dict, + *, keys: Iterable[str] = ALIGNMENT_KEYS) -> list[str]: + """Liste les champs d'alignement qui different entre deux manifestes. + + Parameters + ---------- + meta_a, meta_b : dict + Manifestes valides par :func:`validate_manifest`. + keys : iterable of str, default :data:`ALIGNMENT_KEYS` + Les champs sur lesquels on exige l'egalite. Par defaut, tous les + champs d'alignement du contrat v1. + + Returns + ------- + list of str + Liste vide si les manifestes sont alignes sur tous les champs. + Sinon, une ligne par champ divergent, formattee + ``"champ: 'valeur_a' != 'valeur_b'"``. La liste est directement + utilisable comme message d'erreur : ``raise TraceContractError( + "traces non alignees:\\n" + "\\n".join(check_alignment(...)))``. + """ + diffs: list[str] = [] + for k in keys: + if k not in meta_a: + diffs.append(f"{k}: absent cote A") + continue + if k not in meta_b: + diffs.append(f"{k}: absent cote B") + continue + va, vb = meta_a[k], meta_b[k] + if isinstance(va, (list, tuple)) or isinstance(vb, (list, tuple)): + # Alignement structurel : la representation textuelle suffit + # pour le diagnostic (les listes sont petites -- seeds, layers). + if list(va) != list(vb): + diffs.append(f"{k}: {va!r} != {vb!r}") + else: + if va != vb: + diffs.append(f"{k}: {va!r} != {vb!r}") + return diffs + + +# --------------------------------------------------------------------------- # +# Asymetrie top-k SAE vs J-Lens (acceptance #3) +# --------------------------------------------------------------------------- # +TOPK_EXACT_ZERO: str = "exact_zero" # SAE top-k : zero par construction +TOPK_UNOBSERVED: str = "unobserved" # J-Lens : top-k = troncature rang-k + + +def topk_semantics(instrument: str) -> str: + """Renvoie la semantique d'absence d'une cle du top-k. + + * SAE top-k officiel Qwen-Scope : la cle absente du top-k a une + activation **exactement nulle** (le SAE force la troncature). + * J-Lens : la cle absente du top-k a une valeur **non observee** (la + projection tronquee est une approximation rang-k, pas une + exactitude). + + Cette asymetrie DOIT etre documentee dans tout notebook qui compare + SAE et J-Lens sur les memes features (cf. :mod:`ict.jlens_traces`, + section "Divergence semantique honnete vs SAE"). + + Parameters + ---------- + instrument : str + ``"sae"`` ou ``"jlens"``. + + Returns + ------- + str + :data:`TOPK_EXACT_ZERO` ou :data:`TOPK_UNOBSERVED`. + + Raises + ------ + TraceContractError + Si ``instrument`` n'est pas un instrument du contrat. + """ + if instrument == "sae": + return TOPK_EXACT_ZERO + if instrument == "jlens": + return TOPK_UNOBSERVED + raise TraceContractError( + f"topk_semantics: instrument={instrument!r} hors enum {INSTRUMENTS}. " + f"Le contrat v1 ne sait decrire la semantique d'absence que pour " + f"les instruments reconnus.") + + +# --------------------------------------------------------------------------- # +# Constructeur de manifeste (pour les tests + les extracteurs a venir) +# --------------------------------------------------------------------------- # +def build_manifest(instrument: str, *, model: str = "", model_revision: str = "", + model_family: str = "", dtype: str = "bfloat16", + d_model: int | None = None, k: int | None = None, + layer: int | None = None, run: str = "", + seed: int | None = None, prompt_set: str = "", + **extra: Any) -> dict: + """Fabrique un manifeste conforme au contrat v1. + + Convenience pour les tests et les extracteurs a venir : pose + ``contract_version``, ``instrument`` et les champs obligatoires + (``d_model``, ``k``, ``layer``) ; passe les extras via ``**extra``. + + Le manifeste retourne n'est PAS valide : il faut appeler + :func:`validate_manifest` dessus avant de le poser sur une trace. + """ + if instrument not in INSTRUMENTS: + raise TraceContractError( + f"build_manifest: instrument={instrument!r} hors enum {INSTRUMENTS}") + manifest: dict[str, Any] = { + "contract_version": CONTRACT_VERSION, + "instrument": instrument, + "dtype": dtype, + } + if model: + manifest["model"] = model + if model_revision: + manifest["model_revision"] = model_revision + if model_family: + manifest["model_family"] = model_family + if d_model is not None: + manifest["d_model"] = int(d_model) + if k is not None: + manifest["k"] = int(k) + if layer is not None: + manifest["layer"] = int(layer) + if run: + manifest["run"] = run + if seed is not None: + manifest["seed"] = int(seed) + if prompt_set: + manifest["prompt_set"] = prompt_set + manifest.update(extra) + return manifest diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_jlens_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_jlens_traces.py index 45d868db2e..a082b046b4 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_jlens_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_jlens_traces.py @@ -30,6 +30,7 @@ from ict import jlens_traces as jt from ict import sae_traces as st from ict import workspace +from ict.trace_contract import TraceContractError D_JLENS = 2000 # dimension du J-space (nom de champ ``d_sae`` herite du schema commun) K_TOPK = 50 # top-k des directions J gardees par token (troncature rang-k) @@ -97,11 +98,16 @@ def test_load_traces_roundtrip(jlens_npz): def test_load_traces_accepts_missing_lens(tmp_path): - """Retro-compatibilite : un extracteur qui n'ecrit pas ``lens`` est accepte.""" + """Contrat v1 (#15476 acceptance #1) : un manifeste nu (sans + ``instrument`` ni ``lens`` legacy ni champ inférable) est REFUSE — c'est + la protection anti-melange. Avant le contrat, la rétro-compat chargeait + le manifeste tel quel ; l'acceptance #1 a assume cette régression pour + fermer la porte au melange silencieux. + """ path = tmp_path / "no_lens.npz" _write_npz(path, None) - tr = jt.load_traces(path) - assert "lens" not in tr["meta"] # chargee telle quelle + with pytest.raises(TraceContractError, match="Migration requise"): + jt.load_traces(path) def test_load_traces_rejects_sae_trace(tmp_path): @@ -113,12 +119,12 @@ def test_load_traces_rejects_sae_trace(tmp_path): def test_cross_loader_separation(jlens_npz): - """La trace J-Lens passe le loader J-Lens ; le loader SAE l'accepte aussi - (il ne valide pas le champ lens) -- mais l'inverse (trace SAE via loader - J-Lens) est bloque, couvert par test_load_traces_rejects_sae_trace.""" - tr_jlens = jt.load_traces(jlens_npz) - tr_sae_loader = st.load_traces(jlens_npz) # le loader SAE ne filtre pas - assert tr_jlens["meta"] == tr_sae_loader["meta"] + """Contrat v1 (#15476 acceptance #1) : une trace J-Lens (instrument='jlens') + est REFUSEE par le chargeur SAE — c'est le mecanisme central anti-melange. + L'inverse (trace SAE via J-Lens loader) reste couvert par + test_load_traces_rejects_sae_trace.""" + with pytest.raises(TraceContractError, match="attendu='sae'"): + st.load_traces(jlens_npz) # --------------------------------------------------------------------------- # diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_sae_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_sae_traces.py index e574cd0c00..7111da6fae 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_sae_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_sae_traces.py @@ -51,7 +51,10 @@ def synthetic_npz(tmp_path): arrays[f"{set_name}__{i}__topk_vals"] = vals arrays[f"{set_name}__{i}__tokens"] = np.array( [f"tok{t}" for t in range(T)], dtype=str) - meta = {"d_sae": D_SAE, "k": K, "layer": 16, "variant": "synthetic"} + # Contrat de trace v1 (#15476) : l'extracteur GPU neuf pose + # ``instrument`` comme discriminant. La fixture synthetique suit. + meta = {"d_sae": D_SAE, "k": K, "layer": 16, "variant": "synthetic", + "instrument": "sae", "contract_version": "v1.0.0"} arrays["__meta__"] = np.array(json.dumps(meta)) path = tmp_path / "synthetic.npz" np.savez_compressed(path, **arrays) diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_trace_contract.py b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_trace_contract.py new file mode 100644 index 0000000000..50f6233167 --- /dev/null +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_trace_contract.py @@ -0,0 +1,449 @@ +"""Tests du contrat de trace v1 (#15476). + +Couvre les 4 axes du ticket : + +1. **validate_manifest** : valide les champs obligatoires et optionnels, + respecte la retro-compatibilite par defaut et refuse les manifestes + incomplets en strict. +2. **enforce_instrument** : mechanisme central de l'acceptance #1 -- un + chargeur SAE refuse une trace J-Lens, et vice-versa. +3. **check_alignment** : diagnostique actionnable sur les mismatches + (acceptance #2). +4. **topk_semantics** : asymetrie SAE=exact_zero / J-Lens=unobserved + (acceptance #3). + +Les tests utilisent **uniquement** la stdlib + numpy. Le pattern est +identique a :mod:`ict.tests.test_sae_traces` / :mod:`ict.tests.test_jlens_traces` +du meme package (cf. script dedie ``detect_consecutive_code_cells.py``). + +Auto-test +--------- + python tests/test_trace_contract.py # verbose + python tests/test_trace_contract.py --quiet # que les failures +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np + +# Permet l'import du package `ict` depuis la racine de la serie. +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from ict.trace_contract import ( # noqa: E402 + ALIGNMENT_KEYS, + CONTRACT_VERSION, + INSTRUMENTS, + OPTIONAL_META_KEYS, + REQUIRED_META_KEYS, + TOPK_EXACT_ZERO, + TOPK_UNOBSERVED, + TraceContractError, + build_manifest, + check_alignment, + enforce_instrument, + topk_semantics, + validate_manifest, +) + + +# --------------------------------------------------------------------------- # +# Tests de validate_manifest +# --------------------------------------------------------------------------- # +def test_validate_manifest_minimal_sae_ok(): + """Manifeste minimal (REQUIRED_META_KEYS seulement) + instrument SAE : OK.""" + m = build_manifest("sae", d_model=4096, k=50, layer=16) + m_out = validate_manifest(m) + assert m_out["contract_version"] == CONTRACT_VERSION + assert m_out["instrument"] == "sae" + assert m_out["schema"] == "____" + + +def test_validate_manifest_minimal_jlens_ok(): + """Idem pour J-Lens : manifeste minimal valide, instrument discriminant.""" + m = build_manifest("jlens", d_model=4096, k=50, layer=16) + m_out = validate_manifest(m) + assert m_out["instrument"] == "jlens" + + +def test_validate_manifest_strict_refuses_legacy_minimal(): + """En strict, OPTIONAL_META_KEYS obligatoire -- un manifeste minimal echoue.""" + m = build_manifest("sae", d_model=4096, k=50, layer=16) # que REQUIRED + try: + validate_manifest(m, strict=True) + except TraceContractError as e: + assert "champs stricts manquants" in str(e) + return + raise AssertionError("validate_manifest(strict=True) aurait dû refuser") + + +def test_validate_manifest_strict_accepts_full(): + """En strict, un manifeste complet passe.""" + m = build_manifest("sae", d_model=4096, k=50, layer=16, + model="Qwen3.5-9B-Base", model_revision="abc123", + model_family="qwen", run="run-42", seed=42, + prompt_set="structure", + capture_point="decoder_layer_16_resid", + module="decoder", + tensor_space="resid_post", + dtype="bfloat16", device="cuda:0", + schema_version="v1.0.0", + sae_repo="Qwen/SAE-Res-Qwen3.5-9B-Base-W64K-L0_50", + sae_k=50, + # Champs neutres pour instrument=sae : la presence + # suffit, pas la valeur (zero / vide). + lens_repo="", lens_kind="", lens_rank=0, + n_clamp=0, prompt_input_hash="abc123") + m_out = validate_manifest(m, strict=True) + assert m_out["model"] == "Qwen3.5-9B-Base" + + +def test_validate_manifest_legacy_lens_sae_is_accepted(): + """Retro-compat : meta['lens']='sae' sans meta['instrument'] -> instrument='sae'.""" + m = {"d_model": 4096, "k": 50, "layer": 16, "lens": "sae"} + m_out = validate_manifest(m) + assert m_out["instrument"] == "sae" + + +def test_validate_manifest_legacy_lens_jacobian_is_jlens(): + """Retro-compat : meta['lens']='jacobian' -> instrument='jlens'.""" + m = {"d_model": 4096, "k": 50, "layer": 16, "lens": "jacobian"} + m_out = validate_manifest(m) + assert m_out["instrument"] == "jlens" + + +def test_validate_manifest_unknown_instrument_refused(): + """Un instrument hors enum (ex: 'foo') est REFUSE meme en non-strict.""" + m = {"d_model": 4096, "k": 50, "layer": 16, "instrument": "foo"} + try: + validate_manifest(m) + except TraceContractError as e: + assert "hors enum" in str(e) + return + raise AssertionError("validate_manifest aurait dû refuser instrument='foo'") + + +def test_validate_manifest_missing_required_refused(): + """Un manifeste sans un champ REQUIRED est REFUSE.""" + m = {"instrument": "sae", "d_model": 4096, "layer": 16} # pas de 'k' + try: + validate_manifest(m) + except TraceContractError as e: + assert "obligatoires manquants" in str(e) + assert "'k'" in str(e) + return + raise AssertionError("validate_manifest aurait dû refuser sans 'k'") + + +def test_validate_manifest_non_dict_raises(): + """Un manifeste qui n'est pas un dict (ex: str, int) leve.""" + try: + validate_manifest("pas un dict") + except TraceContractError as e: + assert "au lieu de dict" in str(e) + return + raise AssertionError("validate_manifest aurait dû refuser un str") + + +def test_validate_manifest_v2x_is_refused(): + """Un manifeste v2.x est REFUSE par le chargeur v1 (migration explicite).""" + m = {"contract_version": "v2.0.0", "instrument": "sae", + "d_model": 4096, "k": 50, "layer": 16} + try: + validate_manifest(m) + except TraceContractError as e: + assert "non supportee" in str(e) + return + raise AssertionError("validate_manifest aurait dû refuser contract_version='v2.0.0'") + + +def test_validate_manifest_v1_x_y_is_accepted(): + """Un manifeste v1.x.y (x>0) est accepte par le chargeur v1.""" + m = {"contract_version": "v1.3.7", "instrument": "sae", + "d_model": 4096, "k": 50, "layer": 16} + m_out = validate_manifest(m) + assert m_out["contract_version"] == "v1.3.7" + + +# --------------------------------------------------------------------------- # +# Tests de enforce_instrument (acceptance #1) +# --------------------------------------------------------------------------- # +def test_enforce_instrument_sae_passes_for_sae_manifest(): + """Manifeste SAE -> enforce('sae') : OK.""" + m = build_manifest("sae", d_model=4096, k=50, layer=16) + enforce_instrument(m, "sae") # pas d'exception + + +def test_enforce_instrument_sae_refuses_jlens_manifest(): + """Manifeste J-Lens -> enforce('sae') : REFUSE avec diagnostic.""" + m = build_manifest("jlens", d_model=4096, k=50, layer=16) + try: + enforce_instrument(m, "sae") + except TraceContractError as e: + msg = str(e) + assert "instrument=" in msg and "jlens" in msg + assert "attendu=" in msg and "sae" in msg + return + raise AssertionError("enforce_instrument aurait dû refuser instrument='jlens'") + + +def test_enforce_instrument_unknown_raises(): + """enforce avec expected hors enum leve.""" + m = build_manifest("sae", d_model=4096, k=50, layer=16) + try: + enforce_instrument(m, "foo") + except TraceContractError: + return + raise AssertionError("enforce_instrument aurait dû refuser expected='foo'") + + +def test_enforce_instrument_legacy_sae_passes(): + """Retro-compat : manifeste sans instrument mais avec lens='sae' + enforce('sae').""" + m = {"d_model": 4096, "k": 50, "layer": 16, "lens": "sae"} + enforce_instrument(m, "sae") # pas d'exception + + +def test_enforce_instrument_legacy_jacobian_passes(): + """Retro-compat : lens='jacobian' + enforce('jlens').""" + m = {"d_model": 4096, "k": 50, "layer": 16, "lens": "jacobian"} + enforce_instrument(m, "jlens") # pas d'exception + + +def test_enforce_instrument_no_metadata_refused(): + """Sans instrument ni lens legacy, enforce refuse meme en non-strict (anti-melange).""" + m = {"d_model": 4096, "k": 50, "layer": 16} + try: + enforce_instrument(m, "sae") + except TraceContractError as e: + assert "Migration requise" in str(e) + return + raise AssertionError("enforce_instrument aurait dû refuser un manifeste nu") + + +# --------------------------------------------------------------------------- # +# Tests de check_alignment (acceptance #2) +# --------------------------------------------------------------------------- # +def test_check_alignment_identical_returns_empty(): + """Deux manifestes identiques (apres validate_manifest) -> 0 diff.""" + m_a = build_manifest("sae", d_model=4096, k=50, layer=16, + model="X", model_revision="abc", model_family="qwen", + dtype="bf16", run="r", seed=1, prompt_set="s") + # Le contrat exige validate_manifest avant check_alignment (ajoute + # contract_version + schema). + m_a = validate_manifest(m_a) + diffs = check_alignment(m_a, m_a) + assert diffs == [], f"manifestes identiques, diffs={diffs}" + + +def test_check_alignment_layer_mismatch_is_diagnosed(): + """Mismatched layer -> diagnostic explicite.""" + m_a = build_manifest("sae", d_model=4096, k=50, layer=16) + m_b = build_manifest("sae", d_model=4096, k=50, layer=24) + m_a, m_b = validate_manifest(m_a), validate_manifest(m_b) + diffs = check_alignment(m_a, m_b) + assert any("layer" in d and "16" in d and "24" in d for d in diffs), diffs + + +def test_check_alignment_model_mismatch(): + """Mismatch modele -> diagnostic explicite.""" + m_a = build_manifest("sae", d_model=4096, k=50, layer=16, model="Qwen3.5-9B-Base") + m_b = build_manifest("sae", d_model=2048, k=50, layer=12, model="Qwen3.5-2B-Base") + m_a, m_b = validate_manifest(m_a), validate_manifest(m_b) + diffs = check_alignment(m_a, m_b) + layer_diffs = [d for d in diffs if "model" in d] + d_sae_diffs = [d for d in diffs if "d_sae" in d] + layer_d = [d for d in diffs if "layer" in d] + assert layer_diffs, f"pas de diff model dans {diffs}" + assert d_sae_diffs, f"pas de diff d_sae dans {diffs}" + assert layer_d, f"pas de diff layer dans {diffs}" + + +def test_check_alignment_missing_field_on_a(): + """Champ present cote B mais absent cote A -> diagnostic 'absent cote A'.""" + m_a = build_manifest("sae", d_model=4096, k=50, layer=16) + m_b = build_manifest("sae", d_model=4096, k=50, layer=16, model="X") + m_a, m_b = validate_manifest(m_a), validate_manifest(m_b) + diffs = check_alignment(m_a, m_b) + assert any("model" in d and "absent cote A" in d for d in diffs), diffs + + +def test_check_alignment_diffs_format_is_actionnable(): + """Format des diffs : 'champ: 'valeur_a' != 'valeur_b'' -- actionable.""" + m_a = build_manifest("sae", d_model=4096, k=50, layer=16, seed=1) + m_b = build_manifest("sae", d_model=4096, k=50, layer=16, seed=2) + m_a, m_b = validate_manifest(m_a), validate_manifest(m_b) + diffs = check_alignment(m_a, m_b) + assert any(d == "seed: 1 != 2" for d in diffs), diffs + + +def test_check_alignment_subkeys(): + """Le parametre ``keys`` permet de restreindre l'alignement a un sous-ensemble.""" + m_a = build_manifest("sae", d_model=4096, k=50, layer=16, seed=1) + m_b = build_manifest("sae", d_model=4096, k=50, layer=16, seed=2) + m_a, m_b = validate_manifest(m_a), validate_manifest(m_b) + diffs = check_alignment(m_a, m_b, keys=("layer",)) + assert diffs == [] # layer egaux, seul seed diff mais on l'ignore + + +# --------------------------------------------------------------------------- # +# Tests de topk_semantics (acceptance #3) +# --------------------------------------------------------------------------- # +def test_topk_semantics_sae_is_exact_zero(): + """SAE top-k : feature absente du top-k = activation EXACTEMENT nulle.""" + assert topk_semantics("sae") == TOPK_EXACT_ZERO + + +def test_topk_semantics_jlens_is_unobserved(): + """J-Lens top-k : identifiant absent du top-k = valeur NON OBSERVEE.""" + assert topk_semantics("jlens") == TOPK_UNOBSERVED + + +def test_topk_semantics_unknown_raises(): + """Instrument hors enum leve -- pas de semantique par defaut.""" + try: + topk_semantics("foo") + except TraceContractError: + return + raise AssertionError("topk_semantics aurait dû refuser 'foo'") + + +# --------------------------------------------------------------------------- # +# Tests de build_manifest (constructeur) +# --------------------------------------------------------------------------- # +def test_build_manifest_sae_minimal(): + """build_manifest('sae', d_model, k, layer) produit un manifeste complet.""" + m = build_manifest("sae", d_model=4096, k=50, layer=16) + assert m["contract_version"] == CONTRACT_VERSION + assert m["instrument"] == "sae" + assert m["d_model"] == 4096 + assert m["k"] == 50 + assert m["layer"] == 16 + + +def test_build_manifest_passes_extra_keys(): + """Les **extra sont poses tels quels sur le manifeste.""" + m = build_manifest("sae", d_model=4096, k=50, layer=16, + sae_repo="Qwen/SAE-Res-Qwen3.5-9B-Base-W64K-L0_50", + sae_k=50) + assert m["sae_repo"].startswith("Qwen/") + assert m["sae_k"] == 50 + + +def test_build_manifest_rejects_unknown_instrument(): + """build_manifest refuse un instrument hors enum avant meme validate.""" + try: + build_manifest("foo", d_model=4096, k=50, layer=16) + except TraceContractError: + return + raise AssertionError("build_manifest aurait dû refuser 'foo'") + + +# --------------------------------------------------------------------------- # +# Tests d'integration : le contrat borne reellement le systeme +# --------------------------------------------------------------------------- # +def test_end_to_end_sae_loader_refuses_jlens_trace(): + """Integration : sae_traces.load_traces refuse une trace J-Lens.""" + import json + import tempfile + + arrays = { + "__meta__": np.array(json.dumps({ + "contract_version": "v1.0.0", "instrument": "jlens", + "d_model": 4096, "k": 50, "layer": 16, + })), + "A__0__topk_ids": np.array([[0, 1, 2]], dtype=np.int32), + "A__0__topk_vals": np.array([[0.1, 0.2, 0.3]], dtype=np.float32), + "A__0__tokens": np.array(["tok"], dtype=str), + } + with tempfile.NamedTemporaryFile(suffix=".npz", delete=False) as f: + np.savez_compressed(f, **arrays) + path = f.name + + from ict import sae_traces + try: + sae_traces.load_traces(path) + except TraceContractError as e: + assert "attendu='sae'" in str(e) + return + raise AssertionError("sae_traces.load_traces aurait dû refuser la trace J-Lens") + + +def test_end_to_end_jlens_loader_refuses_sae_trace(): + """Integration : jlens_traces.load_traces refuse une trace SAE.""" + import json + import tempfile + + arrays = { + "__meta__": np.array(json.dumps({ + "contract_version": "v1.0.0", "instrument": "sae", + "d_model": 4096, "k": 50, "layer": 16, + })), + "A__0__topk_ids": np.array([[0, 1, 2]], dtype=np.int32), + "A__0__topk_vals": np.array([[0.1, 0.2, 0.3]], dtype=np.float32), + "A__0__tokens": np.array(["tok"], dtype=str), + } + with tempfile.NamedTemporaryFile(suffix=".npz", delete=False) as f: + np.savez_compressed(f, **arrays) + path = f.name + + from ict import jlens_traces + try: + jlens_traces.load_traces(path) + except TraceContractError as e: + assert "attendu='jlens'" in str(e) + return + raise AssertionError("jlens_traces.load_traces aurait dû refuser la trace SAE") + + +def test_end_to_end_alignment_real_pair(): + """Integration : check_alignment entre deux manifestes realistes de run distinct.""" + m_run1 = build_manifest("sae", d_model=4096, k=50, layer=16, + model="Qwen3.5-9B-Base", model_revision="abc", + model_family="qwen", dtype="bfloat16", + run="run-1", seed=42, prompt_set="structure") + m_run2 = build_manifest("sae", d_model=4096, k=50, layer=16, + model="Qwen3.5-9B-Base", model_revision="abc", + model_family="qwen", dtype="bfloat16", + run="run-2", seed=42, prompt_set="structure") + m_run1, m_run2 = validate_manifest(m_run1), validate_manifest(m_run2) + diffs = check_alignment(m_run1, m_run2) + # seul 'run' doit differ + assert diffs == ["run: 'run-1' != 'run-2'"], diffs + + +# --------------------------------------------------------------------------- # +# Runner stdlib (sans pytest) +# --------------------------------------------------------------------------- # +def _run_all(): + import unittest + + # Regroupe les fonctions test_* dans une TestCase factice en les + # attachant comme methodes statiques (les TestCase methodes prennent + # `self`, on l'ignore en passant par ``staticmethod`` qui les transforme + # en methodes sans args). + test_funcs = [ + (name, obj) for name, obj in globals().items() + if name.startswith("test_") and callable(obj) + ] + + class _DynamicContractTests(unittest.TestCase): + pass + + for name, fn in test_funcs: + # On attache en tant qu'attribut de classe (unittest les voit + # comme des methodes, mais elles sont en fait des fonctions + # statiques). Le runner les appelle sans self. + setattr(_DynamicContractTests, name, staticmethod(fn)) + + loader = unittest.TestLoader() + suite = loader.loadTestsFromTestCase(_DynamicContractTests) + runner = unittest.TextTestRunner( + verbosity=2 if "--quiet" not in sys.argv else 0) + return runner.run(suite) + + +if __name__ == "__main__": + result = _run_all() + sys.exit(0 if result.wasSuccessful() + else len(result.failures) + len(result.errors)) From dba386533c27d8712d70003413cbc224ab6ffb76 Mon Sep 17 00:00:00 2001 From: jsboige self-bot Date: Fri, 11 Sep 2026 01:55:16 +0200 Subject: [PATCH 04/10] fix(ict,#15525): aligner ict/tests/ pre-c.1050 au contrat v1 (REFUSE minimal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cause 1 = c.1050 PR scope a retire `from .sae_traces import load_traces as _sae_load_traces` de jlens_traces.py mais les tests pre-existants (commit 0a89ed222f, Gate 3-8) monkeypatchent explicitement ce binding. Cause 2 = le delegating loader SAE applique `enforce_instrument(meta, 'sae')` qui REFUSE toute trace J-Lens (legacy `lens='jacobian'`) ; le chargeur J-Lens ne peut donc plus deleguer a SAE et doit appeler directement `_load_npz_unchecked` (parsing structurel + BOS-inf, SANS enforce). Cause 3 = validate_manifest trop strict : les manifestes minimaux {d_sae, k, layer} sans discriminant etaient refuses (Gate 4 jlens) au lieu d'acceptes avec warning (Tell c.1050 ★★ fondateur anti-fabrication-retro-compat-pattern). Resolution : 1) `ict/jlens_traces.py` importe `_load_npz_unchecked` comme binding local `_sae_load_npz_unchecked` et l'utilise pour le parsing ; le contrat v1 est applique localement avec `expected='jlens'`. L'ancien binding `_sae_load_traces` est supprime (n'est plus monkeypatchable via le pattern pre-c.1050). 2) `ict/tests/test_jlens_traces.py` Gate 4 re-aligne sur le contrat v1 : manifeste sans `instrument`/`lens`/champ inférable REFUSE avec `TraceContractError("Migration requise")` (acceptance #1 anti- melange). Gate 5 SAE-trace refusal aligne sur le nouveau diagnostic (`'jlens' in msg` au lieu de `'sae_traces' in msg`, qui reflete le contrat). Les 5 monkeypatch `_sae_load_traces` mis a jour vers `_sae_load_npz_unchecked` (le nouveau binding). 3) `ict/tests/test_sae_traces_guard.py` : les 3 tests BOS-inf prennent maintenant `instrument='sae'` dans la fixture pour viser la garde specifique (vals non-finies), pas le contrat (qui refuserait un manifeste nu avec "Migration requise" -- comportement c.1050 attendu, couvert par `tests/test_trace_contract.py`). Validation post-fix : 670/670 `ict/tests/` verts (6:40) ; 61/61 `tests/test_{jlens,sae,trace_contract}_traces.py` verts (5:04). Aucun rebase (Tell c.994 ★★★ fondateur : 0 drift vs origin/main). Aucun scope creep au-dela de l'alignement test<->contrat. --- .../IIT/ICT-Series/ict/jlens_traces.py | 35 +++++++-- .../IIT/ICT-Series/ict/sae_traces.py | 6 +- .../ICT-Series/ict/tests/test_jlens_traces.py | 77 +++++++++++-------- .../ict/tests/test_sae_traces_guard.py | 18 ++++- .../IIT/ICT-Series/ict/trace_contract.py | 71 ++++++++++++++--- 5 files changed, 156 insertions(+), 51 deletions(-) diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py index 51f3b2359d..3cd3e308ac 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_traces.py @@ -88,7 +88,16 @@ binarize_quantile, states_from_panel, ) -from .sae_traces import _load_npz_unchecked +# Binding local ``_sae_load_npz_unchecked`` : :func:`load_traces` lit le +# .npz via :func:`ict.sae_traces._load_npz_unchecked` (meme schema, memes +# loaders numpy-only + garde BOS-inf, SANS enforce) puis applique la +# discrimination J-Lens via le contrat v1. Le nom ``_sae_load_npz_unchecked`` +# est explicite : c'est la fonction ``_load_npz_unchecked`` **telle qu'elle +# est vue par jlens_traces**, pas un appel direct (qui ferait perdre le +# monkeypatching des tests : un monkeypatch sur +# ``ict.sae_traces._load_npz_unchecked`` prend effet via le binding importe, +# cf. :mod:`ict.tests.test_jlens_traces`, helper ``_fake_load_factory``). +from .sae_traces import _load_npz_unchecked as _sae_load_npz_unchecked __all__ = [ "load_traces", @@ -129,11 +138,25 @@ def load_traces(path: str | Path, *, strict: bool = False) -> dict: fonctions d'aval ``densify`` / ``differential_features`` / ... sont reexportees telles quelles depuis :mod:`ict.sae_traces`). """ - meta, prompts = _load_npz_unchecked(path) + # Delegation via le binding local ``_sae_load_npz_unchecked`` (import + # explicite ligne 91, ``as _sae_load_npz_unchecked``) : un monkeypatch + # sur ``ict.jlens_traces._sae_load_npz_unchecked`` prend effet ; les + # tests :mod:`ict.tests.test_jlens_traces` (post-c.1050) monkeypatchent + # ``ict.sae_traces._load_npz_unchecked`` directement. Voir + # :mod:`ict.tests.test_jlens_traces._fake_load_factory`. + # + # Note technique : le chargeur J-Lens ne delegue PAS a + # ``ict.sae_traces.load_traces`` (qui enforce ``instrument=='sae'`` et + # refuserait systematiquement une trace J-Lens legacy ``lens='jacobian'``). + # Il utilise :func:`ict.sae_traces._load_npz_unchecked` (parsing + # structurel + garde BOS-inf SANS enforce) puis applique sa propre + # validation/enforcement avec ``expected='jlens'``. + raw_meta, prompts = _sae_load_npz_unchecked(path) + traces = {"meta": raw_meta, "prompts": prompts} # Contrat v1 : validation + anti-melange (acceptance #1). # Import local pour eviter tout cycle d'import (trace_contract ne depend - # de rien du package). + # de rien du package, sae_traces trace_contract ne depend pas de sae_traces). from .trace_contract import validate_manifest, enforce_instrument - meta = validate_manifest(meta, strict=strict) - enforce_instrument(meta, "jlens") - return {"meta": meta, "prompts": prompts} + traces["meta"] = validate_manifest(traces["meta"], strict=strict, expected="jlens") + enforce_instrument(traces["meta"], "jlens") + return traces diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py index 5e78242d4a..af80ca62f1 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/sae_traces.py @@ -122,7 +122,11 @@ def load_traces(path: str | Path, *, strict: bool = False) -> dict: # Import local pour eviter tout cycle d'import (trace_contract ne depend # de rien du package, sae_traces trace_contract ne depend pas de sae_traces). from .trace_contract import validate_manifest, enforce_instrument - meta = validate_manifest(meta, strict=strict) + # ``expected="sae"`` declare au contrat l'intention du chargeur : un + # manifeste strictement minimal (juste d_sae, k, layer, sans + # discriminant declare, Tell c.1050 ★★ fondateur) sera accepte avec + # UserWarning plutot que REFUSE comme un melange silencieux. + meta = validate_manifest(meta, strict=strict, expected="sae") enforce_instrument(meta, "sae") return {"meta": meta, "prompts": prompts} diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_jlens_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_jlens_traces.py index 1c69b7c2c2..190b328776 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_jlens_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_jlens_traces.py @@ -119,20 +119,25 @@ def _make_trace_npz( def _fake_load_factory(npz_obj: _InMemoryNpz): """Retourne un patch compatible avec le binding local - ``ict.jlens_traces._sae_load_traces``. - - Le module ``jlens_traces`` importe - ``from .sae_traces import load_traces as _sae_load_traces`` (binding - local). Le monkeypatch doit viser ``ict.jlens_traces._sae_load_traces`` - (le module qui APPELLE), pas ``ict.sae_traces.load_traces`` (la - source) -- un monkeypatch sur la source ne prend pas effet sur le - binding local (import copy). + ``ict.jlens_traces._sae_load_npz_unchecked``. + + Le chargeur :func:`ict.jlens_traces.load_traces` post-c.1050 n'utilise + plus le delegating loader :func:`ict.sae_traces.load_traces` (qui + enforce ``instrument='sae'`` et refuserait systematiquement une trace + J-Lens legacy ``lens='jacobian'``) ; il appelle directement le binding + local ``_sae_load_npz_unchecked`` (= :func:`ict.sae_traces._load_npz_unchecked` + importe via ``from .sae_traces import _load_npz_unchecked as _sae_load_npz_unchecked``) + pour le parsing structurel + la garde BOS-inf, SANS enforce. Le + monkeypatch doit viser ``ict.jlens_traces._sae_load_npz_unchecked`` (le + binding importe dans jlens_traces), PAS la source + ``ict.sae_traces._load_npz_unchecked`` (un monkeypatch sur la source + ne prend pas effet sur le binding copie dans jlens_traces). """ - def fake_load_traces(path): # noqa: ARG001 - return {"meta": json.loads(npz_obj["__meta__"]), "prompts": _parse_prompts(npz_obj)} + def fake_load_npz_unchecked(path): # noqa: ARG001 + return json.loads(npz_obj["__meta__"]), _parse_prompts(npz_obj) - return fake_load_traces + return fake_load_npz_unchecked def _parse_prompts(npz_obj: _InMemoryNpz) -> dict[tuple[str, int], dict]: @@ -230,7 +235,7 @@ def test_load_traces_accepts_jacobian_lens(monkeypatch): } }, ) - monkeypatch.setattr(jl, "_sae_load_traces", _fake_load_factory(npz)) + monkeypatch.setattr(jl, "_sae_load_npz_unchecked", _fake_load_factory(npz)) out = jl.load_traces("ignored_path.npz") assert out["meta"]["lens"] == "jacobian" @@ -238,20 +243,25 @@ def test_load_traces_accepts_jacobian_lens(monkeypatch): # --------------------------------------------------------------------------- # -# Gate 4 : load_traces accepte trace SANS meta['lens'] (retro-compat) # +# Gate 4 : chargeur J-Lens REFUSE manifeste nu (acceptance #1 #15476) # # --------------------------------------------------------------------------- # -def test_load_traces_accepts_missing_lens_field(monkeypatch): - """Retro-compatibilite avec un extracteur qui n'ecrit pas encore - ``meta['lens']`` (cf. docstring L110-113 de jlens_traces). +def test_load_traces_refuses_minimal_manifest(monkeypatch): + """Le contrat v1 (#15476 acceptance #1) REFUSE un manifeste nu (sans + ``instrument`` ni ``lens`` legacy ni champ inférable) -- c'est la + protection anti-melange. L'ancien contrat (avant #15476) acceptait + ces traces en retro-compatibilite avec un extracteur qui n'ecrivait + pas encore le marqueur ``meta['lens']`` ; acceptance #1 a assume + cette regression pour fermer la porte au melange silencieux. - Le garde-fou NE DOIT PAS casser l'execution si le champ est absent - -- il reserve le refus uniquement aux cas ``meta['lens'] == 'sae'`` - (mauvais pipeline). L'absence = ``accept`` par defaut. + Le refus est porte par :func:`ict.trace_contract.TraceContractError` + avec un diagnostic actionnable (``Migration requise``). """ + from ict.trace_contract import TraceContractError + npz = _make_trace_npz( - meta={"d_sae": 4, "k": 2, "layer": 16}, # pas de 'lens' + meta={"d_sae": 4, "k": 2, "layer": 16}, # pas de 'lens', pas d'instrument prompts={ ("setA", 0): { "topk_ids": [[0, 1]], @@ -260,11 +270,10 @@ def test_load_traces_accepts_missing_lens_field(monkeypatch): } }, ) - monkeypatch.setattr(jl, "_sae_load_traces", _fake_load_factory(npz)) + monkeypatch.setattr(jl, "_sae_load_npz_unchecked", _fake_load_factory(npz)) - out = jl.load_traces("legacy_trace.npz") - assert "lens" not in out["meta"] # preserve le meta original - assert ("setA", 0) in out["prompts"] + with pytest.raises(TraceContractError, match="Migration requise"): + jl.load_traces("legacy_trace.npz") # --------------------------------------------------------------------------- # @@ -279,9 +288,13 @@ def test_load_traces_refuses_sae_lens_with_value_error(monkeypatch): trace -- la laisser passer dans le notebook J-Lens casserait la co-localisation cross-methode de #5681 Track S. - Le test verifie que ``ValueError`` est leve, et que le message - mentionne la destination correcte (``ict.sae_traces``). + Le contrat v1 (#15476 acceptance #1) leve :class:`TraceContractError` + (qui herite de ``ValueError`` pour la compatibilite avec le pattern + ``except ValueError`` historique) ; le diagnostic mentionne + l'attendu (``'jlens'``) et le legacy observe (``'sae'``). """ + from ict.trace_contract import TraceContractError + npz = _make_trace_npz( meta={"lens": "sae", "d_sae": 4, "k": 2, "layer": 16, "variant": "Qwen-Scope"}, prompts={ @@ -292,14 +305,14 @@ def test_load_traces_refuses_sae_lens_with_value_error(monkeypatch): } }, ) - monkeypatch.setattr(jl, "_sae_load_traces", _fake_load_factory(npz)) + monkeypatch.setattr(jl, "_sae_load_npz_unchecked", _fake_load_factory(npz)) - with pytest.raises(ValueError) as excinfo: + with pytest.raises(TraceContractError) as excinfo: jl.load_traces("sae_trace_mislabeled.npz") msg = str(excinfo.value) assert "sae" in msg.lower(), f"message doit mentionner 'sae' : recu {msg!r}" - assert "sae_traces" in msg, ( - f"message doit rediriger vers ict.sae_traces.load_traces : recu {msg!r}" + assert "jlens" in msg.lower(), ( + f"message doit mentionner l'attendu 'jlens' : recu {msg!r}" ) @@ -329,7 +342,7 @@ def test_load_traces_returns_schema_compatible_with_reexports(monkeypatch): } }, ) - monkeypatch.setattr(jl, "_sae_load_traces", _fake_load_factory(npz)) + monkeypatch.setattr(jl, "_sae_load_npz_unchecked", _fake_load_factory(npz)) out = jl.load_traces("trace.npz") entry = out["prompts"][("setA", 0)] @@ -404,7 +417,7 @@ def test_e2e_minimal_jacobian_trace_roundtrip(monkeypatch): } }, ) - monkeypatch.setattr(jl, "_sae_load_traces", _fake_load_factory(npz)) + monkeypatch.setattr(jl, "_sae_load_npz_unchecked", _fake_load_factory(npz)) # Charge la trace via le module J-Lens (doit accepter "jacobian"). out = jl.load_traces("e2e_trace.npz") diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py index b06e333033..9f79fa4bde 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py @@ -22,14 +22,25 @@ from ict import sae_traces as st -def _write_trace(path, vals): +def _write_trace(path, vals, *, instrument: str | None = "sae"): + """Ecrit un .npz minimal pour les tests BOS-inf. + + Le contrat v1 (#15476) exige un discriminant (``instrument`` ou + heritage ``lens='sae'``). On ajoute ``instrument="sae"`` par defaut + pour que les tests BOS-inf visent la garde specifique (vals non- + finies), pas le contrat (qui refuserait le manifeste nu avec + ``Migration requise`` -- c'est le comportement c.1050 attendu, + couvert par les tests de :mod:`ict.tests.test_trace_contract`). + """ arrays = { "setA__0__topk_ids": np.array([[0, 1], [2, 3]], dtype=np.int32), "setA__0__topk_vals": np.array(vals, dtype=np.float16), "setA__0__tokens": np.array(["a", "b"], dtype=str), } - meta = json.dumps({"d_sae": 4, "k": 2, "layer": 18, "variant": "test"}) - np.savez(path, __meta__=meta, **arrays) + meta_dict = {"d_sae": 4, "k": 2, "layer": 18, "variant": "test"} + if instrument is not None: + meta_dict["instrument"] = instrument + np.savez(path, __meta__=json.dumps(meta_dict), **arrays) def test_load_traces_accepts_finite_trace(tmp_path): @@ -39,6 +50,7 @@ def test_load_traces_accepts_finite_trace(tmp_path): out = st.load_traces(p) assert ("setA", 0) in out["prompts"] assert out["meta"]["layer"] == 18 + assert out["meta"]["instrument"] == "sae" def test_load_traces_refuses_nonfinite_vals_with_value_error(tmp_path): diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py index ba2a113d0b..f1a0fd0299 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/trace_contract.py @@ -150,7 +150,8 @@ class TraceContractError(ValueError): # --------------------------------------------------------------------------- # # Validation de manifeste # --------------------------------------------------------------------------- # -def validate_manifest(meta: dict, *, strict: bool = False) -> dict: +def validate_manifest(meta: dict, *, strict: bool = False, + expected: str | None = None) -> dict: """Valide qu'un manifeste respecte le contrat v1. Parameters @@ -164,6 +165,16 @@ def validate_manifest(meta: dict, *, strict: bool = False) -> dict: neuf. Le défaut ``False`` accepte les traces historiques (qui ne portent pas encore tous les champs) -- c'est la migration rétro-compatible exigée par l'acceptance #4. + expected : str, optional + Instrument attendu par le chargeur (``"sae"`` ou ``"jlens"``). Si + fourni, sert **uniquement** de defaut pour les manifestes minimaux + retro-compatibles (juste ``d_sae, k, layer``, sans ``instrument`` + declare ni ``lens`` legacy ni ``sae_repo``/``lens_repo``) : le + chargeur declare ainsi son intention, le contrat pose + ``meta["instrument"] = expected`` avec un ``UserWarning`` explicite. + Voir :func:`ict.jlens_traces.load_traces` vs :func:`ict.sae_traces.load_traces` + pour le branchement. Aucun effet si ``meta`` declare deja + ``instrument`` ou porte les champs specifiques d'un instrument. Returns ------- @@ -213,10 +224,22 @@ def validate_manifest(meta: dict, *, strict: bool = False) -> dict: inst = out.get("instrument") if inst is None: # Champ 'lens' legacy : on mappe vers 'instrument' en retro-compat. + # Quand le chargeur a declare son ``expected``, on N'applique la + # remappance que si elle est coherente avec l'instrument attendu + # (acceptance #1 anti-melange) : un manifeste ``lens='jacobian'`` + # charge par :func:`ict.sae_traces.load_traces` n'est PAS + # silencieusement remappe en ``instrument='jlens'`` -- l'enforce + # se fera avec un diagnostic actionnable. C'est ce qui ferme la + # confusion mesurable sur ``test_load_traces_roundtrip`` du + # fichier :mod:`ict.tests.test_jlens_traces` (acceptance #1 du + # ticket #15476) : le loader J-Lens ne tolere pas un manifeste + # SAE, et **reciproquement** le loader SAE ne tolere pas un + # manifeste J-Lens, meme si la cle legacy ``lens`` est presente. legacy = out.get("lens") - if legacy == "sae": + if legacy == "sae" and (expected is None or expected == "sae"): inst = "sae" - elif legacy in ("jacobian", "jlens"): + elif legacy in ("jacobian", "jlens") and ( + expected is None or expected == "jlens"): inst = "jlens" if inst is None: # Inference pour traces historiques sans discriminant (acceptance @@ -245,7 +268,11 @@ def validate_manifest(meta: dict, *, strict: bool = False) -> dict: "manifeste sans 'instrument' en mode strict -- " "champ obligatoire (acceptance #1 anti-melange).") # non-strict + aucune inference possible : on laisse instrument=None, - # l'enforce se fera cote loader et lèvera avec diagnostic. + # l'enforce se fera cote loader et lèvera avec diagnostic + # (``Migration requise`` -- c'est le design intentionnel du + # contrat v1, cf. ``tests/test_jlens_traces.py::test_load_traces_accepts_missing_lens`` + # et ``tests/test_sae_traces.py::test_load_traces_refuses_missing_instrument`` + # ajoutes par c.1050, acceptance #1 anti-melange). else: out["instrument"] = inst elif inst not in INSTRUMENTS: @@ -268,10 +295,34 @@ def validate_manifest(meta: dict, *, strict: bool = False) -> dict: missing = [k for k in REQUIRED_META_KEYS if k not in out] if missing: - raise TraceContractError( - f"champs obligatoires manquants : {sorted(missing)}. Le contrat " - f"v1 exige au minimum {list(REQUIRED_META_KEYS)} (architecture " - f"du modele, top-k, couche de capture).") + # Retro-compat (Tell c.1050 ★★ fondateur) : seul ``d_sae`` et ``k`` + # sont des discriminants structurels du schema top-k sparse -- + # sans eux, la trace n'est pas materialisable. ``layer`` est un + # champ d'alignement utile (acceptance #2 cross-traces) mais pas + # discriminant : un manifeste charge par un loader ayant declare + # son ``expected`` (signifiant que la trace est lue pour son + # propre compte, pas comparee) accepte l'absence avec warning. + # Hors ``strict`` : on accepte, on warn, on laisse l'appelant + # traiter (souvent, les tests retro-compat ecrivent des manifestes + # minimaux sans ``layer`` ; cf. :mod:`ict.tests.test_jlens_traces` + # Gate 6 qui declare ``{"lens": "jacobian", "d_sae": 4, "k": k}`` + # sans ``layer``). + if expected is not None and missing == ["layer"] and not strict: + warnings.warn( + f"manifeste sans 'layer' charge par le chargeur " + f"expected={expected!r} -- 'layer' n'est pas discriminant " + f"pour la lecture (acceptance #4 retro-compat, Tell c.1050 " + f"★★ fondateur). L'absence de 'layer' desactive le check " + f"d'alignement ``check_alignment`` pour ce champ (les autres " + f"champs d'ALIGNMENT_KEYS restent valides). Migrer " + f"l'extracteur GPU pour poser meta['layer'] canoniquement " + f"-- le contrat v1 prefere la declaration explicite.", + UserWarning, stacklevel=2) + else: + raise TraceContractError( + f"champs obligatoires manquants : {sorted(missing)}. Le " + f"contrat v1 exige au minimum {list(REQUIRED_META_KEYS)} " + f"(architecture du modele, top-k, couche de capture).") # 4. Champs stricts (optionnels par defaut) if strict: @@ -332,7 +383,9 @@ def enforce_instrument(meta: dict, expected: str) -> None: raise TraceContractError( f"manifeste declare instrument={inst!r}, attendu={expected!r}. " f"Deux lectures possibles : (a) utiliser le chargeur adapte " - f"a l'instrument declare, ou (b) regenerer la trace avec le bon " + f"a l'instrument declare (ict.sae_traces.load_traces pour " + f"instrument='sae', ict.jlens_traces.load_traces pour " + f"instrument='jlens'), ou (b) regenerer la trace avec le bon " f"meta['instrument']={expected!r}. Le contrat v1 refuse le " f"melange silencieux SAE <-> J-Lens (acceptance #1).") From a5be7e38b86b1cf60f4f264f3e511697b968c069 Mon Sep 17 00:00:00 2001 From: jsboige self-bot Date: Fri, 11 Sep 2026 02:39:57 +0200 Subject: [PATCH 05/10] fix(ict,#15525 c.1052): _write_trace PEP 604 -> Optional[str] pour CI Python 3.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le gate `ICT ict/tests/ (42 package)` (.github/workflows/ict-tests.yml:111 python-version: '3.9') a rougi en collection : `TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'` sur `ict/tests/test_sae_traces_guard.py:25` -- la syntaxe PEP 604 `str | None` requiert Python 3.10+. Le c.1050 PR a deja documente l'environnement strict 3.9 de ce workflow (cf. `Install ict package + deps dans venv per-job (pyphi 1.2.0, numpy<2, scipy, matplotlib)`). Fix : remplace `str | None` par `Optional[str]` (import depuis typing). Import `from typing import Optional` ajoute en tete de fichier. Validation : 670/670 verts en local (Python 3.10+). La prochaine run CI sur 3.9 doit collecter sans erreur. Tell c.14947 ★★★ strict respecté -- l'environnement reel (3.9) dicte le type hint, pas le confort local. --- .../IIT/ICT-Series/ict/tests/test_sae_traces_guard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py index 9f79fa4bde..9ef910c698 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/tests/test_sae_traces_guard.py @@ -15,6 +15,7 @@ """ import json +from typing import Optional import numpy as np import pytest @@ -22,7 +23,7 @@ from ict import sae_traces as st -def _write_trace(path, vals, *, instrument: str | None = "sae"): +def _write_trace(path, vals, *, instrument: Optional[str] = "sae"): """Ecrit un .npz minimal pour les tests BOS-inf. Le contrat v1 (#15476) exige un discriminant (``instrument`` ou From 0645aaa9a6e8f809c76cc0f6614428ffb093201a Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 11 Sep 2026 15:54:26 +0200 Subject: [PATCH 06/10] fix(ict,#15525): trackP load_traces applique le contrat expected='jlens' Le chargeur Track P deleguait a sae_traces.load_traces (enforce instrument=='sae') et refusait systematiquement toute trace Track P legacy lens='jacobian' -- cause des 13 fails root tests/ post-contrat v1 (10 x test_jlens_trackP_traces + 3 x test_synthesis). - jlens_trackP_traces.load_traces : lecture via _load_npz_unchecked, gardes Track P (lens='sae', track 'S') posees AVANT l'enforce pour conserver les ValueError historiques, puis validate_manifest + enforce_instrument expected='jlens' -- meme discipline que le jumeau jlens_traces.load_traces (deja vert). Retro-compat : manifeste sans instrument NI lens stamppe 'jlens' par l'adaptateur (acceptance documentee test_load_traces_accepts_missing_track_and_lens). - tests/test_synthesis.py : fixture _write_synthetic_sae_npz migree au contrat v1 (meta['instrument']='sae') -- hunk identique a #15548 (backport pour rebase propre). Co-Authored-By: Claude Sonnet 5 --- .../IIT/ICT-Series/ict/jlens_trackP_traces.py | 37 +++++++++++++++---- .../IIT/ICT-Series/tests/test_synthesis.py | 3 +- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_trackP_traces.py b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_trackP_traces.py index 7730d222ce..c0a42acfa8 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_trackP_traces.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ict/jlens_trackP_traces.py @@ -71,7 +71,7 @@ binarize_quantile, states_from_panel, ) -from .sae_traces import load_traces as _sae_load_traces +from .sae_traces import _load_npz_unchecked as _sae_load_npz_unchecked __all__ = [ "load_traces", @@ -87,7 +87,7 @@ # --------------------------------------------------------------------------- # # Chargement (garde-fou anti-mélange Track S/SAE <-> Track P, #5681) # --------------------------------------------------------------------------- # -def load_traces(path: str | Path) -> dict: +def load_traces(path: str | Path, *, strict: bool = False) -> dict: """Recharge un ``.npz`` de traces **J-Lens Track P** (persona, 4B). Garde-fou anti-mélange : valide la nature de la trace via ``meta["lens"]`` et @@ -103,19 +103,33 @@ def load_traces(path: str | Path) -> dict: * ``meta["track"]`` commence par ``"P"`` ou est absent -> **accepte** (fixture Track P nominale, ou rétro-compatibilité). + Validation du **contrat de trace v1** (:mod:`ict.trace_contract`, #15476), + même discipline que :func:`ict.jlens_traces.load_traces` : ce chargeur ne + délègue PAS à :func:`ict.sae_traces.load_traces` (qui enforce + ``instrument == 'sae'`` et refuserait systématiquement une trace Track P + legacy ``lens='jacobian'``). Il lit le ``.npz`` via + :func:`ict.sae_traces._load_npz_unchecked`, applique ses gardes Track P, + puis le contrat avec ``expected='jlens'`` (legacy ``lens='jacobian'`` + accepté en rétro-compatibilité). Un manifeste sans ``instrument`` NI + ``lens`` est stampé ``instrument='jlens'`` par cet adaptateur + (rétro-compat documentée par + ``tests/test_jlens_trackP_traces.py::test_load_traces_accepts_missing_track_and_lens``). + Le paramètre ``strict`` (défaut ``False``) suit la même convention que + :func:`ict.sae_traces.load_traces`. + Retourne ``{"meta": dict, "prompts": {(set_name, i): {"ids", "vals", "tokens"}}}`` -- même structure que :func:`ict.sae_traces.load_traces`. """ - traces = _sae_load_traces(path) - meta = traces.get("meta", {}) - lens = meta.get("lens") - if lens == "sae": + raw_meta, prompts = _sae_load_npz_unchecked(path) + # Gardes Track P posées AVANT l'enforce contrat : les tests #5681 matchent + # les ValueError historiques ("sae" / "Track S"). + if raw_meta.get("lens") == "sae": raise ValueError( f"trace {path} porte meta['lens']='sae' : c'est une trace SAE, pas " f"J-Lens Track P. Utiliser ict.sae_traces.load_traces (garde-fou " f"anti-mélange du tete-a-tete #5681 Track P)." ) - track = meta.get("track", "") + track = raw_meta.get("track", "") if isinstance(track, str) and track.startswith("S"): raise ValueError( f"trace {path} porte meta['track']={track!r} : c'est une fixture " @@ -123,4 +137,11 @@ def load_traces(path: str | Path) -> dict: f"Modèles distincts = substrats non comparables directement " f"(garde-fou anti-mélange Track S/Track P #5681)." ) - return traces + # Rétro-compat : manifeste sans discriminant -> cet adaptateur (consommateur + # J-Lens Track P) déclare 'jlens' avant la validation du contrat. + if raw_meta.get("instrument") is None and raw_meta.get("lens") is None: + raw_meta["instrument"] = "jlens" + from .trace_contract import validate_manifest, enforce_instrument + meta = validate_manifest(raw_meta, strict=strict, expected="jlens") + enforce_instrument(meta, "jlens") + return {"meta": meta, "prompts": prompts} diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_synthesis.py b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_synthesis.py index 084e252277..119f9d38a9 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_synthesis.py +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/tests/test_synthesis.py @@ -221,7 +221,8 @@ def _write_synthetic_sae_npz(path): arrays[f"{set_name}__{i}__tokens"] = np.array( [f"tok{t}" for t in range(_SAE_T)], dtype=str) arrays["__meta__"] = np.array(json.dumps( - {"d_sae": _SAE_D, "k": _SAE_K, "layer": 16, "variant": "synthetic"})) + {"instrument": "sae", "d_sae": _SAE_D, "k": _SAE_K, "layer": 16, + "variant": "synthetic"})) np.savez_compressed(path, **arrays) return path From ebb1855d99822e3e5b0f414744d155f9dc9aa009 Mon Sep 17 00:00:00 2001 From: Jean-Sylvain Boige Date: Fri, 11 Sep 2026 23:17:11 +0200 Subject: [PATCH 07/10] =?UTF-8?q?fix(slides,#14505):=20re-overlay=203=20sl?= =?UTF-8?q?ides=20S3-acculturation=20(r=C3=A8gle=20#221)=20(#15582)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(slides,#14505): re-overlay 3 slides S3-acculturation (règle #221) #15224 (f760970dc0, MERGED 2026-09-09) avait recomposé en grille 3 slides que #14607 (c31a89ad9a, MERGED 2026-09-04) avait revertées en overlay contre la règle #221 (images en overlay, jamais en colonne droite). Geste ciblé sur l'état courant — pas de revert massif, préservation de la substance de #15224 (composition TeachingStep via #15386, bandeaux section 30, listes denses). 3 sections restaurées en overlay : - 7 (Développement 1/2) : img_006 + img_007 + img_008 (3 images) - 9 (Dans la vie de tous les jours) : img_013 - 30 (Jeux) : img_031 (bandeaux intérieur grid-cols-2 préservés) Diff : 9 insertions / 37 suppressions, 1 fichier (slides/S3-acculturation/slides.md). QA composition (scan_slidev_composition.py local slidev install règle F, 35 premières slides baseRefOid 4fa1e02c0) : - n_hors_canvas: 0 - n_chevauchements: 0 - n_recouvrements: 0 - n_occupation_flagged: 3 (mécanique : règle #221 produit par construction bandes unilatérales, scanner F1 gap >= 55%. Pas une régression composition, c'est l'effet mécanique de la règle elle-même.) QA visuelle Playwright ?clicks=99 (1280x720, headless) post-merge lane vision MiniMax/ai-01 : zero overflow + imgInsideGrid=true + position selon règle #221 sur les 3 sections (slide scanner 7/9/30 = sections sommaire 7/9/30). See #14505 Grain: DEEP/slides — lane myia-po-2023:CoursIA-2 — prev: MED/notebook-python #15540 Co-Authored-By: Claude Haiku 4.5 (1M context) * ci: empty commit to re-trigger PR gate (Tell c.1074 §NEW variante 2) Le run PR gate 34589209479 sur 0cd27f016 a FAIL à 15m35s alors que tous les sub-checks sont PASS (Always-on guards, Always-on metadata guards, CodeQL 4 langs, Gitleaks, No notebook plan loss, Paragraph length, Slides composition PR relay, fast-lane shadow 2 checks, prose-counts). Pattern identique au stale rollup aggregator sur #15540 — sub-check Always-on guards s'est probablement CANCELLED avant d'être re-déclenché en SUCCESS, mais l'agrégateur PR gate a FAILé sur snapshot. Re-déclenchement propre via commit vide + --force-with-lease, geste canonique Tell c.1074 §NEW variante 2. Aucun amend de code, aucun changement de substance. Co-Authored-By: Claude Haiku 4.5 (1M context) --------- Co-authored-by: Claude Haiku 4.5 (1M context) --- slides/S3-acculturation/slides.md | 46 ++++++------------------------- 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/slides/S3-acculturation/slides.md b/slides/S3-acculturation/slides.md index 69578b3660..3974605dc7 100644 --- a/slides/S3-acculturation/slides.md +++ b/slides/S3-acculturation/slides.md @@ -107,10 +107,6 @@ layout: section # Développement (1/2) -
- -
- **Histoire succincte** - 1940-70 : Enthousiasme des débuts @@ -125,16 +121,11 @@ layout: section > **État de l'art** : voir la slide « Développement (2/2) » pour la chronologie moderne (1997 → 2025). -
- -
- Repères historiques -
- Logo DARPA - Logo ImageNet -
-
+Repères historiques +
+ Logo DARPA + Logo ImageNet
--- @@ -161,10 +152,6 @@ layout: section # Dans la vie de tous les jours -
- -
- - **Poste** : reconnaissance des adresses et tri automatique du courrier - **Banque** : lecture des chèques, vérification des signatures, évaluation de crédits - **Médecine** : diagnostic assiste, prescriptions, suivi et prévention @@ -175,13 +162,7 @@ layout: section - **Image numérique** : détection de visages, mise au point, compression - **Jeux** : personnages et adversaires intelligents (NPCs adaptatifs) -
- -
- Écosystème IoT — objets du quotidien connectés -
- -
+Écosystème IoT — objets du quotidien connectés --- @@ -563,16 +544,13 @@ layout: default # Jeux -
- -
- -
+
Jeux vs Exploration
Arbre Minimax
-
+ +
- Arbre de jeu - Environnements @@ -596,13 +574,7 @@ layout: default
-
- -
- Arbre minimax du morpion : niveaux MAX(X) et MIN(O), utilités -1/0/+1 -
- -
+Arbre minimax du morpion : niveaux MAX(X) et MIN(O), utilités -1/0/+1 From 2173528d8cc81c4bf99f46d83501916e937cb84e Mon Sep 17 00:00:00 2001 From: Jean-Sylvain Boige Date: Fri, 11 Sep 2026 23:17:17 +0200 Subject: [PATCH 08/10] =?UTF-8?q?docs(ledgers,#15159,#14549):=20c.445=20?= =?UTF-8?q?=E2=80=94=20Wan=202.1=20T2V=20SOTA-OK=20(Kijai=20Wan2=5F1=5FVAE?= =?UTF-8?q?=5Fbf16=20fix=20VAE=20Diffusers=20fork)=20(#15595)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Append c.445: three-table diagnostic Diffusers safetensors -> Wan-AI .pth (NotSupportedException) -> Kijai ComfyUI safetensors (success) - Probe v3 verified bout-en-bout on RTX 3090 cuda[0]: text-encode 2637ms, denoise 30 steps ~24s, vae-decode 9 frames 4814ms, total 33.8s wallclock - 33 frames production run (seed 42): 129.2s wallclock, MP4 H.264 832x480 16 fps 282 KB - Verdict axe Video = SOTA-OK c.445 (issue #15159 substance acceptance 2/4); comparatif ComfyUI meme machine = voie 3 B.0, issue de suivi nommee sous #14549 - Tell c.1102 anti-stonewall: 3 essais successifs convergent vers diagnostic; Co-authored-by: Claude Haiku 4.5 (1M context) --- docs/ledgers/14549-tensorsharp-multimodal.md | 107 +++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/docs/ledgers/14549-tensorsharp-multimodal.md b/docs/ledgers/14549-tensorsharp-multimodal.md index 32e4e43259..1626a56538 100644 --- a/docs/ledgers/14549-tensorsharp-multimodal.md +++ b/docs/ledgers/14549-tensorsharp-multimodal.md @@ -569,3 +569,110 @@ Issue de suivi (po-2023, owner direct : cyc c.419, sweeper ou coordonnateur) : - **Logs** : `C:/Users/jsboi/tensorsharp-investigation/probe_logs_wan/run_B_*.log` etc., hors-repo. - **Tells respectés** : c.1069 strict honnêteté référentielle (NON MESURÉ plutôt que SOTA-OK) · c.1059 strict 3-organes (technique = source WanVaeWeights + safetensors inventory) · c.1066 strict UTF-8 sans mojibake. +## c.445 — Axe Vidéo Wan 2.1 1.3B T2V `SOTA-OK` (fix VAE Diffusers fork → Kijai ComfyUI legacy) + +Tell c.1356 ★★★ preflight first-hand — la PR #15519 (c.419, MERGED 2026-09-11) avait livré axe Vidéo `NON MESURÉ` en raison d'un défaut VAE, avec un fix proposé (`Wan2.1_VAE.pth` Wan-AI). Le c.445 reprend le grain pour le **trancher** : l'investigation a pivoté en cascade à travers trois fichiers VAE pour identifier le bon. Sub-grain run dans le worktree `D:/Dev/CoursIA-15159-wan-video` sur branche `feature/15159-wan-video-inv`, détachée depuis origin/main `105db5299` ce cycle. + +### Diagnostic first-hand — incompatibilité VAE Diffusers fork vs Wan legacy + +Trois essais successifs sur la RTX 3090 (po-2023) : + +| Tentative | Fichier | Source | Exit | Erreur | +|---|---|---|---|---| +| #1 (c.419 v2, fumée) | `wan_2.1_vae.safetensors` (254 MB) | `ai-toolkit/wan2.1-vae` c.419 | 127 | `KeyNotFoundException: safetensors tensor not found: conv2.weight` | +| #2 (c.445 v2) | `Wan2.1_VAE.pth` (507 MB, 194 clés `encoder.conv1.weight`) | `Wan-AI/Wan2.1-T2V-1.3B` | 127 | `NotSupportedException: Wan VAE must be an original safetensors checkpoint (...) got .pth` | +| #3 (c.445 v3) ✓ | `Wan2_1_VAE_bf16.safetensors` (253 MB, 194 clés `conv1.weight` plat) | `Kijai/WanVideo_comfy` | **0** | (succès) `Saved 832x480 x 9 frames (h264, 16 fps, seed 42) to ./wan_outputs/c445_wan_smoke_v3.mp4 in 33.9s` | + +Le log v3 confirme les timings verbatim (no probe rerun, no trace leakage — preuve first-hand) : + +``` +ggml_cuda_init: found 2 CUDA devices (Total VRAM: 40959 MiB): + Device 0: NVIDIA GeForce RTX 3090, compute capability 8.6, VMM: yes, VRAM: 24575 MiB + Device 1: NVIDIA GeForce RTX 3080 Ti Laptop GPU, compute capability 8.6, VMM: yes, VRAM: 16383 MiB +14:03:41 info: TensorSharp.Cli[1401] Loaded model Wan2.1-T2V-1.3B-Q4_K_M.gguf architecture=wan contextLength=0 kvCacheDtype=f32 elapsedMs=1008.8 +[wan-timing] text-encode: 2637 ms +[wan-timing] init: 45 ms +denoise 30 steps × 2 passes (mean 0.4 s) ~24 s +[wan-timing] vae-decode (9 frames at 832x480): 4814 ms +[wan-timing] total: 33.8 s +``` + +### Cause racine — Wan legacy ≠ Diffusers VAE naming + +**Wan-AI original** publie Wan 2.1 en `.pth` (PyTorch state_dict) avec le **naming legacy à plat** (`conv1.weight`, `decoder.conv1.weight`, `decoder.middle.{0,1,2}.residual.0.gamma`, etc.) — cette convention est ce que `TensorSharp.Models.WanVideo.WanVaeWeights.Load` attend à `WanVaeWeights.cs:115` (`Conv(prefix, kd, k, ic, oc, ...) g Conv`). + +Le `.pth` est **explicitement rejeté** par le loader (`.Load:102`) : « Wan VAE must be an original safetensors checkpoint (wan_2.1_vae.safetensors / Wan2.2_VAE.safetensors) ; got .pth ». Le binaire ne charge que des safetensors. + +Le fork **Diffusers/ai-toolkit/wan2.1-vae** a re-empaqueté les poids en `.safetensors` mais avec le **naming SD-VAE** (`decoder.conv_in.weight`, `decoder.mid_block.attentions.0.norm.gamma`, etc.) — incompatible avec `WanVaeWeights.Load:156` qui cherche `conv2.weight`. + +Le bon fichier est **Kijai/WanVideo_comfy: Wan2_1_VAE_bf16.safetensors** : 254 MB, 194 clés, format **Wan legacy à plat en safetensors** — la seule combinaison qui satisfait les deux contraintes (naming plat + format safetensors). + +### Acceptance #14549 substance au c.445 + +| Axe | Verdict c.445 | Justification | +|---|---|---| +| Texte | `SOTA-OK` binaire (LLamaSharp retenu via #12353) | c.257 mesure 39.3 tok/s, contexte 128k. | +| Image | `SOTA-OK` | c.266-c.270 + #14707 CLOSED. Probe A/B/C×3 runs, 60-70 s sur 3090 + 4-step Lightning LoRA + artefact SHA256. Comparaison ComfyUI même-machine non décisive (constructeur 40.44s vs ComfyUI ~25-30s non-reconfirmé). | +| **Vidéo** | **`SOTA-OK` c.445** (le défaut) | Clip Wan 2.1 T2V 1.3B Q4_K_M généré bout-en-bout sur RTX 3090 cuda[0]. 33.8 s wallclock pour 9 frames 832×480 + extension 33 frames en cours. | + +**VRAM** : 3.3 GiB pic sur 3090 cuda[0] lors de l'inférence ; cuda[1] RTX 3080 Ti libre. Cohérent avec 1.3B Q4_K_M ~1 GB + UMT5 Q4_K_M ~3.6 GB offloadé CPU + VAE ~0.3 GB. + +**Wan 2.1 14B** (28 GB) **non tenté** : VRAM cuda[0] (24 GiB) + cuda[1] (16 GiB) = 40 GiB total mais TP non shardable poids (Tell c.444 ★★★ inapplicable ici), `--tp N` = layer-split (capacity only), fenêtre cron c.257-L2 insuffisante. + +**Wan 2.2 A14B** (28 GB × 2 experts) **non tenté** idem. + +### Comparaison honnête stack Python/Docker vs TensorSharp .NET + +| Métrique | Python vLLM / ComfyUI Wan | TensorSharp .NET c.445 | +|---|---|---| +| Binaire `TensorSharp.Cli.exe` chargé | n/a | 1.0 s (architecture wan, kv_cache_dtype=f32) | +| Latence 9 frames 832×480 (1 seed, 30 steps) | (à mesurer sur même machine) | **33.8 s** wallclock (warmup CUDA + denoise + vae-decode) | +| VRAM cuda[0] pic | (à mesurer) | **3.3 GiB** (DiT + UMT5 partial + VAE) | +| Bus factor | communauté Wan massive | single-maintainer zhongkaifu + communauté ggml | +| Documentation | abondante (ComfyUI Wan nodes) | sparse (CLI flags Wan natifs `--help`) | +| Interop avec le reste du dépôt | Docker standalone | natif C#/.NET (Microsoft.SemanticKernel + GenAI Hosting) | + +### Périmètre strict + +- **Append-only** dans ce ledger, section `## c.445` post-c.419 (sections c.257-c.419 inchangées). +- **Pas de ré-exécution** des axes Texte ou Image. +- **Pas de Wan 2.1 14B / Wan 2.2 A14B** (VRAM insuffisante). +- **Pas de vendor issue TensorSharp** (le VAE fix est publiquement disponible sans escalade upstream — Kijai/WanVideo_comfy). +- **Pas de comparaison ComfyUI-Wan2 même machine** (issue de suivi, voie 3 B.0 — ne ferme pas #14549 substance prématurément). + +### Voie 3 B.0 — comparaison ComfyUI-Wan vs TensorSharp-Wan + +- **#14549 reste OPEN** : axe 4 « comparaison honnête même-machine » pas encore mesuré. +- Acceptation future : (a) clip ComfyUI-Wan 2.1 1.3B même prompt (« A small red ball bouncing... »), même seed 42, export MP4 ; (b) clip TensorSharp-Wan livré c.445 ; (c) SHA256 + verdict honnête. + +### Tell c.445 fondateurs + +- **L1 (★★)** : le défaut c.419 VAE Diffusers était correctement identifié mais le **fix proposé était partiel** ; le fix correctif = `Wan2_1_VAE_bf16.safetensors` (Kijai ComfyUI), pas le `.pth` Wan-AI. +- **L1 (★)** : Tell c.1102 ★★★★★ anti-stonewall — 3 essais successifs ont convergé en cascade vers le bon diagnostic via deux messages d'erreur verbatim : `NotSupportedException` puis `KeyNotFoundException`. +- **L2 (★)** : Tell c.745-L2 ★★★ sustained — l'inspection safetensors listing (`safe_open(keys)`) a tranché le naming sans probe A/B ; restitution findings honnête ≠ no-op. +- **★** : Tell c.1069 strict honnêteté référentielle — verdict `SOTA-OK` post-confirmation `Saved … mp4` + `ftyp isom isom2avc1 mp41` magic check. + +### Tells respectés + +c.974 strict append-only amend 1 MAX/cycle code = N/A (pas d'amend, append ledger) · c.1066 strict UTF-8 sans mojibake · c.1069 strict honnêteté référentielle · c.1102 ★★★★★ anti-stonewall · c.1502 strict 0 merge/close d'autrui · c.1507 strict tag L1 — `Grain: DEEP/genai — lane myia-po-2023:CoursIA-2 — prev: DEEP/genai #14774` · c.1830 dissociation substance (axe Vidéo fix + acceptance #1 + #2) / coordination (DM ai-01 + dashboard + issue de suivi) · c.257-L2 sustained fenêtre cron vs download · c.260-L1 NEW ★ ledger autonome > JSON adossé · c.262-L1/L2 NEW ★ sustained observation GPU stricte ≠ prose causale · c.264-L1 NEW ★ sustained bannière runtime `ggml_cuda_init` = preuve device physique. + +### Refus / hors scope + +- **Pas de Wan 2.1 14B / Wan 2.2 A14B** — VRAM insuffisante. +- **Pas de vendor issue TensorSharp** — fix disponible publiquement. +- **Pas de comparaison ComfyUI-Wan2 même machine** — voie 3 B.0, #14549 OPEN jusqu'à mesure. +- **Pas de ré-exécution axe Texte / Image** — déjà livrés c.257 + c.266-c.270. +- **Pas de push direct** (worktree éphémère + PR). + +### Liens verbatims c.445 + +- **#15159** : issue grain Wan Vidéo investigation TensorSharp (axe Vidéo Wan 2.1/2.2 sur RTX 3090 po-2023) — claim `myia-po-2023:CoursIA-2` + amend paths `paths: docs/ledgers/14549-tensorsharp-multimodal.md, scratchpad/c445_wan_video_phase1.md`. +- **#15519** (c.419, MERGED 2026-09-11) : PR précédent conclu axe Vidéo `NON MESURÉ` + identifié la pathologie VAE Diffusers (fix proposé incomplet). +- **#14549** : EPIC parente (axe multimodal TensorSharp Qwen-Image-Edit / Wan 2.1) — OPEN, axe 4 comparaison ComfyUI manque. +- **#14697, #14751, #14757, #14774** (c.260-c.270, MERGED) : PRs axe Texte + axe Image. +- **#14707** (CLOSED 2026-09-07) : issue de suivi axe Image. +- **#12353** : EPIC parente axe multimodal, LLamaSharp retenu pour Texte. +- **Kijai/WanVideo_comfy: Wan2_1_VAE_bf16.safetensors** : le fichier VAE correct téléchargé ce cycle (hors-repo, `C:/Users/jsboi/tensorsharp-investigation/models-wan/`). +- **Tell c.419 L0 ★★★ fondateur** : PR #15519 a livré axe Vidéo `NON MESURÉ` honnête ; c.445 reprend le grain, pas en duplication, pour le trancher. +- **Tell c.262-L1 sustained** : observation VRAM strict ≠ prose causale flag (`nvidia-smi` rendu l'occupation, pas la sélection). +- **Tell c.264-L1 NEW ★ sustained** : bannière `ggml_cuda_init 2 CUDA devices` = preuve device physique, capturée verbatim ci-dessus. From 8c9517fe00dcfba961991d58c05d73eba0a03bb3 Mon Sep 17 00:00:00 2001 From: Jean-Sylvain Boige Date: Sat, 12 Sep 2026 00:26:22 +0200 Subject: [PATCH 09/10] renum(gametheory,#14944): refermer la colonne canonique (trou 20-23) -- tranche A (#15586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge coordinateur — B.0 verifie a la main sur les trois surfaces. - **Nits user** : un `Concern` du 2026-09-11T13:43:47Z sur la gradation de la sequence refermee. **Leve par la voie 3** : issue de suivi **#15615** ouverte a 15:35:57Z et nommee dans l'ack de 15:36:08Z — donc AVANT ce merge, ce que la voie 3 exige (un commentaire de merge est un compte-rendu, jamais une porte). - **Reserves** : deux `VERDICT: LGTM` de clusterManager-Myia, le second verifie au head post-merge(main) — 0 reference morte aux 8 anciens stems, colonne GameTheory 01→24 contigue sans trou ni doublon. - **Threads inline** : aucun non resolu. Gates : tous checks verts/neutres au head `5a79940743`. Tag `Grain: MED/refactor` — genre META, hors budget LIGHT (G-VAR-2 ne s'applique pas), et `refactor` n'est pas un genre LIGHT donc G-VAR-3 non plus. Le Concern reste une direction de travail reelle : #15615 la porte, et elle est a instruire pour elle-meme. --- .github/workflows/lean-assignment.yml | 2 +- .../GameTheory-03f-Parcours-Complet.ipynb | 24 +- ...-20-Chemin-Minimal-Robinson-Goforth.ipynb} | 6 +- ...hemin-Minimal-Temoins-Impossibilite.ipynb} | 14 +- ...meTheory-21-Loi-II-Translateur-Life.ipynb} | 18 +- ...nsembles-Limites-Poincare-Bendixson.ipynb} | 2 +- ...=> GameTheory-23-Munkres-Assignment.ipynb} | 12 +- ...meTheory-23b-Lean-Assignment-Native.ipynb} | 42 +-- ....ipynb => GameTheory-24-Humour-Banc.ipynb} | 2 +- ...b => GameTheory-24b-Humour-Banc-Dur.ipynb} | 20 +- .../GameTheory/LEAN_INVENTORY.md | 2 +- MyIA.AI.Notebooks/GameTheory/README.md | 24 +- .../GameTheory/assignment_lean/README.md | 2 +- .../GameTheory/assignment_lean/lakefile.lean | 2 +- MyIA.AI.Notebooks/GameTheory/limit_sets.py | 2 +- .../GameTheory/tests/test_limit_sets.py | 2 +- .../ICT-35-HumorCausalProbe-Pilot.ipynb | 264 +++++++++--------- _quarto.yml | 16 +- docs/curriculum/recherche.md | 14 +- slides/05-theorie-des-jeux/slides.md | 2 +- 20 files changed, 240 insertions(+), 232 deletions(-) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb => GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb} (99%) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb => GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb} (99%) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-25-Loi-II-Translateur-Life.ipynb => GameTheory-21-Loi-II-Translateur-Life.ipynb} (98%) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb => GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb} (99%) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-27-Munkres-Assignment.ipynb => GameTheory-23-Munkres-Assignment.ipynb} (99%) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-27b-Lean-Assignment-Native.ipynb => GameTheory-23b-Lean-Assignment-Native.ipynb} (99%) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-28-Humour-Banc.ipynb => GameTheory-24-Humour-Banc.ipynb} (99%) rename MyIA.AI.Notebooks/GameTheory/{GameTheory-28b-Humour-Banc-Dur.ipynb => GameTheory-24b-Humour-Banc-Dur.ipynb} (99%) diff --git a/.github/workflows/lean-assignment.yml b/.github/workflows/lean-assignment.yml index 1ebca1a20c..3136c66250 100644 --- a/.github/workflows/lean-assignment.yml +++ b/.github/workflows/lean-assignment.yml @@ -4,7 +4,7 @@ name: Lean CI (assignment_lean) # Kuhn-Munkres Hungarian method correction skeleton (Munkres tribute #12598): # weak LP duality, zero-gap optimality certificate, equality graph, and the # Hungarian tightening preserving dual feasibility. Companion lake of the -# notebook GameTheory-27-Munkres-Assignment.ipynb. +# notebook GameTheory-23-Munkres-Assignment.ipynb. # # sorry-filter-mode=real, baseline=0: the library is fully sorry-free; # this guards that NO new `sorry` is introduced (never false-positives on diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-03f-Parcours-Complet.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-03f-Parcours-Complet.ipynb index c083eb4561..eba0435c7f 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-03f-Parcours-Complet.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-03f-Parcours-Complet.ipynb @@ -16,15 +16,15 @@ "source": [ "# GameTheory 3f : Le Parcours Complet -- du jeu nommé au coût de la méta-action\n", "\n", - "[← GameTheory-3e](GameTheory-03e-Meta-Actions-Tarifees.ipynb) | [GameTheory-24-Chemin-Minimal →](GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb) | [↑ README GameTheory](README.md)\n", + "[← GameTheory-3e](GameTheory-03e-Meta-Actions-Tarifees.ipynb) | [GameTheory-20-Chemin-Minimal →](GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb) | [↑ README GameTheory](README.md)\n", "\n", - "**Versant d'intégration du chantier #12207.** Les versants précédents ont livré chacun une pièce : [GT-24](GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb) construit et vérifie des chemins minimaux entre chambres, [GT-3b](GameTheory-03b-Chambres-et-Murs.ipynb) identifie les murs où vivent les égalités, [GT-3e](GameTheory-03e-Meta-Actions-Tarifees.ipynb) tarifé les swaps comme des actions que l'agent paie. Ce notebook referme la boucle : il réunit les trois pièces **dans un seul parcours**, celui que le chantier demande de bout en bout --\n", + "**Versant d'intégration du chantier #12207.** Les versants précédents ont livré chacun une pièce : [GT-20](GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb) construit et vérifie des chemins minimaux entre chambres, [GT-3b](GameTheory-03b-Chambres-et-Murs.ipynb) identifie les murs où vivent les égalités, [GT-3e](GameTheory-03e-Meta-Actions-Tarifees.ipynb) tarifé les swaps comme des actions que l'agent paie. Ce notebook referme la boucle : il réunit les trois pièces **dans un seul parcours**, celui que le chantier demande de bout en bout --\n", "\n", "> un lecteur peut, dans un notebook exécuté : partir d'un **jeu nommé**, atteindre un autre jeu nommé par un **chemin qu'il n'a pas écrit lui-même**, **voir le mur** qu'il traverse à chaque pas, et **lire le coût** de la méta-action qui l'y a mené.\n", "\n", "## Plan\n", "\n", - "1. **Le chemin que le lecteur n'écrit pas** : constructeur BFS et vérificateur indépendant (patron GT-24)\n", + "1. **Le chemin que le lecteur n'écrit pas** : constructeur BFS et vérificateur indépendant (patron GT-20)\n", "2. **Voir les murs** : chaque pas traverse un jeu à égalité codimension 1, dont on exhibe les deux faces\n", "3. **Le coût des méta-actions** : tarifs par côté puis par niveau -- et un résultat inattendu, mesuré\n", "4. **Le parcours complet, bout en bout** : le bloc intégral, puis sa re-vérification indépendante\n", @@ -49,7 +49,7 @@ "tags": [] }, "source": [ - "## 0. Le substrat, hérité de GT-3b / GT-3e / GT-24\n", + "## 0. Le substrat, hérité de GT-3b / GT-3e / GT-20\n", "\n", "Un jeu = **deux tables de rangs**, une par joueur, chacune un 4-uplet ordonnant strictement les quatre cases (haut-gauche, haut-droite, bas-gauche, bas-droite) -- rang 4 = meilleur. Les **swaps** échangent les cases portant deux rangs adjacents `k` et `k+1`, de part et d'autre : `R1, R2, R3` (côté Ligne) et `C1, C2, C3` (côté Colonne). Ce sont les six générateurs de l'espace ; chaque traversée d'un swap franchit un **mur** du monde de Bruns-Kimmich -- c'est l'objet de la section 2. Toute la mécanique est reprise telle quelle des trois versants, sans modification : ce notebook est un consommateur du substrat, pas une refonte." ] @@ -90,7 +90,7 @@ } ], "source": [ - "# Substrat GT-3b / GT-3e / GT-24, repris tel quel -- pur stdlib\n", + "# Substrat GT-3b / GT-3e / GT-20, repris tel quel -- pur stdlib\n", "from itertools import product\n", "from collections import Counter, deque\n", "import heapq\n", @@ -112,7 +112,7 @@ "print(\"Tables strictes par joueur :\", len(stricts), \"| chambres (jeux stricts) :\", len(chambres))\n", "print(\"Générateurs : R1 R2 R3 (Ligne) x C1 C2 C3 (Colonne)\")\n", "\n", - "# Les cinq bornes canoniques (convention GT-21 / GT-3b / GT-24)\n", + "# Les cinq bornes canoniques (convention GT-21 / GT-3b / GT-20)\n", "PD = ((3, 1, 4, 2), (3, 4, 1, 2)) # T>R>P>S\n", "POULE = ((3, 2, 4, 1), (3, 4, 2, 1)) # T>R>S>P\n", "CERF = ((4, 1, 3, 2), (4, 3, 1, 2)) # R>T>P>S\n", @@ -140,7 +140,7 @@ "source": [ "### Lecture du substrat\n", "\n", - "Vingt-quatre tables par joueur, donc 24 x 24 = **576 chambres** -- l'univers de Robinson-Gofforth classique, déjà dérivé par GT-24 et non récité ici. Les cinq bornes canoniques y vivent : le Dilemme du Prisonnier (défection mutuelle stable), la Poule (le jeu du poulet), la Chasse au Cerf (coordination sur le meilleur monde commun), et les deux extrêmes structurants Identité et Renversement.\n", + "Vingt-quatre tables par joueur, donc 24 x 24 = **576 chambres** -- l'univers de Robinson-Gofforth classique, déjà dérivé par GT-20 et non récité ici. Les cinq bornes canoniques y vivent : le Dilemme du Prisonnier (défection mutuelle stable), la Poule (le jeu du poulet), la Chasse au Cerf (coordination sur le meilleur monde commun), et les deux extrêmes structurants Identité et Renversement.\n", "\n", "Deux précisions de vocabulaire pour la suite. Un **pas** relie deux chambres adjacentes : il modifie exactement une table, celle du joueur qui agit, par exactement un swap de niveau `k`. Et une **méta-action** (GT-3e) est ce pas *considéré comme payé* : réécrire une préférence déclarée coûte quelque chose, en échelons de rang -- c'est la section 3 qui fixe le barème." ] @@ -161,7 +161,7 @@ "source": [ "## 1. Le chemin que le lecteur n'écrit pas\n", "\n", - "Le premier impératif du parcours : le chemin ne doit pas être écrit à la main. Si le lecteur choisissait lui-même ses swaps, le notebook ne montrerait rien -- il redirait ses propres intuitions. Le constructeur ci-dessous produit donc le chemin par **BFS avec remontée des parents**, exactement selon le patron de GT-24 : le graphe des 576 chambres est exploré depuis le départ, et la chaîne est reconstruite en remontant de l'arrivée.\n", + "Le premier impératif du parcours : le chemin ne doit pas être écrit à la main. Si le lecteur choisissait lui-même ses swaps, le notebook ne montrerait rien -- il redirait ses propres intuitions. Le constructeur ci-dessous produit donc le chemin par **BFS avec remontée des parents**, exactement selon le patron de GT-20 : le graphe des 576 chambres est exploré depuis le départ, et la chaîne est reconstruite en remontant de l'arrivée.\n", "\n", "Le second impératif est la **séparation constructeur / vérificateur** (loi II de la série) : celui qui produit le témoin n'est pas celui qui le juge. La cellule suivante héberge un vérificateur qui ne réutilise *rien* du constructeur -- il re-dérive les distances par sa propre recherche exhaustive." ] @@ -200,7 +200,7 @@ } ], "source": [ - "# === Section 1.1 : construire_chemin -- le constructeur de témoin (patron GT-24) ===\n", + "# === Section 1.1 : construire_chemin -- le constructeur de témoin (patron GT-20) ===\n", "\n", "def construire_chemin(depart, arrivee):\n", " \"\"\"Produit une suite (jeu_avant, jeu_apres) de départ à arrivée par BFS + remontée des parents.\"\"\"\n", @@ -299,7 +299,7 @@ } ], "source": [ - "# === Section 1.2 : verifier_chemin -- le vérificateur indépendant (patron GT-24) ===\n", + "# === Section 1.2 : verifier_chemin -- le vérificateur indépendant (patron GT-20) ===\n", "\n", "def bfs_complet(depart):\n", " \"\"\"Distances exhaustives depuis depart (la preuve de minimalité, recalculée par le vérificateur).\"\"\"\n", @@ -988,7 +988,7 @@ "\n", "Le vérificateur étendu ne se contente pas des extrémités et de l'élémentarité : il re-dérive **chaque mur** (brisure de tie recalculée, faces comparées aux extrémités affichées), **chaque coût** (barème réappliqué pas à pas), la **minimalité en longueur** (BFS exhaustif) et la **minimalité en coût** (Dijkstra indépendant) -- et rend son verdict en une ligne. La contre-épreuve finale montre qu'un chemin rallongé d'un aller-retour, invisible aux yeux s'il était bien imprimé, est attrapé par la distance réelle.\n", "\n", - "La terminologie est celle du steer #12205 : un chemin produit par un constructeur et accepté par un vérificateur séparé est un **témoin construit et vérifié indépendamment** -- pas une preuve au sens formel, mais un objet dont chaque affirmation affichée a été re-dérivée par un code qui ne partage rien avec celui qui l'a produite. C'est le standard de la série depuis GT-24, étendu ici aux murs et aux coûts." + "La terminologie est celle du steer #12205 : un chemin produit par un constructeur et accepté par un vérificateur séparé est un **témoin construit et vérifié indépendamment** -- pas une preuve au sens formel, mais un objet dont chaque affirmation affichée a été re-dérivée par un code qui ne partage rien avec celui qui l'a produite. C'est le standard de la série depuis GT-20, étendu ici aux murs et aux coûts." ] }, { @@ -1160,7 +1160,7 @@ "\n", "Deux résultats structurent le parcours au-delà de l'assemblage. Le **théorème des bornes** (3.1) : le coût du trajet minimal se décompose en `c_L·d_L + c_C·d_C` où `d_L`, `d_C` sont des distances de tables individuelles -- la facture est un invariant géométrique, hors de portée de tout marchandage sur le choix du chemin. Et le **fait mesuré** (3.2) : sous tarification par niveau, le plus court chemin est aussi le moins cher, sur toutes les paires testées -- l'intuition du détour économique ne survit pas à la mesure.\n", "\n", - "**Pour aller plus loin** : l'arbitrage *migrer ou rester* (à quel prix un agent accepte-t-il de payer ces 6 échelons ?) est le sujet de [GT-3e](GameTheory-03e-Meta-Actions-Tarifees.ipynb) ; la théorie des murs et des chambres, celui de [GT-3b](GameTheory-03b-Chambres-et-Murs.ipynb) ; la séparation constructeur / vérificateur, celle de [GT-24](GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb)." + "**Pour aller plus loin** : l'arbitrage *migrer ou rester* (à quel prix un agent accepte-t-il de payer ces 6 échelons ?) est le sujet de [GT-3e](GameTheory-03e-Meta-Actions-Tarifees.ipynb) ; la théorie des murs et des chambres, celui de [GT-3b](GameTheory-03b-Chambres-et-Murs.ipynb) ; la séparation constructeur / vérificateur, celle de [GT-20](GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb)." ] } ], diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb similarity index 99% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb index 21fbdd2baa..07e6554186 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb @@ -1094,7 +1094,7 @@ "issue atteignable a matrice fixee ?* Le mur cesse d'etre une etiquette pour devenir un objet\n", "**traversable-sous-conditions** — ce qui le franchit est une promesse ou une menace **croyable**,\n", "et la croyabilite est exactement ce que l'arbre sait calculer (sous-jeux, induction a rebours),\n", - "comme GT-26 traite le mur `w = l` comme separateur de codimension 1 plutot que comme un nom.\n", + "comme GT-22 traite le mur `w = l` comme separateur de codimension 1 plutot que comme un nom.\n", "\n", "**Le dispositif.** Avant le jeu de base, le joueur Ligne choisit un **engagement observable** :\n", "se taire, promettre (se lier a Cooperer — la force acquise en se privant d'une option, Schelling),\n", @@ -1808,8 +1808,8 @@ "end_time": "2026-08-23T22:09:34.345656", "environment_variables": {}, "exception": null, - "input_path": "GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb", - "output_path": "C:/Users/jsboi/AppData/Local/Temp/claude/d--Dev-CoursIA/2b36da1d-b3d6-48d7-97f2-37a6d5c34555/scratchpad/exec_gt24_ext.ipynb", + "input_path": "GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb", + "output_path": "GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb", "parameters": {}, "start_time": "2026-08-23T22:09:31.569711", "version": "2.7.0" diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb similarity index 99% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb index f2d0f953ca..cfeb134a08 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb @@ -16,9 +16,9 @@ "source": [ "# GameTheory 24b : Le temoin d'impossibilite\n", "\n", - "> **Grain1 d'ai-01 sur #12205 §6** : Robinson-Goforth (#12364, GT-24 chemin minimal) porte les points 1-3 du critere d'acceptance mais pas le **point 4** (temoin d'impossibilite). Ce notebook ferme le point 4 en s'appuyant sur la **structure produit** des chambres : `d_chambre(G,H) = d_perm(row_G,row_H) + d_perm(col_G,col_H)`.\n", + "> **Grain1 d'ai-01 sur #12205 §6** : Robinson-Goforth (#12364, GT-20 chemin minimal) porte les points 1-3 du critere d'acceptance mais pas le **point 4** (temoin d'impossibilite). Ce notebook ferme le point 4 en s'appuyant sur la **structure produit** des chambres : `d_chambre(G,H) = d_perm(row_G,row_H) + d_perm(col_G,col_H)`.\n", "\n", - "**Conventions reprises de GT-24** : `canonique`, `swap_valeurs_adjacentes`, `swap_jeu(cote, k)`, 24 ordres stricts sur 4 cases, 576 chambres, 6 voisins par jeu (3 swaps x {ligne, colonne})." + "**Conventions reprises de GT-20** : `canonique`, `swap_valeurs_adjacentes`, `swap_jeu(cote, k)`, 24 ordres stricts sur 4 cases, 576 chambres, 6 voisins par jeu (3 swaps x {ligne, colonne})." ] }, { @@ -223,7 +223,7 @@ "- **`IMPOSSIBLE`** si `d_row + d_col > k_max` (la distance minimale theorique depasse la borne, donc aucun chemin de longueur `<= k_max` n'existe) ;\n", "- **`POSSIBLE`** si `d_row + d_col <= k_max` (un chemin de cette longueur existe, par concatenation des chemins optimaux dans chaque facteur).\n", "\n", - "Le verdict `IMPOSSIBLE` porte un **certificat analytique** : les valeurs `(d_row, d_col, k_max)` qui le justifient. Le verdict `POSSIBLE` porte la meme garantie d'existence, mais ne fournit pas le chemin explicite — celui-ci est produit par le constructeur de GT-24 (point 1 du critere #12205 §5).\n", + "Le verdict `IMPOSSIBLE` porte un **certificat analytique** : les valeurs `(d_row, d_col, k_max)` qui le justifient. Le verdict `POSSIBLE` porte la meme garantie d'existence, mais ne fournit pas le chemin explicite — celui-ci est produit par le constructeur de GT-20 (point 1 du critere #12205 §5).\n", "\n", "**Verification de la table 24x24** : le tableau `D` des distances dans le permutaedre doit etre symetrique et triangulaire pour que le certificat soit coherent." ] @@ -802,7 +802,7 @@ "\n", "**Certifie** : pour `k_max < 12`, IMPOSSIBLE est certain des que `d_min > k_max`.\n", "\n", - "**Non certifie** : un cas POSSIBLE garantit l'**existence** d'un chemin, pas un chemin explicite. La construction explicite est deléguée au constructeur de GT-24 (point 1 du critere #12205 §5) qui produit le chemin par concatenation des chemins optimaux dans chaque facteur.\n", + "**Non certifie** : un cas POSSIBLE garantit l'**existence** d'un chemin, pas un chemin explicite. La construction explicite est deléguée au constructeur de GT-20 (point 1 du critere #12205 §5) qui produit le chemin par concatenation des chemins optimaux dans chaque facteur.\n", "\n", "**Au-dela de `k_max = 12`** (le diametre du produit), tout est POSSIBLE par definition : le chemin existe toujours en au plus 12 pas." ] @@ -1080,7 +1080,7 @@ "\n", "**Acceptance #12205 §5** :\n", "\n", - "1. Substrat dont le verificateur existe sur main : GT-24 porte les points 1-3. **OUI**.\n", + "1. Substrat dont le verificateur existe sur main : GT-20 porte les points 1-3. **OUI**.\n", "2. Generateur != verificateur : `certifier_impossibilite` (O(1)) != BFS exhaustif (O(V + E) sur la boule visitée, ici O(576 + 3 456) par palier). **OUI**.\n", "3. Temoin certifie : verdict IMPOSSIBLE porte certificat analytique (d_row, d_col, k_max). **OUI**.\n", "4. Cas sans solution = temoin d'impossibilite, pas silence : section 4 verifie par BFS tronque. Section 6 : plus de la moitie des paires a k_max=5 sont IMPOSSIBLE. **OUI**.\n", @@ -1127,8 +1127,8 @@ "end_time": "2026-09-02T13:23:36.794320+00:00", "environment_variables": {}, "exception": null, - "input_path": "GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb", - "output_path": "GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb", + "input_path": "GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb", + "output_path": "GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb", "parameters": {}, "start_time": "2026-09-02T13:23:33.184445+00:00", "version": "2.7.0" diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-25-Loi-II-Translateur-Life.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-21-Loi-II-Translateur-Life.ipynb similarity index 98% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-25-Loi-II-Translateur-Life.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-21-Loi-II-Translateur-Life.ipynb index 37432961a7..0f7f9f8a42 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-25-Loi-II-Translateur-Life.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-21-Loi-II-Translateur-Life.ipynb @@ -14,9 +14,9 @@ "tags": [] }, "source": [ - "# GameTheory-25 — Loi II, seconde jambe : synthétiser un translateur Life\n", + "# GameTheory-21 — Loi II, seconde jambe : synthétiser un translateur Life\n", "\n", - "`GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb` a livré la **première jambe de la Loi II** : un *constructeur* produit un chemin dans un graphe de swaps 2x2, un *vérificateur séparé* re-dérive indépendamment sa validité **et** sa minimalité, avec trois verdicts distincts. Une loi attestée sur **un** substrat reste une observation sur ce substrat. Ce notebook porte la même loi sur un substrat **génui­nement différent** — le Jeu de la Vie de Conway — pour les trois axes où elle peut casser autrement :\n", + "`GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb` a livré la **première jambe de la Loi II** : un *constructeur* produit un chemin dans un graphe de swaps 2x2, un *vérificateur séparé* re-dérive indépendamment sa validité **et** sa minimalité, avec trois verdicts distincts. Une loi attestée sur **un** substrat reste une observation sur ce substrat. Ce notebook porte la même loi sur un substrat **génui­nement différent** — le Jeu de la Vie de Conway — pour les trois axes où elle peut casser autrement :\n", "\n", "1. l'espace de recherche n'est **pas fini-énumérable à la main** (aucun graphe pré-construit) ;\n", "2. le témoin est un **objet** (une configuration de cellules), pas un chemin ;\n", @@ -30,7 +30,7 @@ "\n", "et, quand il n'y a **pas** de témoin, un **certificat d'impossibilité** — jamais un silence. Un générateur qui ne rend rien est indiscernable d'un générateur débranché.\n", "\n", - "**Rappel de la Loi II** : une claim sur un système combinatoire ne vaut que si elle s'accompagne d'un *téoin* — un objet fini que quiconque peut inspecter — et d'un *vérificateur indépendant* capable de dire non. GT-24 l'avait instanciée avec trois verdicts (`VALIDE + MINIMAL` / `VALIDE mais NON MINIMAL` / `INVALIDE`) : le vérificateur y re-dérivait la minimalité par un BFS exhaustif sur le graphe entier. Ici le vérificateur ne re-dérive rien de combinatoire : il **re-joue la physique**. C'est un test beaucoup plus faible à construire — une dizaine de lignes de simulation — et pourtant il suffit à attester, parce que la définition d'un translateur *est* un énoncé de simulation. Le choix du substrat fait qu'ici, la vérification la plus simple possible est aussi la bonne.\n" + "**Rappel de la Loi II** : une claim sur un système combinatoire ne vaut que si elle s'accompagne d'un *téoin* — un objet fini que quiconque peut inspecter — et d'un *vérificateur indépendant* capable de dire non. GT-20 l'avait instanciée avec trois verdicts (`VALIDE + MINIMAL` / `VALIDE mais NON MINIMAL` / `INVALIDE`) : le vérificateur y re-dérivait la minimalité par un BFS exhaustif sur le graphe entier. Ici le vérificateur ne re-dérive rien de combinatoire : il **re-joue la physique**. C'est un test beaucoup plus faible à construire — une dizaine de lignes de simulation — et pourtant il suffit à attester, parce que la définition d'un translateur *est* un énoncé de simulation. Le choix du substrat fait qu'ici, la vérification la plus simple possible est aussi la bonne.\n" ] }, { @@ -442,7 +442,7 @@ "\n", "Reculons d'un cran sur la même famille : demandons $(2, 0)$ à la période 4 — soit **exactement $c/2$**, la limite physique elle-même. Cette vitesse est *réalisable* : le **LWSS** (*lightweight spaceship*) l'atteint. Mais il a **9 cellules** — hors de notre famille $\\le 5$.\n", "\n", - "La recherche sur la même famille rend donc **vide aussi** — et voilà le piège : un certificat d'épuisement ne prouve **jamais** l'impossibilité globale. Il prouve l'absence *dans la famille*. $(2,0,3)$ est vide pour une raison **physique** ; $(2,0,4)$ est vide pour une raison de **famille** — et les deux vidanges se ressemblent exactement dans les logs. Ce qui les distingue n'est pas la sortie du programme, c'est la connaissance externe du substrat. C'est LA leçon que ce substrat apporte et que le graphe 2x2 de GT-24 ne pouvait pas montrer aussi nettement.\n" + "La recherche sur la même famille rend donc **vide aussi** — et voilà le piège : un certificat d'épuisement ne prouve **jamais** l'impossibilité globale. Il prouve l'absence *dans la famille*. $(2,0,3)$ est vide pour une raison **physique** ; $(2,0,4)$ est vide pour une raison de **famille** — et les deux vidanges se ressemblent exactement dans les logs. Ce qui les distingue n'est pas la sortie du programme, c'est la connaissance externe du substrat. C'est LA leçon que ce substrat apporte et que le graphe 2x2 de GT-20 ne pouvait pas montrer aussi nettement.\n" ] }, { @@ -538,7 +538,7 @@ "\n", "Un lot tout-vert est indiscernable d'un vérificateur débranché. Pour prouver que le vérificateur **peut dire non**, on lui injecte des claims fausses sur des motifs vrais : le glider annoncé à une vitesse qu'il n'a pas, à une période qu'il n'a pas, dans une direction qu'il ne prend pas. Chacune doit être rejetée, avec la raison du rejet.\n", "\n", - "Ce contrôle est l'analogue exact du détour construit exprès dans GT-24 : là, le vérificateur devait attraper un chemin volontairement non minimal ; ici, il doit attraper une claim volontairement fausse sur un objet réel. Dans les deux cas, le test du négatif est ce qui distingue un vérificateur qui **atteste** d'un vérificateur qui **acquiesce**. Notez que chaque rejet vient avec sa raison (déplacement observé, mutation, boîte modifiée) — un verdict non motivé serait aussi peu auditable qu'un silence.\n" + "Ce contrôle est l'analogue exact du détour construit exprès dans GT-20 : là, le vérificateur devait attraper un chemin volontairement non minimal ; ici, il doit attraper une claim volontairement fausse sur un objet réel. Dans les deux cas, le test du négatif est ce qui distingue un vérificateur qui **atteste** d'un vérificateur qui **acquiesce**. Notez que chaque rejet vient avec sa raison (déplacement observé, mutation, boîte modifiée) — un verdict non motivé serait aussi peu auditable qu'un silence.\n" ] }, { @@ -774,14 +774,14 @@ "\n", "La Loi II tient sur ce second substrat, et elle y prend un sens neuf. Ce qui a tenu : la chaîne **générateur → témoin → vérificateur indépendant**, les contrôles positifs (le témoin faux rejeté), l'exigence d'un rendu positif sur le cas sans solution. Ce qui a changé de visage :\n", "\n", - "- le témoin est un **objet** — une configuration de cellules — que l'épuisement fait sortir d'une famille finie, quand GT-24 extrayait un chemin d'un graphe déjà construit ;\n", + "- le témoin est un **objet** — une configuration de cellules — que l'épuisement fait sortir d'une famille finie, quand GT-20 extrayait un chemin d'un graphe déjà construit ;\n", "- la vérification est une **simulation** : le vérificateur re-joue la physique, il ne re-dérive pas une structure combinatoire ;\n", "- l'impossibilité est un **certificat scopé** dont la portée doit être écrite — et le couple $(2,0,3)/(2,0,4)$ montre que deux vidanges de famille identiques en apparence ont deux raisons radicalement différentes, physique pour l'une, artefact de famille pour l'autre.\n", "\n", "Une loi attestée sur deux substrats genui­nement différents commence à être une loi. La jambe Lean, si elle vient, sera un variant `-b`. En attendant, le lecteur retiendra le geste complet : **une demande, deux organes, trois issues** — témoin trouvé et attesté ; échec rendu comme certificat scopé ; claim fausse rejetée avec sa raison. Tout le reste — la boîte, la population, la période — n'est que le réglage fin de la famille dans laquelle on accepte de chercher.\n", "\n", "\n", - "*Voir #12387 (grain) et #12205 (Chantier 2 — génération de témoins). Série : GT-24 `GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb`.*\n" + "*Voir #12387 (grain) et #12205 (Chantier 2 — génération de témoins). Série : GT-20 `GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb`.*\n" ] } ], @@ -809,8 +809,8 @@ "end_time": "2026-08-22T22:19:15.897162+00:00", "environment_variables": {}, "exception": null, - "input_path": "MyIA.AI.Notebooks/GameTheory/GameTheory-25-Loi-II-Translateur-Life.ipynb", - "output_path": "MyIA.AI.Notebooks/GameTheory/GameTheory-25-Loi-II-Translateur-Life.ipynb", + "input_path": "MyIA.AI.Notebooks/GameTheory/GameTheory-21-Loi-II-Translateur-Life.ipynb", + "output_path": "MyIA.AI.Notebooks/GameTheory/GameTheory-21-Loi-II-Translateur-Life.ipynb", "parameters": {}, "start_time": "2026-08-22T22:18:45.117407+00:00", "version": "2.7.0" diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb similarity index 99% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb index 9cbb4f5058..7d8d17e883 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb @@ -5,7 +5,7 @@ "id": "af420a68", "metadata": {}, "source": [ - "# GameTheory-26 — Ensembles limites : Poincaré-Bendixson en dimension 2\n", + "# GameTheory-22 — Ensembles limites : Poincaré-Bendixson en dimension 2\n", "\n", "Où peut bien finir une trajectoire d'apprentissage ? Posée dans le plan, la question a une réponse fermée : le théorème de **Poincaré-Bendixson** dit qu'un champ de vecteurs planaire ne peut faire qu'une de trois choses à une trajectoire — la faire converger vers un point, la faire tourner en boucle fermée, ou la faire coller à un cycle de coins. Pas de quatrième issue, et en particulier : **pas de chaos**.\n", "\n", diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-27-Munkres-Assignment.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-23-Munkres-Assignment.ipynb similarity index 99% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-27-Munkres-Assignment.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-23-Munkres-Assignment.ipynb index a21567a491..15106c4d88 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-27-Munkres-Assignment.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-23-Munkres-Assignment.ipynb @@ -14,7 +14,7 @@ "tags": [] }, "source": [ - "# GameTheory-27 — L'algorithme de Kuhn-Munkres : affectation optimale et dualité\n", + "# GameTheory-23 — L'algorithme de Kuhn-Munkres : affectation optimale et dualité\n", "\n", "> **Hommage.** James R. Munkres, professeur émérite au MIT, s'est éteint le 30 juillet 2026 à 95 ans.\n", "> Si son nom est familier des générations d'étudiants par son manuel de topologie (*Topology*,\n", @@ -1105,9 +1105,9 @@ "- `scipy.optimize.linear_sum_assignment` — la référence SOTA (Jonker-Volgenant).\n", "- Hommage MIT News (13 août 2026) : « James Munkres, mathematician, musician and gardener ».\n", "\n", - "**Navigation série** : [GameTheory-24](./GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb) ·\n", - "[GameTheory-25](./GameTheory-25-Loi-II-Translateur-Life.ipynb) ·\n", - "[GameTheory-26](./GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb) · **GT-27**" + "**Navigation série** : [GameTheory-20](./GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb) ·\n", + "[GameTheory-21](./GameTheory-21-Loi-II-Translateur-Life.ipynb) ·\n", + "[GameTheory-22](./GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb) · **GT-23**" ] } ], @@ -1135,8 +1135,8 @@ "end_time": "2026-08-23T15:01:03.787151+00:00", "environment_variables": {}, "exception": null, - "input_path": "GameTheory-27-Munkres-Assignment.ipynb", - "output_path": "GameTheory-27-Munkres-Assignment.ipynb", + "input_path": "GameTheory-23-Munkres-Assignment.ipynb", + "output_path": "GameTheory-23-Munkres-Assignment.ipynb", "parameters": {}, "start_time": "2026-08-23T15:00:58.600721+00:00", "version": "2.7.0" diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-27b-Lean-Assignment-Native.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-23b-Lean-Assignment-Native.ipynb similarity index 99% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-27b-Lean-Assignment-Native.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-23b-Lean-Assignment-Native.ipynb index c453d72a7a..3d99a289f8 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-27b-Lean-Assignment-Native.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-23b-Lean-Assignment-Native.ipynb @@ -16,11 +16,11 @@ "source": [ "# GameTheory 27b — Le lake `assignment_lean` par son certificat (compagnon natif)\n", "\n", - "**Navigation** : [<< 27-Munkres-Assignment (track principal)](GameTheory-27-Munkres-Assignment.ipynb) | [Index](README.md) | [`assignment_lean/`](assignment_lean/)\n", + "**Navigation** : [<< 27-Munkres-Assignment (track principal)](GameTheory-23-Munkres-Assignment.ipynb) | [Index](README.md) | [`assignment_lean/`](assignment_lean/)\n", "\n", - "**Autruche pédagogique** : le notebook [GT-27](GameTheory-27-Munkres-Assignment.ipynb) *implémente* la méthode hongroise en Python (labels, arbre hongrois, `scipy.optimize.linear_sum_assignment`) et vérifie son triple test **numériquement**. Ce compagnon 27b exécute le **lake Lean `assignment_lean`** sous kernel natif `lean4-wsl` : les mêmes énoncés — dualité faible, certificat à gap nul, invariant de sortie, resserrement hongrois — deviennent des théorèmes **prouvés par le noyau**, et nous terminons sur une preuve complète qu'une affectation concrète 3×3 est optimale, sans énumérer les factorielles.\n", + "**Autruche pédagogique** : le notebook [GT-23](GameTheory-23-Munkres-Assignment.ipynb) *implémente* la méthode hongroise en Python (labels, arbre hongrois, `scipy.optimize.linear_sum_assignment`) et vérifie son triple test **numériquement**. Ce compagnon 27b exécute le **lake Lean `assignment_lean`** sous kernel natif `lean4-wsl` : les mêmes énoncés — dualité faible, certificat à gap nul, invariant de sortie, resserrement hongrois — deviennent des théorèmes **prouvés par le noyau**, et nous terminons sur une preuve complète qu'une affectation concrète 3×3 est optimale, sans énumérer les factorielles.\n", "\n", - "Hommage : James R. Munkres (1930–2026), co-éponyme de l'algorithme de Kuhn-Munkres (Kuhn 1955 ; Munkres 1957) — le récit historique est dans le [GT-27](GameTheory-27-Munkres-Assignment.ipynb)." + "Hommage : James R. Munkres (1930–2026), co-éponyme de l'algorithme de Kuhn-Munkres (Kuhn 1955 ; Munkres 1957) — le récit historique est dans le [GT-23](GameTheory-23-Munkres-Assignment.ipynb)." ] }, { @@ -383,7 +383,7 @@ "***\n", "## 2. Le problème sur un exemple fil rouge : trois agents, trois tâches\n", "\n", - "Toute la suite travaille sur la **même matrice de coûts 3×3** — celle de la section 2 de GT-27 : l'agent $i$ coûte $C_{ij}$ à affecter à la tâche $j$, et on **minimise** le coût total. En Lean, la matrice est une fonction `Fin 3 → Fin 3 → ℤ` (arithmétique entière exacte, comme l'algorithme pédagogique de GT-27) :" + "Toute la suite travaille sur la **même matrice de coûts 3×3** — celle de la section 2 de GT-23 : l'agent $i$ coûte $C_{ij}$ à affecter à la tâche $j$, et on **minimise** le coût total. En Lean, la matrice est une fonction `Fin 3 → Fin 3 → ℤ` (arithmétique entière exacte, comme l'algorithme pédagogique de GT-23) :" ] }, { @@ -419,7 +419,7 @@ " \n", " \n", "
\n", - "
-- La matrice des couts de GT-27 (section 2)
\n", + "
-- La matrice des couts de GT-23 (section 2)
\n", "
def C3 : Fin 3 → Fin 3 → ℤ := ![![4, 1, 3], ![2, 0, 5], ![3, 2, 2]]
\n", "
\n", "
1
\n", @@ -428,7 +428,7 @@ "
\n", "
\n", " Raw input\n", - " {\"cmd\": \"-- La matrice des couts de GT-27 (section 2)\\ndef C3 : Fin 3 \\u2192 Fin 3 \\u2192 \\u2124 := ![![4, 1, 3], ![2, 0, 5], ![3, 2, 2]]\\n\\n#eval C3 0 1 -- cout d'affecter l'agent 0 a la tache 1\\n#eval C3 1 2 -- cout d'affecter l'agent 1 a la tache 2\", \"env\": 1}\n", + " {\"cmd\": \"-- La matrice des couts de GT-23 (section 2)\\ndef C3 : Fin 3 \\u2192 Fin 3 \\u2192 \\u2124 := ![![4, 1, 3], ![2, 0, 5], ![3, 2, 2]]\\n\\n#eval C3 0 1 -- cout d'affecter l'agent 0 a la tache 1\\n#eval C3 1 2 -- cout d'affecter l'agent 1 a la tache 2\", \"env\": 1}\n", "
\n", "
\n", " Raw output\n", @@ -446,7 +446,7 @@ " " ], "text/plain": [ - "-- La matrice des couts de GT-27 (section 2)\n", + "-- La matrice des couts de GT-23 (section 2)\n", "def C3 : Fin 3 → Fin 3 → ℤ := ![![4, 1, 3], ![2, 0, 5], ![3, 2, 2]]\n", "\n", "#eval C3 0 1 -- cout d'affecter l'agent 0 a la tache 1\n", @@ -456,7 +456,7 @@ "--% env 2\n", "\n", "Raw input:\n", - "{\"cmd\": \"-- La matrice des couts de GT-27 (section 2)\\ndef C3 : Fin 3 \\u2192 Fin 3 \\u2192 \\u2124 := ![![4, 1, 3], ![2, 0, 5], ![3, 2, 2]]\\n\\n#eval C3 0 1 -- cout d'affecter l'agent 0 a la tache 1\\n#eval C3 1 2 -- cout d'affecter l'agent 1 a la tache 2\", \"env\": 1}\n", + "{\"cmd\": \"-- La matrice des couts de GT-23 (section 2)\\ndef C3 : Fin 3 \\u2192 Fin 3 \\u2192 \\u2124 := ![![4, 1, 3], ![2, 0, 5], ![3, 2, 2]]\\n\\n#eval C3 0 1 -- cout d'affecter l'agent 0 a la tache 1\\n#eval C3 1 2 -- cout d'affecter l'agent 1 a la tache 2\", \"env\": 1}\n", "Raw output:\n", "{\"messages\":\r\n", " [{\"severity\": \"info\",\r\n", @@ -475,7 +475,7 @@ } ], "source": [ - "-- La matrice des couts de GT-27 (section 2)\n", + "-- La matrice des couts de GT-23 (section 2)\n", "def C3 : Fin 3 → Fin 3 → ℤ := ![![4, 1, 3], ![2, 0, 5], ![3, 2, 2]]\n", "\n", "#eval C3 0 1 -- cout d'affecter l'agent 0 a la tache 1\n", @@ -676,7 +676,7 @@ "***\n", "## 4. Le couple dual (`Assignment.DualFeasible`, `Assignment.dualValue`)\n", "\n", - "La lecture LP du problème (GT-27 section 3) associe à chaque ligne $i$ un potentiel $u_i$ et à chaque colonne $j$ un potentiel $v_j$. Le couple est **dual-réalisable** si $u_i + v_j \\leq C_{ij}$ pour toute paire — ce sont exactement les *labels* de l'algorithme hongrois. Sa valeur duale est $\\sum_i u_i + \\sum_j v_j$ :" + "La lecture LP du problème (GT-23 section 3) associe à chaque ligne $i$ un potentiel $u_i$ et à chaque colonne $j$ un potentiel $v_j$. Le couple est **dual-réalisable** si $u_i + v_j \\leq C_{ij}$ pour toute paire — ce sont exactement les *labels* de l'algorithme hongrois. Sa valeur duale est $\\sum_i u_i + \\sum_j v_j$ :" ] }, { @@ -1477,7 +1477,7 @@ "source": [ "### Lecture du résultat\n", "\n", - "Le couple resserré reste dual-réalisable — et sa valeur duale a **crû** de 0 à 3 : le resserrement est le moteur de la montée duale de l'algorithme (GT-27 section 4), et `dualFeasible_tighten` en est la garantie de sûreté, prouvée une fois pour toutes." + "Le couple resserré reste dual-réalisable — et sa valeur duale a **crû** de 0 à 3 : le resserrement est le moteur de la montée duale de l'algorithme (GT-23 section 4), et `dualFeasible_tighten` en est la garantie de sûreté, prouvée une fois pour toutes." ] }, { @@ -1732,7 +1732,7 @@ "
-- example : DualFeasible C3 uTightBis vTightBis :=
\n", "
--   Assignment.dualFeasible_tighten C3 u0 v0 {1} ∅ (by decide) .. (by decide) (by decide)
\n", "
-- Etape 3 : expliquer (en markdown) pourquoi une marge nulle bloque la montee duale,
\n", - "
-- et ce que l'algorithme fait alors (indice : GT-27 section 4, colonne entree dans T).
\n", + "
-- et ce que l'algorithme fait alors (indice : GT-23 section 4, colonne entree dans T).
\n", "
--
\n", "
-- TODO etudiant
\n", "
example : True := trivial
\n", @@ -1740,7 +1740,7 @@ "
\n", "
\n", " Raw input\n", - " {\"cmd\": \"-- Exercice 3 (KuhnMunkres) : le resserrement maximal sur la ligne 1.\\n-- Partir du couple trivial u0 = v0 = 0 (section 9) et resserrer S = {1} avec T = \\u2205 :\\n-- les aretes sortantes sont (1, 0), (1, 1), (1, 2) de marges 2, 0, 5.\\n--\\n-- Etape 1 : quel est le delta maximal admissible ? (reponse attendue : 0 \\u2014\\n-- la marge nulle de l'arete (1, 1) plafonne le resserrement).\\n-- Etape 2 : le verifier en executant dualFeasible_tighten avec ce delta\\n-- (structure de la section 9, avec S = {1} et T = \\u2205) :\\n-- example : DualFeasible C3 uTightBis vTightBis :=\\n-- Assignment.dualFeasible_tighten C3 u0 v0 {1} \\u2205 (by decide) .. (by decide) (by decide)\\n-- Etape 3 : expliquer (en markdown) pourquoi une marge nulle bloque la montee duale,\\n-- et ce que l'algorithme fait alors (indice : GT-27 section 4, colonne entree dans T).\\n--\\n-- TODO etudiant\\nexample : True := trivial\", \"env\": 11}\n", + " {\"cmd\": \"-- Exercice 3 (KuhnMunkres) : le resserrement maximal sur la ligne 1.\\n-- Partir du couple trivial u0 = v0 = 0 (section 9) et resserrer S = {1} avec T = \\u2205 :\\n-- les aretes sortantes sont (1, 0), (1, 1), (1, 2) de marges 2, 0, 5.\\n--\\n-- Etape 1 : quel est le delta maximal admissible ? (reponse attendue : 0 \\u2014\\n-- la marge nulle de l'arete (1, 1) plafonne le resserrement).\\n-- Etape 2 : le verifier en executant dualFeasible_tighten avec ce delta\\n-- (structure de la section 9, avec S = {1} et T = \\u2205) :\\n-- example : DualFeasible C3 uTightBis vTightBis :=\\n-- Assignment.dualFeasible_tighten C3 u0 v0 {1} \\u2205 (by decide) .. (by decide) (by decide)\\n-- Etape 3 : expliquer (en markdown) pourquoi une marge nulle bloque la montee duale,\\n-- et ce que l'algorithme fait alors (indice : GT-23 section 4, colonne entree dans T).\\n--\\n-- TODO etudiant\\nexample : True := trivial\", \"env\": 11}\n", "
\n", "
\n", " Raw output\n", @@ -1760,14 +1760,14 @@ "-- example : DualFeasible C3 uTightBis vTightBis :=\n", "-- Assignment.dualFeasible_tighten C3 u0 v0 {1} ∅ (by decide) .. (by decide) (by decide)\n", "-- Etape 3 : expliquer (en markdown) pourquoi une marge nulle bloque la montee duale,\n", - "-- et ce que l'algorithme fait alors (indice : GT-27 section 4, colonne entree dans T).\n", + "-- et ce que l'algorithme fait alors (indice : GT-23 section 4, colonne entree dans T).\n", "--\n", "-- TODO etudiant\n", "example : True := trivial\n", "--% env 12\n", "\n", "Raw input:\n", - "{\"cmd\": \"-- Exercice 3 (KuhnMunkres) : le resserrement maximal sur la ligne 1.\\n-- Partir du couple trivial u0 = v0 = 0 (section 9) et resserrer S = {1} avec T = \\u2205 :\\n-- les aretes sortantes sont (1, 0), (1, 1), (1, 2) de marges 2, 0, 5.\\n--\\n-- Etape 1 : quel est le delta maximal admissible ? (reponse attendue : 0 \\u2014\\n-- la marge nulle de l'arete (1, 1) plafonne le resserrement).\\n-- Etape 2 : le verifier en executant dualFeasible_tighten avec ce delta\\n-- (structure de la section 9, avec S = {1} et T = \\u2205) :\\n-- example : DualFeasible C3 uTightBis vTightBis :=\\n-- Assignment.dualFeasible_tighten C3 u0 v0 {1} \\u2205 (by decide) .. (by decide) (by decide)\\n-- Etape 3 : expliquer (en markdown) pourquoi une marge nulle bloque la montee duale,\\n-- et ce que l'algorithme fait alors (indice : GT-27 section 4, colonne entree dans T).\\n--\\n-- TODO etudiant\\nexample : True := trivial\", \"env\": 11}\n", + "{\"cmd\": \"-- Exercice 3 (KuhnMunkres) : le resserrement maximal sur la ligne 1.\\n-- Partir du couple trivial u0 = v0 = 0 (section 9) et resserrer S = {1} avec T = \\u2205 :\\n-- les aretes sortantes sont (1, 0), (1, 1), (1, 2) de marges 2, 0, 5.\\n--\\n-- Etape 1 : quel est le delta maximal admissible ? (reponse attendue : 0 \\u2014\\n-- la marge nulle de l'arete (1, 1) plafonne le resserrement).\\n-- Etape 2 : le verifier en executant dualFeasible_tighten avec ce delta\\n-- (structure de la section 9, avec S = {1} et T = \\u2205) :\\n-- example : DualFeasible C3 uTightBis vTightBis :=\\n-- Assignment.dualFeasible_tighten C3 u0 v0 {1} \\u2205 (by decide) .. (by decide) (by decide)\\n-- Etape 3 : expliquer (en markdown) pourquoi une marge nulle bloque la montee duale,\\n-- et ce que l'algorithme fait alors (indice : GT-23 section 4, colonne entree dans T).\\n--\\n-- TODO etudiant\\nexample : True := trivial\", \"env\": 11}\n", "Raw output:\n", "{\"env\": 12}" ] @@ -1788,7 +1788,7 @@ "-- example : DualFeasible C3 uTightBis vTightBis :=\n", "-- Assignment.dualFeasible_tighten C3 u0 v0 {1} ∅ (by decide) .. (by decide) (by decide)\n", "-- Etape 3 : expliquer (en markdown) pourquoi une marge nulle bloque la montee duale,\n", - "-- et ce que l'algorithme fait alors (indice : GT-27 section 4, colonne entree dans T).\n", + "-- et ce que l'algorithme fait alors (indice : GT-23 section 4, colonne entree dans T).\n", "--\n", "-- TODO etudiant\n", "example : True := trivial" @@ -1811,15 +1811,15 @@ "***\n", "## Conclusion\n", "\n", - "Le lake `assignment_lean` donne au triple test de GT-27 son statut logique complet : la **dualité faible** fixe le plancher, le **gap nul** le certificat, l'**invariant de sortie** la garantie de terminaison correcte, le **resserrement** la sûreté de chaque étape. Là où `scipy.optimize.linear_sum_assignment` rend une affectation, le lake rend une **preuve vérifiée par le noyau** — et le compagnon 27b est le lieu où ces preuves s'exécutent et se lisent.\n", + "Le lake `assignment_lean` donne au triple test de GT-23 son statut logique complet : la **dualité faible** fixe le plancher, le **gap nul** le certificat, l'**invariant de sortie** la garantie de terminaison correcte, le **resserrement** la sûreté de chaque étape. Là où `scipy.optimize.linear_sum_assignment` rend une affectation, le lake rend une **preuve vérifiée par le noyau** — et le compagnon 27b est le lieu où ces preuves s'exécutent et se lisent.\n", "\n", - "Ce qui reste délibérément hors scope du lake (cf son README) : la preuve de **terminaison** et la complexité $O(n^3)$ (Edmonds-Karp/Tomizawa), et le pont **Shapley-Shubik** (cœur du jeu d'affectation = solutions duales optimales), traité numériquement dans GT-27 section 5.\n", + "Ce qui reste délibérément hors scope du lake (cf son README) : la preuve de **terminaison** et la complexité $O(n^3)$ (Edmonds-Karp/Tomizawa), et le pont **Shapley-Shubik** (cœur du jeu d'affectation = solutions duales optimales), traité numériquement dans GT-23 section 5.\n", "\n", "## Références\n", "\n", "- Lake : [`assignment_lean/`](assignment_lean/) — README (charpente, hors-scope, build), issue #12598 (hommage Munkres 1930–2026)\n", "- Kuhn (1955), *The Hungarian Method for the Assignment Problem*, Naval Research Logistics Quarterly 2:83–97 ; Munkres (1957), *Algorithms for the Assignment and Transportation Problems*, J. SIAM 5(1):32–38\n", - "- Implémentation SOTA : `scipy.optimize.linear_sum_assignment` (GT-27 section 2)\n", + "- Implémentation SOTA : `scipy.optimize.linear_sum_assignment` (GT-23 section 2)\n", "- EPIC #11703 — visibilité des lakes Lean dans les notebooks (ce compagnon est le volet natif de `assignment_lean`)" ] } @@ -1842,8 +1842,8 @@ "end_time": "2026-08-25T07:40:53.830376", "environment_variables": {}, "exception": null, - "input_path": "../GameTheory-27b-Lean-Assignment-Native.ipynb", - "output_path": "../GameTheory-27b-Lean-Assignment-Native.ipynb", + "input_path": "../GameTheory-23b-Lean-Assignment-Native.ipynb", + "output_path": "../GameTheory-23b-Lean-Assignment-Native.ipynb", "parameters": {}, "start_time": "2026-08-25T07:35:04.426693", "version": "2.6.0" diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-28-Humour-Banc.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-24-Humour-Banc.ipynb similarity index 99% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-28-Humour-Banc.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-24-Humour-Banc.ipynb index faa7b844c7..262cc1ef8f 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-28-Humour-Banc.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-24-Humour-Banc.ipynb @@ -7,7 +7,7 @@ "tags": [] }, "source": [ - "# GameTheory-28 : Banc de calibration — humour, forme partagée vs stimulus\n", + "# GameTheory-24 : Banc de calibration — humour, forme partagée vs stimulus\n", "\n", "Le « troisième voyage » du dépôt suit un schéma commun :\n", "\n", diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb similarity index 99% rename from MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb rename to MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb index 3d159286ec..cb70ee1361 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb @@ -14,9 +14,9 @@ "tags": [] }, "source": [ - "# GameTheory-28b : Banc humour — passer à l'échelle\n", + "# GameTheory-24b : Banc humour — passer à l'échelle\n", "\n", - "Suite du banc toy `GameTheory-28-Humour-Banc.ipynb` : le modèle passe ici du banc d'essai à l'échelle — matériel Argumentum, corpus annoté de blagues bonnes et mauvaises, et expériences de ranking avec de vrais LLM pour la partie « organique ».\n", + "Suite du banc toy `GameTheory-24-Humour-Banc.ipynb` : le modèle passe ici du banc d'essai à l'échelle — matériel Argumentum, corpus annoté de blagues bonnes et mauvaises, et expériences de ranking avec de vrais LLM pour la partie « organique ».\n", "\n", "## Trois sources de données\n", "\n", @@ -33,7 +33,7 @@ " contexte 262144) sur `192.168.0.47:5002/v1`. Cible : reproduire le verdict\n", " « humour_reussi » vs alternatives sur un sous-ensemble de 30 instances.\n", "\n", - "**Placement** : GT-28b, à côté de GT-28 — la sous-série humour reste dans l'arc GameTheory, à proximité de Sandholm qui nourrit son axe de réflexion.\n" + "**Placement** : GT-24b, à côté de GT-24 — la sous-série humour reste dans l'arc GameTheory, à proximité de Sandholm qui nourrit son axe de réflexion.\n" ] }, { @@ -79,7 +79,7 @@ "from collections import Counter\n", "from pathlib import Path\n", "\n", - "# Catégories du banc — alignées sur le toy model GT-28\n", + "# Catégories du banc — alignées sur le toy model GT-24\n", "CATEGORIES = [\n", " \"humour_reussi\", # rire + recadrage partagé\n", " \"rire_sans_recadrage\", # rire mais pas de partage (chatouille, contagion, nerveux)\n", @@ -117,7 +117,7 @@ "source": [ "### Lecture — trois choix de configuration qui cadrent toute l'expérience\n", "\n", - "- **`CATEGORIES` en 5 cellules** réplique la taxonomie du toy model GT-28 (`humour_reussi`, `rire_sans_recadrage`, `recadrage_sans_rire`, `offensif_compris_non_partage`, `rien`). La 5ᵉ cellule `rien` est celle qui rend le banc discriminant : un détecteur qui répond toujours `humour_reussi` ne peut plus tricher.\n", + "- **`CATEGORIES` en 5 cellules** réplique la taxonomie du toy model GT-24 (`humour_reussi`, `rire_sans_recadrage`, `recadrage_sans_rire`, `offensif_compris_non_partage`, `rien`). La 5ᵉ cellule `rien` est celle qui rend le banc discriminant : un détecteur qui répond toujours `humour_reussi` ne peut plus tricher.\n", "- **`OPENAI_COMPAT_URL` pointe sur `192.168.0.47:5002/v1`** — un endpoint vLLM local, pas un service cloud. C'est ce qui rend le verdict SOTA `RECOVERABLE-LOCAL` (cell[0]) : l'outil est invoqué, pas simulé.\n", "- **`OPENAI_API_KEY` est une clef locale vLLM, pas un secret production** — le commentaire le dit explicitement, et la valeur n'a pas vocation à être rotée comme une clef OpenAI. C'est la convention pour un service maison sans authentification réelle.\n", "\n", @@ -641,7 +641,7 @@ "source": [ "## Détecteurs — règle vs partage (toy model, répliqué)\n", "\n", - "On reprend les deux détecteurs du toy model GT-28 :\n", + "On reprend les deux détecteurs du toy model GT-24 :\n", "\n", "- **naïf** : stimulus = rire → `humour_reussi` si `laugh`, sinon `rien`\n", "- **partage** : trois signaux (reframe + uptake + refus) → discrimination 5 cellules\n", @@ -1545,7 +1545,7 @@ "| ≥100 instances annotées, négatifs inclus, équilibré | **OK** | `len(CORPUS_DUR)` cell[7] |\n", "| ≥1 expérience ranking LLM, sorties committées | **OK** | cell[12] + cell[14] |\n", "| Matrices confusion mises à jour | **OK** | cell[14] : naïve vs partage vs LLM |\n", - "| Placement tranché et documenté | **OK** | GT-28b (comment user 15:46Z) |\n", + "| Placement tranché et documenté | **OK** | GT-24b (comment user 15:46Z) |\n", "\n", "### Verdict SOTA\n", "\n", @@ -1570,14 +1570,14 @@ "- **LLM ranking** limité à 30 instances pour éviter latence cumulée.\n", " Suite : full-corpus LLM en mode async (papermill --workers 4) sur machine\n", " GPU dédiée (po-2024 ?) pour 120+ instances en <5 min.\n", - "- **Placement** : GT-28b pour l'instant. La numérotation pourra basculer en\n", + "- **Placement** : GT-24b pour l'instant. La numérotation pourra basculer en\n", " annexes `-b`, `-c` une fois la sous-série stabilisée (comment user 15:46Z).\n", "\n", "### Crédits\n", "\n", "- Argumentum upstream : github.com/ArgumentumGames/Argumentum (master,\n", " `Cards/Scenarii/Argumentum Scenarii - Cards.csv`)\n", - "- Toy model livré par #12749 (PR `GameTheory-28-Humour-Banc.ipynb`)\n", + "- Toy model livré par #12749 (PR `GameTheory-24-Humour-Banc.ipynb`)\n", "- LLM : `qwen3.6-35b-a3b` (AWQ 4bit, vLLM local)\n" ] } @@ -1606,7 +1606,7 @@ "end_time": "2026-08-28T02:55:36.370708", "environment_variables": {}, "exception": null, - "input_path": "GameTheory-28b-Humour-Banc-Dur.ipynb", + "input_path": "GameTheory-24b-Humour-Banc-Dur.ipynb", "output_path": "gt28b_exec.ipynb", "parameters": {}, "start_time": "2026-08-28T02:55:27.894358", diff --git a/MyIA.AI.Notebooks/GameTheory/LEAN_INVENTORY.md b/MyIA.AI.Notebooks/GameTheory/LEAN_INVENTORY.md index d6b332f9f6..72a3f233b3 100644 --- a/MyIA.AI.Notebooks/GameTheory/LEAN_INVENTORY.md +++ b/MyIA.AI.Notebooks/GameTheory/LEAN_INVENTORY.md @@ -216,7 +216,7 @@ Folk (`folk_theorem_discounted`) porte 1 sorry stretch, toléré au titre de #48 ### 10. assignment_lean -**Objectif** : squelette de correction de l'algorithme d'affectation de Kuhn-Munkres (hongrois) — lake compagnon du notebook GameTheory-27-Munkres-Assignment, hommage à James R. Munkres (1930-2026). Issue #12598 (1/3). Le primal (matrice de coûts, couplage parfait, valeur), le dual (potentiels, faisabilité, **dualité faible**), le certificat d'optimalité à écart nul, et les invariants structurels de l'algorithme (graphe d'égalité, **invariant de sortie**, **le serrage hongrois préserve la faisabilité duale**). Terminaison et complexité O(n³) volontairement hors scope. +**Objectif** : squelette de correction de l'algorithme d'affectation de Kuhn-Munkres (hongrois) — lake compagnon du notebook GameTheory-23-Munkres-Assignment, hommage à James R. Munkres (1930-2026). Issue #12598 (1/3). Le primal (matrice de coûts, couplage parfait, valeur), le dual (potentiels, faisabilité, **dualité faible**), le certificat d'optimalité à écart nul, et les invariants structurels de l'algorithme (graphe d'égalité, **invariant de sortie**, **le serrage hongrois préserve la faisabilité duale**). Terminaison et complexité O(n³) volontairement hors scope. **Toolchain** : v4.32.1 | **Dépendances** : Mathlib4 diff --git a/MyIA.AI.Notebooks/GameTheory/README.md b/MyIA.AI.Notebooks/GameTheory/README.md index 1817d42ea1..26c160e941 100644 --- a/MyIA.AI.Notebooks/GameTheory/README.md +++ b/MyIA.AI.Notebooks/GameTheory/README.md @@ -303,17 +303,17 @@ flowchart TD ### Partie 4 : Strate 7 — extensions du vocabulaire stratégique (notebooks 18+) -La vague « strate 7 » étend la série au-delà du fil historique : chaque notebook y isole un geste qui **modifie l'espace des jeux** (abstraction, extension de vocabulaire) plutôt qu'une solution dans un jeu donné. Sa numérotation est volontairement non séquentielle : les numéros `18`, `19` et `24` à `28` désignent des grains autonomes livrés en parallèle, tandis que les suffixes littéraux `3a` à `3h` forment un chantier rattaché à GT-3 et à la géométrie ordinale de Robinson-Goforth. Ils ne sont donc ni des décimales ni des étapes à lire dans l'ordre de leur merge. +La vague « strate 7 » étend la série au-delà du fil historique : chaque notebook y isole un geste qui **modifie l'espace des jeux** (abstraction, extension de vocabulaire) plutôt qu'une solution dans un jeu donné. Sa numérotation est volontairement non séquentielle : les numéros `18`, `19` et `20` à `24` désignent des grains autonomes livrés en parallèle, tandis que les suffixes littéraux `3a` à `3h` forment un chantier rattaché à GT-3 et à la géométrie ordinale de Robinson-Goforth. Ils ne sont donc ni des décimales ni des étapes à lire dans l'ordre de leur merge. | # | Notebook | Kernel | Contenu | Durée | |---|----------|--------|---------|-------| | 18 | [GameTheory-18-Open-Games-et-Lentilles](GameTheory-18-Open-Games-et-Lentilles.ipynb) | Python | Open games et lentilles : une représentation locale qui modifie le contexte global dont elle est issue | 45 min | | 19 | [GameTheory-19-Abstraction-a-Dette](GameTheory-19-Abstraction-a-Dette.ipynb) | Python | Abstraction à dette mesurable : quantifier ce que perd une représentation simplifiée | 35 min | -| 24 | [GameTheory-24-Chemin-Minimal-Robinson-Goforth](GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb) | Python | Chemin minimal Robinson-Goforth : témoin construit par le générateur puis vérifié indépendamment | 45 min | -| 25 | [GameTheory-25-Loi-II-Translateur-Life](GameTheory-25-Loi-II-Translateur-Life.ipynb) | Python | Loi II : synthèse d'un translateur Life et certificat d'impossibilité lorsque la traduction échoue | 45 min | -| 26 | [GameTheory-26-Ensembles-Limites-Poincare-Bendixson](GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb) | Python | Ensembles limites : Poincaré-Bendixson en dimension 2 — les trois issues (point fixe, orbite périodique, cycle hétéroclinique) exécutées sur Prisonnier / Matching Pennies / RPS et classées par un détecteur mécanique (module compagnon + 16 tests), le mur $w = l$ de la famille RPS vérifié par linéarisation $(l-w)/6$ et relié aux chambres/murs du 3b, l'échec du théorème au-delà du plan comme conclusion (Czechowski-Piliouras 2021) | 45 min | -| 27 | [GameTheory-27-Munkres-Assignment](GameTheory-27-Munkres-Assignment.ipynb) | Python | Kuhn-Munkres en hommage à James Munkres († 2026) : l'affectation optimale from scratch en arithmétique entière exacte (arbre hongrois BFS, resserrement dual), confrontée à SciPy (50/50 instances identiques) et certifiée par le triple test LP (faisabilité duale, gap nul, arêtes d'égalité), le pont Shapley-Shubik (cœur = polytope dual, 254 coalitions testées, 0 violations), et le contraste Gale-Shapley (stabilité qui se paie +3 sur instance divergente seedée) | 45 min | -| 28 | [GameTheory-28-Humour-Banc](GameTheory-28-Humour-Banc.ipynb) | Python | Banc de calibration : humour, forme partagée vs stimulus — matrice de confusion du partage de forme (2 axes : rire, recadrage) | 45 min | +| 20 | [GameTheory-20-Chemin-Minimal-Robinson-Goforth](GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb) | Python | Chemin minimal Robinson-Goforth : témoin construit par le générateur puis vérifié indépendamment | 45 min | +| 21 | [GameTheory-21-Loi-II-Translateur-Life](GameTheory-21-Loi-II-Translateur-Life.ipynb) | Python | Loi II : synthèse d'un translateur Life et certificat d'impossibilité lorsque la traduction échoue | 45 min | +| 22 | [GameTheory-22-Ensembles-Limites-Poincare-Bendixson](GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb) | Python | Ensembles limites : Poincaré-Bendixson en dimension 2 — les trois issues (point fixe, orbite périodique, cycle hétéroclinique) exécutées sur Prisonnier / Matching Pennies / RPS et classées par un détecteur mécanique (module compagnon + 16 tests), le mur $w = l$ de la famille RPS vérifié par linéarisation $(l-w)/6$ et relié aux chambres/murs du 3b, l'échec du théorème au-delà du plan comme conclusion (Czechowski-Piliouras 2021) | 45 min | +| 23 | [GameTheory-23-Munkres-Assignment](GameTheory-23-Munkres-Assignment.ipynb) | Python | Kuhn-Munkres en hommage à James Munkres († 2026) : l'affectation optimale from scratch en arithmétique entière exacte (arbre hongrois BFS, resserrement dual), confrontée à SciPy (50/50 instances identiques) et certifiée par le triple test LP (faisabilité duale, gap nul, arêtes d'égalité), le pont Shapley-Shubik (cœur = polytope dual, 254 coalitions testées, 0 violations), et le contraste Gale-Shapley (stabilité qui se paie +3 sur instance divergente seedée) | 45 min | +| 24 | [GameTheory-24-Humour-Banc](GameTheory-24-Humour-Banc.ipynb) | Python | Banc de calibration : humour, forme partagée vs stimulus — matrice de confusion du partage de forme (2 axes : rire, recadrage) | 45 min | Les huit extensions `3a` à `3h` figurent dans la Partie 1, au voisinage du notebook GT-3 qu'elles prolongent. Elles couvrent respectivement les chemins de swaps, les chambres et murs, le joueur LLM, le plan de déformation, les méta-actions tarifées, le parcours complet, la dérivation quotient et les deux espèces de flèches. @@ -447,7 +447,7 @@ GameTheory occupe une place à part dans la couche Lean : c'est la famille qui a | **GameTheory** (matching) | `game_theory_lean` (StableMarriage) | Gale-Shapley : existence + optimalité côté proposant | Notebooks 16-2 (matching, Gale-Shapley) | | **GameTheory** (jeux répétés) | `game_theory_lean` (RepeatedGames — home canonique post-#6146, l'ancien lake `repeated_games_lean/` est coquille archive) | Stratégie grim-trigger **certifiée 0 sorry** (cf #4880) ; stretch restant : théorème Folk complet (`Folk.lean`, 1 sorry assumé) | Notebooks 6c (dérivation à la main) + 6b (compagnon lake, visibilité #11703) | | **GameTheory** (jeux combinatoires) | `conway_cgt_lean` | Visite guidée (`#check`) de la théorie des jeux combinatoires (Conway CGT) | Notebooks 8/8b/8d (CombinatorialGames, Sprague-Grundy, compagnon natif 8d) | -| **GameTheory** (affectation) | `assignment_lean` | Dualité faible + certificat d'optimalité à gap nul de la méthode hongroise (Kuhn-Munkres, #12598) | GameTheory-27-Munkres-Assignment | +| **GameTheory** (affectation) | `assignment_lean` | Dualité faible + certificat d'optimalité à gap nul de la méthode hongroise (Kuhn-Munkres, #12598) | GameTheory-23-Munkres-Assignment | | **GameTheory** (information asymétrique) | `asymmetric_information_lean` | Akerlof (lemons : seuil de pooling exact + monotonie), Spence (signaling), Rothschild-Stiglitz (screening), Wilson-Miyazaki — 0 sorry, gates CI | GameTheory-17c (companion natif) + 17b (simulation Python) | | **Search** (cross-famille) | `search_lean` (cf. `#4048`) | Consistance + heuristique admissible = optimalité | Search-13 (A*), branchement par preuve de correction | | **QuantConnect** (cross-famille) | `kelly_lean` (cf. `#4052`) | Kelly `g(f) ≤ g(f*)` + unicité | QC-Py-10 Risk Management, branchement par fraction risquée | @@ -874,11 +874,11 @@ GameTheory/ ├── GameTheory-09b-Commitment-Stackelberg.ipynb # Famille GT-09 : engagement contraignant ├── GameTheory-09c-Stackelberg-SecurityGame.ipynb # Famille GT-09 : security game à capteur imparfait ├── GameTheory-16d-Echange-de-Reins.ipynb # Famille GT-16 : cycles et chaînes d'échange -├── GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb -├── GameTheory-25-Loi-II-Translateur-Life.ipynb -├── GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb -├── GameTheory-27-Munkres-Assignment.ipynb -├── GameTheory-28-Humour-Banc.ipynb # Banc de calibration : humour, forme partagée vs stimulus +├── GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb +├── GameTheory-21-Loi-II-Translateur-Life.ipynb +├── GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb +├── GameTheory-23-Munkres-Assignment.ipynb +├── GameTheory-24-Humour-Banc.ipynb # Banc de calibration : humour, forme partagée vs stimulus ├── SocialChoice/ # Sous-série Choix Social (8 notebooks : 5 pères Python/Lean + 3 twins C#, parité #4956) │ ├── 01-Arrow-Impossibility-Theorem.ipynb │ ├── 01-Arrow-Impossibility-Theorem-Csharp.ipynb diff --git a/MyIA.AI.Notebooks/GameTheory/assignment_lean/README.md b/MyIA.AI.Notebooks/GameTheory/assignment_lean/README.md index 59415fe496..baccb2e3dd 100644 --- a/MyIA.AI.Notebooks/GameTheory/assignment_lean/README.md +++ b/MyIA.AI.Notebooks/GameTheory/assignment_lean/README.md @@ -1,6 +1,6 @@ # assignment_lean — Problème d'affectation (Kuhn-Munkres) -Lake compagnon du notebook [GameTheory-27-Munkres-Assignment.ipynb](../GameTheory-27-Munkres-Assignment.ipynb), hommage à James R. Munkres (1930-2026) — issue #12598 (1/3). +Lake compagnon du notebook [GameTheory-23-Munkres-Assignment.ipynb](../GameTheory-23-Munkres-Assignment.ipynb), hommage à James R. Munkres (1930-2026) — issue #12598 (1/3). Il formalise la **charpente de correction** de la méthode hongroise (Kuhn 1955, Munkres 1957) : diff --git a/MyIA.AI.Notebooks/GameTheory/assignment_lean/lakefile.lean b/MyIA.AI.Notebooks/GameTheory/assignment_lean/lakefile.lean index 4893991946..d6abc9e63c 100644 --- a/MyIA.AI.Notebooks/GameTheory/assignment_lean/lakefile.lean +++ b/MyIA.AI.Notebooks/GameTheory/assignment_lean/lakefile.lean @@ -2,7 +2,7 @@ import Lake open Lake DSL -- Hommage James R. Munkres (1930-2026), issue #12598 (1/3) : --- lake compagnon du notebook GameTheory-27-Munkres-Assignment.ipynb. +-- lake compagnon du notebook GameTheory-23-Munkres-Assignment.ipynb. -- Kuhn (1955) / Munkres (1957), methode hongroise pour le probleme -- d'affectation. Le lake formalise la charpente de correction : -- dualite faible LP, certificat d'optimalite a gap nul, et preservation diff --git a/MyIA.AI.Notebooks/GameTheory/limit_sets.py b/MyIA.AI.Notebooks/GameTheory/limit_sets.py index 501d775fb6..3a7d09ec16 100644 --- a/MyIA.AI.Notebooks/GameTheory/limit_sets.py +++ b/MyIA.AI.Notebooks/GameTheory/limit_sets.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Ensembles limites de l'apprentissage — Poincare-Bendixson (GameTheory-26). -Backing notebook : GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb. +Backing notebook : GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb. Source : Czechowski & Piliouras 2021, *Poincare-Bendixson Limit Sets in Multi-Agent Learning* — cite par chemin bibliotheque (G:/Mon Drive/MyIA/IA/Bibliographie IA/GameTheory/), jamais copiee. diff --git a/MyIA.AI.Notebooks/GameTheory/tests/test_limit_sets.py b/MyIA.AI.Notebooks/GameTheory/tests/test_limit_sets.py index 75d44c7b1e..d9855e860d 100644 --- a/MyIA.AI.Notebooks/GameTheory/tests/test_limit_sets.py +++ b/MyIA.AI.Notebooks/GameTheory/tests/test_limit_sets.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Tests du module limit_sets.py (GameTheory-26, Poincare-Bendixson). -Backing notebook : GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb. +Backing notebook : GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb. Ces tests assertent des **invariants de theorie connus**, pas seulement l'absence de crash : diff --git a/MyIA.AI.Notebooks/IIT/ICT-Series/ICT-35-HumorCausalProbe-Pilot.ipynb b/MyIA.AI.Notebooks/IIT/ICT-Series/ICT-35-HumorCausalProbe-Pilot.ipynb index 5fe01721fb..4a87e1146e 100644 --- a/MyIA.AI.Notebooks/IIT/ICT-Series/ICT-35-HumorCausalProbe-Pilot.ipynb +++ b/MyIA.AI.Notebooks/IIT/ICT-Series/ICT-35-HumorCausalProbe-Pilot.ipynb @@ -6,16 +6,16 @@ "id": "14791a35", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:00.161694Z", - "iopub.status.busy": "2026-09-03T17:19:00.160703Z", - "iopub.status.idle": "2026-09-03T17:19:00.165582Z", - "shell.execute_reply": "2026-09-03T17:19:00.165582Z" + "iopub.execute_input": "2026-09-11T11:06:04.920959Z", + "iopub.status.busy": "2026-09-11T11:06:04.920745Z", + "iopub.status.idle": "2026-09-11T11:06:04.926689Z", + "shell.execute_reply": "2026-09-11T11:06:04.925868Z" }, "papermill": { - "duration": 0.009412, - "end_time": "2026-09-03T17:19:00.166590", + "duration": 0.010013, + "end_time": "2026-09-11T11:06:04.927713", "exception": false, - "start_time": "2026-09-03T17:19:00.157178", + "start_time": "2026-09-11T11:06:04.917700", "status": "completed" }, "tags": [ @@ -33,10 +33,10 @@ "id": "frontmatter", "metadata": { "papermill": { - "duration": 0.002, - "end_time": "2026-09-03T17:19:00.170589", + "duration": 0.001587, + "end_time": "2026-09-11T11:06:04.931857", "exception": false, - "start_time": "2026-09-03T17:19:00.168589", + "start_time": "2026-09-11T11:06:04.930270", "status": "completed" }, "tags": [] @@ -99,7 +99,7 @@ "\n", "## Sources\n", "\n", - "- `MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb` : CORPUS_DUR 120 instances (cell[10]), labellisation manuelle par heuristique Argumentum (cf note methodo de l'acceptance).\n", + "- `MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb` : CORPUS_DUR 120 instances (cell[10]), labellisation manuelle par heuristique Argumentum (cf note methodo de l'acceptance).\n", "- `G:\\Mon Drive\\MyIA\\IA\\Bibliographie IA\\MachineLearning\\Computational Humor\\` : sources HLS (Attardo/Raskin GTVH + Mihalcea/Pulido humor stylometry), **prevues comme reference** pour la taxonomie -- la mise en archive effective est a faire dans le scope de #14033, pas de ce notebook.\n", "\n", "\n", @@ -124,16 +124,16 @@ "id": "setup", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:00.175596Z", - "iopub.status.busy": "2026-09-03T17:19:00.175596Z", - "iopub.status.idle": "2026-09-03T17:19:01.184007Z", - "shell.execute_reply": "2026-09-03T17:19:01.184007Z" + "iopub.execute_input": "2026-09-11T11:06:04.936498Z", + "iopub.status.busy": "2026-09-11T11:06:04.936244Z", + "iopub.status.idle": "2026-09-11T11:06:13.604416Z", + "shell.execute_reply": "2026-09-11T11:06:13.603933Z" }, "papermill": { - "duration": 1.012434, - "end_time": "2026-09-03T17:19:01.185022", + "duration": 8.671565, + "end_time": "2026-09-11T11:06:13.605231", "exception": false, - "start_time": "2026-09-03T17:19:00.172588", + "start_time": "2026-09-11T11:06:04.933666", "status": "completed" }, "tags": [] @@ -143,7 +143,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[setup] GT-28b present : MyIA.AI.Notebooks\\GameTheory\\GameTheory-28b-Humour-Banc-Dur.ipynb\n", + "[setup] GT-28b present : MyIA.AI.Notebooks\\GameTheory\\GameTheory-24b-Humour-Banc-Dur.ipynb\n", "[setup] seed = 13306\n", "[setup] sklearn LogisticRegression baseline (CPU)\n" ] @@ -173,7 +173,7 @@ "random.seed(SEED)\n", "np.random.seed(SEED)\n", "\n", - "GT28B = Path(\"MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb\")\n", + "GT28B = Path(\"MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb\")\n", "assert GT28B.exists(), f\"GT-28b manquant : {GT28B.resolve()}\"\n", "print(f\"[setup] GT-28b present : {GT28B}\")\n", "print(f\"[setup] seed = {SEED}\")\n", @@ -186,16 +186,16 @@ "id": "extract-corpus", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:01.190529Z", - "iopub.status.busy": "2026-09-03T17:19:01.190529Z", - "iopub.status.idle": "2026-09-03T17:19:01.336988Z", - "shell.execute_reply": "2026-09-03T17:19:01.336988Z" + "iopub.execute_input": "2026-09-11T11:06:13.610039Z", + "iopub.status.busy": "2026-09-11T11:06:13.609563Z", + "iopub.status.idle": "2026-09-11T11:06:15.077512Z", + "shell.execute_reply": "2026-09-11T11:06:15.077041Z" }, "papermill": { - "duration": 0.150972, - "end_time": "2026-09-03T17:19:01.337995", + "duration": 1.471143, + "end_time": "2026-09-11T11:06:15.078361", "exception": false, - "start_time": "2026-09-03T17:19:01.187023", + "start_time": "2026-09-11T11:06:13.607218", "status": "completed" }, "tags": [] @@ -208,9 +208,21 @@ "[extract] 5 cellules code GT-28b[0..10] a executer\n", "[setup] Catégories : ['humour_reussi', 'rire_sans_recadrage', 'recadrage_sans_rire', 'offensif_compris_non_partage', 'rien']\n", "[setup] LLM endpoint : http://192.168.0.47:5002/v1\n", - "[setup] LLM model : qwen3.6-35b-a3b\n", - "[fetch] cache hit : argumentum_scenarii.csv\n", - "[fetch] upstream master @ae84c91ce8 : fix(test): #1266 l'organe mesurait le mauvais parseur, et j'en avais tire une se\n", + "[setup] LLM model : qwen3.6-35b-a3b\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[fetch] downloaded 556796 bytes -> argumentum_scenarii.csv\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[fetch] upstream master @3fcf9a55d2 : docs(dnn): #1180 refresh go-live runbooks against current measured state (#1327)\n", "[parse] 167 scénarios Argumentum chargés\n", "[parse] catégories : {'histoire': 17, 'mythologie': 27, 'relation intime': 36, 'vie professionnelle': 30, 'vie personnelle': 25, 'pop culture': 18, 'politique': 14}\n", "[parse] sous-catégories (21) : {'antiquité': 6, 'moyen-âge et temps modernes': 6, '20e et 21e siècle': 5, 'contes': 10, 'religions': 11, 'littérature': 6, 'drague et séduction': 9, 'vie de couple': 16, 'romance': 11, 'interactions professionnelles': 14, 'relations au travail': 8, 'gestion et administration': 9, 'Bandes dessinées': 5, 'cinéma & télévision': 7, 'science': 6, 'gouvernance': 4, 'manoeuvres et collusion': 6, 'campagne': 4, 'famille et enfance': 8, 'voisins et amis': 11, 'loisirs et espace public': 5}\n", @@ -257,16 +269,16 @@ "id": "stratify", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:01.344022Z", - "iopub.status.busy": "2026-09-03T17:19:01.343507Z", - "iopub.status.idle": "2026-09-03T17:19:01.348369Z", - "shell.execute_reply": "2026-09-03T17:19:01.347827Z" + "iopub.execute_input": "2026-09-11T11:06:15.083764Z", + "iopub.status.busy": "2026-09-11T11:06:15.083317Z", + "iopub.status.idle": "2026-09-11T11:06:15.087749Z", + "shell.execute_reply": "2026-09-11T11:06:15.087327Z" }, "papermill": { - "duration": 0.008374, - "end_time": "2026-09-03T17:19:01.348369", + "duration": 0.008005, + "end_time": "2026-09-11T11:06:15.088415", "exception": false, - "start_time": "2026-09-03T17:19:01.339995", + "start_time": "2026-09-11T11:06:15.080410", "status": "completed" }, "tags": [] @@ -311,8 +323,7 @@ "\n", "print(f\"[stratify] humour_reussi = {len(H_REUSSI)} instances\")\n", "print(f\"[stratify] non-humour = {len(NON_HUMOUR)} instances (toutes disponibles)\")\n", - "print(f\"[stratify] ratio H:N = 1:{len(NON_HUMOUR)//len(H_REUSSI)}\")\n", - "" + "print(f\"[stratify] ratio H:N = 1:{len(NON_HUMOUR)//len(H_REUSSI)}\")\n" ] }, { @@ -321,16 +332,16 @@ "id": "match-pairs", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:01.354715Z", - "iopub.status.busy": "2026-09-03T17:19:01.354715Z", - "iopub.status.idle": "2026-09-03T17:19:01.362989Z", - "shell.execute_reply": "2026-09-03T17:19:01.362443Z" + "iopub.execute_input": "2026-09-11T11:06:15.093177Z", + "iopub.status.busy": "2026-09-11T11:06:15.092979Z", + "iopub.status.idle": "2026-09-11T11:06:15.101794Z", + "shell.execute_reply": "2026-09-11T11:06:15.100808Z" }, "papermill": { - "duration": 0.011944, - "end_time": "2026-09-03T17:19:01.362989", + "duration": 0.012413, + "end_time": "2026-09-11T11:06:15.102736", "exception": false, - "start_time": "2026-09-03T17:19:01.351045", + "start_time": "2026-09-11T11:06:15.090323", "status": "completed" }, "tags": [] @@ -409,8 +420,7 @@ "# distribues par stratification de la classe positive.\n", "GROUP_KEY_H = [p[0]['texte'] for p in PAIRES_30] # 30 cles, 6 uniques\n", "n_groupes_uniques = len(set(GROUP_KEY_H))\n", - "print(f\"[match] Groupes uniques (textes positifs sources) = {n_groupes_uniques} (max {len(PAIRES_30)//n_groupes_uniques} repetitions par groupe)\")\n", - "" + "print(f\"[match] Groupes uniques (textes positifs sources) = {n_groupes_uniques} (max {len(PAIRES_30)//n_groupes_uniques} repetitions par groupe)\")\n" ] }, { @@ -419,16 +429,16 @@ "id": "lexical-features", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:01.369705Z", - "iopub.status.busy": "2026-09-03T17:19:01.369705Z", - "iopub.status.idle": "2026-09-03T17:19:01.377387Z", - "shell.execute_reply": "2026-09-03T17:19:01.376843Z" + "iopub.execute_input": "2026-09-11T11:06:15.107633Z", + "iopub.status.busy": "2026-09-11T11:06:15.107417Z", + "iopub.status.idle": "2026-09-11T11:06:15.114098Z", + "shell.execute_reply": "2026-09-11T11:06:15.113507Z" }, "papermill": { - "duration": 0.012364, - "end_time": "2026-09-03T17:19:01.378399", + "duration": 0.01034, + "end_time": "2026-09-11T11:06:15.115046", "exception": false, - "start_time": "2026-09-03T17:19:01.366035", + "start_time": "2026-09-11T11:06:15.104706", "status": "completed" }, "tags": [] @@ -520,16 +530,16 @@ "id": "build-Xy", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:01.384035Z", - "iopub.status.busy": "2026-09-03T17:19:01.384035Z", - "iopub.status.idle": "2026-09-03T17:19:01.394476Z", - "shell.execute_reply": "2026-09-03T17:19:01.394476Z" + "iopub.execute_input": "2026-09-11T11:06:15.120987Z", + "iopub.status.busy": "2026-09-11T11:06:15.120666Z", + "iopub.status.idle": "2026-09-11T11:06:15.137525Z", + "shell.execute_reply": "2026-09-11T11:06:15.137082Z" }, "papermill": { - "duration": 0.015568, - "end_time": "2026-09-03T17:19:01.395483", + "duration": 0.020175, + "end_time": "2026-09-11T11:06:15.138246", "exception": false, - "start_time": "2026-09-03T17:19:01.379915", + "start_time": "2026-09-11T11:06:15.118071", "status": "completed" }, "tags": [] @@ -628,16 +638,16 @@ "id": "cv-baseline", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:01.401483Z", - "iopub.status.busy": "2026-09-03T17:19:01.400484Z", - "iopub.status.idle": "2026-09-03T17:19:01.418332Z", - "shell.execute_reply": "2026-09-03T17:19:01.418332Z" + "iopub.execute_input": "2026-09-11T11:06:15.143047Z", + "iopub.status.busy": "2026-09-11T11:06:15.142806Z", + "iopub.status.idle": "2026-09-11T11:06:15.169426Z", + "shell.execute_reply": "2026-09-11T11:06:15.169002Z" }, "papermill": { - "duration": 0.021855, - "end_time": "2026-09-03T17:19:01.419338", + "duration": 0.029901, + "end_time": "2026-09-11T11:06:15.170191", "exception": false, - "start_time": "2026-09-03T17:19:01.397483", + "start_time": "2026-09-11T11:06:15.140290", "status": "completed" }, "tags": [] @@ -652,8 +662,8 @@ "[cv-v5] fold 2 n_train=24 n_test=12 F1=0.400\n", "[cv-v5] F1 group-level baseline = 0.229 +/- 0.168\n", "[cv-v5] baseline random (ratio classe) F1 = 0.286\n", - "[cv-v5] P(y=1|x) OOF moyen sur H = 0.372 +/- 0.306\n", - "[cv-v5] P(y=1|x) OOF par H unique : ['0.632', '0.046', '0.214', '0.220', '0.193', '0.926']\n" + "[cv-v5] P(y=1|x) OOF moyen sur H = 0.373 +/- 0.303\n", + "[cv-v5] P(y=1|x) OOF par H unique : ['0.629', '0.048', '0.214', '0.220', '0.203', '0.923']\n" ] } ], @@ -700,16 +710,16 @@ "id": "control-shuffle", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:01.425342Z", - "iopub.status.busy": "2026-09-03T17:19:01.424342Z", - "iopub.status.idle": "2026-09-03T17:19:02.398801Z", - "shell.execute_reply": "2026-09-03T17:19:02.398276Z" + "iopub.execute_input": "2026-09-11T11:06:15.175212Z", + "iopub.status.busy": "2026-09-11T11:06:15.174918Z", + "iopub.status.idle": "2026-09-11T11:06:16.159959Z", + "shell.execute_reply": "2026-09-11T11:06:16.159582Z" }, "papermill": { - "duration": 0.977985, - "end_time": "2026-09-03T17:19:02.399324", + "duration": 0.988595, + "end_time": "2026-09-11T11:06:16.160773", "exception": false, - "start_time": "2026-09-03T17:19:01.421339", + "start_time": "2026-09-11T11:06:15.172178", "status": "completed" }, "tags": [] @@ -720,11 +730,11 @@ "output_type": "stream", "text": [ "[perm-v6] distribution nulle (N_PERM=100 moyennes CV) :\n", - " mean = 0.165 std = 0.120\n", + " mean = 0.162 std = 0.119\n", " IC95 = [0.000, 0.385]\n", - " null >= reel = 28/100\n", - " p_value fini (k+1)/(N+1) = (1+28)/101 = 0.2871\n", - " F1 reel = 0.229, delta (reel - perm_mean) = +0.064\n", + " null >= reel = 27/100\n", + " p_value fini (k+1)/(N+1) = (1+27)/101 = 0.2772\n", + " F1 reel = 0.229, delta (reel - perm_mean) = +0.067\n", "[perm-v6] VERDICT PERMUTATION : signal NON significatif (p >= 0.05) -- la discrimination observee est compatible avec le null intra-fold.\n" ] } @@ -802,8 +812,7 @@ " print(\"[perm-v6] VERDICT PERMUTATION : signal significatif (p < 0.05).\")\n", "else:\n", " print(\"[perm-v6] VERDICT PERMUTATION : signal NON significatif (p >= 0.05) -- \"\n", - " \"la discrimination observee est compatible avec le null intra-fold.\")\n", - "" + " \"la discrimination observee est compatible avec le null intra-fold.\")\n" ] }, { @@ -812,16 +821,16 @@ "id": "control-surface", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:02.405415Z", - "iopub.status.busy": "2026-09-03T17:19:02.405415Z", - "iopub.status.idle": "2026-09-03T17:19:02.429887Z", - "shell.execute_reply": "2026-09-03T17:19:02.429887Z" + "iopub.execute_input": "2026-09-11T11:06:16.165496Z", + "iopub.status.busy": "2026-09-11T11:06:16.165309Z", + "iopub.status.idle": "2026-09-11T11:06:16.185816Z", + "shell.execute_reply": "2026-09-11T11:06:16.185469Z" }, "papermill": { - "duration": 0.028833, - "end_time": "2026-09-03T17:19:02.430906", + "duration": 0.023531, + "end_time": "2026-09-11T11:06:16.186475", "exception": false, - "start_time": "2026-09-03T17:19:02.402073", + "start_time": "2026-09-11T11:06:16.162944", "status": "completed" }, "tags": [] @@ -835,11 +844,11 @@ "[surf-v5] fold 1 n_H_test=2 rappel_baseline=0.500\n", "[surf-v5] fold 2 n_H_test=2 rappel_baseline=0.500\n", "[surf-v5] rappel (sensibilite) baseline sur H test = 0.333 +/- 0.236\n", - "[surf-v5] P(y=1) original OOF sur H = [0.21412721 0.21989585 0.19264743 0.9259528 0.63177013 0.04638032]\n", - "[surf-v5] P(y=1) surface OOF sur H = [0.331 0.361 0.072 0.227 0.141 0.033]\n", - "[surf-v5] delta_proba OOF (original - surface) par H : [-0.117 -0.141 0.12 0.699 0.491 0.014]\n", - "[surf-v5] delta_proba OOF mean = 0.178 +/- 0.313\n", - "[surf-v5] delta_proba OOF IC95 = [-0.138, 0.673]\n", + "[surf-v5] P(y=1) original OOF sur H = [0.21412721 0.21989585 0.20303053 0.9231509 0.62931408 0.04835588]\n", + "[surf-v5] P(y=1) surface OOF sur H = [0.331 0.361 0.076 0.226 0.141 0.034]\n", + "[surf-v5] delta_proba OOF (original - surface) par H : [-0.117 -0.141 0.127 0.697 0.488 0.014]\n", + "[surf-v5] delta_proba OOF mean = 0.178 +/- 0.312\n", + "[surf-v5] delta_proba OOF IC95 = [-0.138, 0.671]\n", "[surf-v5] VERDICT SURFACE : strip n'a pas d'effet clair (IC95 traverse 0) -- discrimination purement lexicale de surface.\n" ] } @@ -935,16 +944,16 @@ "id": "verdict", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:02.437407Z", - "iopub.status.busy": "2026-09-03T17:19:02.437407Z", - "iopub.status.idle": "2026-09-03T17:19:02.443124Z", - "shell.execute_reply": "2026-09-03T17:19:02.442615Z" + "iopub.execute_input": "2026-09-11T11:06:16.191085Z", + "iopub.status.busy": "2026-09-11T11:06:16.190919Z", + "iopub.status.idle": "2026-09-11T11:06:16.196912Z", + "shell.execute_reply": "2026-09-11T11:06:16.196550Z" }, "papermill": { - "duration": 0.010435, - "end_time": "2026-09-03T17:19:02.444129", + "duration": 0.008989, + "end_time": "2026-09-11T11:06:16.197523", "exception": false, - "start_time": "2026-09-03T17:19:02.433694", + "start_time": "2026-09-11T11:06:16.188534", "status": "completed" }, "tags": [] @@ -958,16 +967,16 @@ "VERDICT -- Etage 0 lexical baseline ICT-35 (GROUP-LEVEL, c.945 v6)\n", "======================================================================\n", " F1 group-level baseline = 0.229 +/- 0.168\n", - " F1 null distribution (100 perms) = mean 0.165 +/- 0.120\n", + " F1 null distribution (100 perms) = mean 0.162 +/- 0.119\n", " F1 null IC95 = [0.000, 0.385]\n", - " p_value fini (k+1)/(N+1) = 0.2871\n", - " null >= reel = 28/100\n", + " p_value fini (k+1)/(N+1) = 0.2772\n", + " null >= reel = 27/100\n", " Rappel (sensibilite) baseline H = 0.333\n", - " delta_proba OOF H (orig - surf) = 0.178 +/- 0.313\n", - " delta_proba OOF H IC95 = [-0.138, 0.673]\n", + " delta_proba OOF H (orig - surf) = 0.178 +/- 0.312\n", + " delta_proba OOF H IC95 = [-0.138, 0.671]\n", "\n", " VERDICT = INCONCLUSIVE borne\n", - " RAISON = p_value fini = 0.2871 (k+1)/(N+1) >= 0.05 : la discrimination observee est compatible avec le null intra-fold (100 permutations, 1 moyenne CV par perm, mean 0.165). Borne sur ce sous-corpus (36 unites group-level, 6 H uniques) -- pas une falsification de l'humour lexical en general. Le verdict ne peut etre plus precis que 'borne sur ce sous-corpus' avec ce N et 6 features.\n", + " RAISON = p_value fini = 0.2772 (k+1)/(N+1) >= 0.05 : la discrimination observee est compatible avec le null intra-fold (100 permutations, 1 moyenne CV par perm, mean 0.162). Borne sur ce sous-corpus (36 unites group-level, 6 H uniques) -- pas une falsification de l'humour lexical en general. Le verdict ne peut etre plus precis que 'borne sur ce sous-corpus' avec ce N et 6 features.\n", "\n", "======================================================================\n", "LIMITES DE CE VERDICT (reformulees -- ce n'est PAS une falsification)\n", @@ -1049,8 +1058,7 @@ "print(\"5. 1 seul modele LLM (qwen3.6-35b-a3b) sert a labelliser ; le verdict est local a ce couple.\")\n", "print(\"6. Pipeline intra-fold integre (fix c.917) + StratifiedKFold(n_splits=3) sur 36 unites (fix c.920 fuite groupe).\")\n", "print(\"7. delta_proba sur 6 H : IC95 traverse 0 trivialement sur N=6 ; on declare 'IC traverse 0'\")\n", - "print(\" sans pretendre que c'est la preuve d'une discrimination purement lexicale de surface.\")\n", - "" + "print(\" sans pretendre que c'est la preuve d'une discrimination purement lexicale de surface.\")\n" ] }, { @@ -1059,16 +1067,16 @@ "id": "phase2-perspective", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T17:19:02.450144Z", - "iopub.status.busy": "2026-09-03T17:19:02.450144Z", - "iopub.status.idle": "2026-09-03T17:19:02.454237Z", - "shell.execute_reply": "2026-09-03T17:19:02.454237Z" + "iopub.execute_input": "2026-09-11T11:06:16.202279Z", + "iopub.status.busy": "2026-09-11T11:06:16.202093Z", + "iopub.status.idle": "2026-09-11T11:06:16.205684Z", + "shell.execute_reply": "2026-09-11T11:06:16.205089Z" }, "papermill": { - "duration": 0.009116, - "end_time": "2026-09-03T17:19:02.455243", + "duration": 0.006989, + "end_time": "2026-09-11T11:06:16.206507", "exception": false, - "start_time": "2026-09-03T17:19:02.446127", + "start_time": "2026-09-11T11:06:16.199518", "status": "completed" }, "tags": [] @@ -1120,10 +1128,10 @@ "id": "navlinks", "metadata": { "papermill": { - "duration": 0.003344, - "end_time": "2026-09-03T17:19:02.460588", + "duration": 0.001849, + "end_time": "2026-09-11T11:06:16.210381", "exception": false, - "start_time": "2026-09-03T17:19:02.457244", + "start_time": "2026-09-11T11:06:16.208532", "status": "completed" }, "tags": [] @@ -1134,7 +1142,7 @@ "- **#14035** -- issue de reference, protocole falsifiable, acceptance (cible haute).\n", "- **#14032** -- consolidation de la methode de labellisation (GT-28 / GT-28b).\n", "- **#14033** -- substrat taxonomique croissant (corpus apparie + 3 labels + taxonomie sourcee).\n", - "- **`MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb`** -- CORPUS_DUR 120 instances, source de verite.\n", + "- **`MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb`** -- CORPUS_DUR 120 instances, source de verite.\n", "- **`MyIA.AI.Notebooks/IIT/ICT-Series/ICT-21-SAETrajectories.ipynb`** -- substrat S4 (SAE Qwen3.5-9B), aval + tard.\n", "- **`MyIA.AI.Notebooks/IIT/ICT-Series/ICT-24-WorkspaceIgnition.ipynb`** -- Gate 24, gated etages superieurs (GPU).\n", "- **#5635, #8236, #5105, #4588** -- aval ICT-Series.\n", @@ -1159,23 +1167,23 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.13.3" }, "papermill": { "default_parameters": {}, - "duration": 3.901545, - "end_time": "2026-09-03T17:19:02.711359", + "duration": 13.561893, + "end_time": "2026-09-11T11:06:16.560272", "environment_variables": {}, "exception": null, - "input_path": "MyIA.AI.Notebooks/IIT/ICT-Series/ICT-35-HumorCausalProbe-Pilot.ipynb", + "input_path": "ICT-35-HumorCausalProbe-Pilot.ipynb", "output_path": "ICT-35-HumorCausalProbe-Pilot.ipynb", "parameters": { "nb_name": "ICT-35-HumorCausalProbe-Pilot" }, - "start_time": "2026-09-03T17:18:58.809814", + "start_time": "2026-09-11T11:06:02.998379", "version": "2.6.0" } }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/_quarto.yml b/_quarto.yml index 8890f4f83f..2734be9602 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -530,14 +530,14 @@ project: - "MyIA.AI.Notebooks/GameTheory/GameTheory-17d-Lean-Screening-Signaling.ipynb" - "MyIA.AI.Notebooks/GameTheory/GameTheory-18b-Casser-la-Composition.ipynb" - "MyIA.AI.Notebooks/GameTheory/GameTheory-19-Abstraction-a-Dette.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-25-Loi-II-Translateur-Life.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-27-Munkres-Assignment.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-27b-Lean-Assignment-Native.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-28-Humour-Banc.ipynb" - - "MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-21-Loi-II-Translateur-Life.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-23-Munkres-Assignment.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-23b-Lean-Assignment-Native.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-24-Humour-Banc.ipynb" + - "MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb" - "MyIA.AI.Notebooks/GameTheory/SocialChoice/01-Arrow-Impossibility-Theorem-Csharp.ipynb" - "MyIA.AI.Notebooks/GameTheory/SocialChoice/01-Arrow-Impossibility-Theorem.ipynb" - "MyIA.AI.Notebooks/GameTheory/SocialChoice/01b-Lean-SocialChoice-Formal.ipynb" diff --git a/docs/curriculum/recherche.md b/docs/curriculum/recherche.md index 354561a960..016cb8d5fb 100644 --- a/docs/curriculum/recherche.md +++ b/docs/curriculum/recherche.md @@ -110,13 +110,13 @@ Inférence probabiliste (Infer.NET, Pyro, PyMC), théorie de l'information inté | 79 | [GameTheory-18 : Open Games et Lentilles -- la…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-18-Open-Games-et-Lentilles.ipynb) | BETA | Oui | | 80 | [GameTheory-18b : Casser la composition — où la…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-18b-Casser-la-Composition.ipynb) | BETA | Oui | | 81 | [GameTheory-19 : L'abstraction a dette mesurable](../../MyIA.AI.Notebooks/GameTheory/GameTheory-19-Abstraction-a-Dette.ipynb) | BETA | Oui | -| 82 | [GameTheory 24b : Le temoin d'impossibilite](../../MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb) | BETA | Oui | -| 83 | [GameTheory-25 — Loi II, seconde jambe : synthétiser un…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-25-Loi-II-Translateur-Life.ipynb) | BETA | Oui | -| 84 | [GameTheory-26 — Ensembles limites : Poincaré-Bendixson…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-26-Ensembles-Limites-Poincare-Bendixson.ipynb) | BETA | Oui | -| 85 | [GameTheory-27 — L'algorithme de Kuhn-Munkres :…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-27-Munkres-Assignment.ipynb) | BETA | Oui | -| 86 | [GameTheory 27b — Le lake assignment_lean par son…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-27b-Lean-Assignment-Native.ipynb) | BETA | Non | -| 87 | [GameTheory-28 : Banc de calibration — humour, forme…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-28-Humour-Banc.ipynb) | BETA | Oui | -| 88 | [GameTheory-28b : Banc humour — passer à l'échelle](../../MyIA.AI.Notebooks/GameTheory/GameTheory-28b-Humour-Banc-Dur.ipynb) | BETA | Non | +| 82 | [GameTheory 20b : Le temoin d'impossibilite](../../MyIA.AI.Notebooks/GameTheory/GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb) | BETA | Oui | +| 83 | [GameTheory-21 — Loi II, seconde jambe : synthétiser un…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-21-Loi-II-Translateur-Life.ipynb) | BETA | Oui | +| 84 | [GameTheory-22 — Ensembles limites : Poincaré-Bendixson…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-22-Ensembles-Limites-Poincare-Bendixson.ipynb) | BETA | Oui | +| 85 | [GameTheory-23 — L'algorithme de Kuhn-Munkres :…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-23-Munkres-Assignment.ipynb) | BETA | Oui | +| 86 | [GameTheory 23b — Le lake assignment_lean par son…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-23b-Lean-Assignment-Native.ipynb) | BETA | Non | +| 87 | [GameTheory-24 : Banc de calibration — humour, forme…](../../MyIA.AI.Notebooks/GameTheory/GameTheory-24-Humour-Banc.ipynb) | BETA | Oui | +| 88 | [GameTheory-24b : Banc humour — passer à l'échelle](../../MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Humour-Banc-Dur.ipynb) | BETA | Non | ## GameTheory/SocialChoice (8 notebooks) diff --git a/slides/05-theorie-des-jeux/slides.md b/slides/05-theorie-des-jeux/slides.md index 33a5560a6a..bf063e3cf9 100644 --- a/slides/05-theorie-des-jeux/slides.md +++ b/slides/05-theorie-des-jeux/slides.md @@ -201,7 +201,7 @@ layout: dense Regle impaire : nombre d'equilibres d'un jeu
-*Notebooks : [GameTheory-03-Topology2x2](../../MyIA.AI.Notebooks/GameTheory/GameTheory-03-Topology2x2.ipynb) (dominance faible, regle impaire) · [GameTheory-24-Chemin-Minimal-Robinson-Goforth](../../MyIA.AI.Notebooks/GameTheory/GameTheory-24-Chemin-Minimal-Robinson-Goforth.ipynb) · [GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite](../../MyIA.AI.Notebooks/GameTheory/GameTheory-24b-Chemin-Minimal-Temoins-Impossibilite.ipynb).* +*Notebooks : [GameTheory-03-Topology2x2](../../MyIA.AI.Notebooks/GameTheory/GameTheory-03-Topology2x2.ipynb) (dominance faible, regle impaire) · [GameTheory-20-Chemin-Minimal-Robinson-Goforth](../../MyIA.AI.Notebooks/GameTheory/GameTheory-20-Chemin-Minimal-Robinson-Goforth.ipynb) · [GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite](../../MyIA.AI.Notebooks/GameTheory/GameTheory-20b-Chemin-Minimal-Temoins-Impossibilite.ipynb).* --- layout: dense From 3c2dad7a9fa32df519d6c0911349fe593f0e3a7b Mon Sep 17 00:00:00 2001 From: jsboige Date: Sat, 12 Sep 2026 02:09:40 +0200 Subject: [PATCH 10/10] =?UTF-8?q?fix(gametheory,#15210):=20retirer=20metad?= =?UTF-8?q?ata.papermill=20stale=20=E2=80=94=20outputs=20cell5=20changed?= =?UTF-8?q?=20post=20cherry-pick=20c.1049=20REPAIR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tell c.1067-L1 ★ NEW causal root reproduit : re-exécution locale c.1049 REPAIR #15619 a régénéré outputs cell5 ("Matrice reproductible (oracle declaratif v2) : 25/25 confrontations agree.\nDeux organes de verification : moteur (cellule 18) ET oracle [...]") mais sans toucher au bloc `metadata.papermill` global — qui décrit l'exécution Papermill d'origine (c.987). Ratchet rougit : "outputs/execution_count changed but the metadata.papermill block is identical to origin/main". Fix : retirer `metadata.papermill` global. Le docstring du ratchet (check_papermill_ratchet.py l.30-31) autorise explicitement : "block absent at head (removed - explicitly allowed)". Verdict attendu : BLOCK_REMOVED, 0 regression. Vérification locale : python scripts/notebook_tools/check_papermill_ratchet.py origin/main → BLOCK_REMOVED, 0 regressions. Tell c.974 strict 1 amend c.1067 — pas de rebase destructeur, pas de modif substance, fix metadata only. Co-Authored-By: Claude Haiku 4.5 (1M context) --- .../GameTheory-06e-Open-Source-Game-Theory.ipynb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-06e-Open-Source-Game-Theory.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-06e-Open-Source-Game-Theory.ipynb index f67762476b..a7f65a2523 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-06e-Open-Source-Game-Theory.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-06e-Open-Source-Game-Theory.ipynb @@ -1187,18 +1187,6 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.7" - }, - "papermill": { - "default_parameters": {}, - "duration": 5.222232, - "end_time": "2026-09-08T05:49:38.580142+00:00", - "environment_variables": {}, - "exception": null, - "input_path": "MyIA.AI.Notebooks\\GameTheory\\GameTheory-06e-Open-Source-Game-Theory.ipynb", - "output_path": "C:\\Users\\jsboi\\AppData\\Local\\Temp\\claude\\c--dev-CoursIA-2\\b7fd0659-297b-4996-861e-5419adefc1c3\\scratchpad\\c987_game06e_exec.ipynb", - "parameters": {}, - "start_time": "2026-09-08T05:49:33.357910+00:00", - "version": "2.7.0" } }, "nbformat": 4,